DBPro Newcomers / Beginners Guide To The Camera Tutorial
Joined: Tue Nov 19th 2002
Please note that this is part of a set of tutorials, all linked to an as yet unfinished new Part 5 of the 'Dark Basic Programming For Beginners' series found here:
As such, there may be references to topics covered in the main tutorial which has not yet been completed, so although as a stand-alone reference this tutorial may be of use to you, please bear this in mind.
TDK's Beginners Guide To The Dark Basic Camera
Note: In Dark Basic Professional you can have multiple cameras, but to retain compatibility with Dark Basic V1 (Classic) and Pro, only the default camera (camera 0) will be used throughout this tutorial. Using the default camera in DBPro is assumed if you don't supply the camera number parameter.
In 3D, what you see of your world depends on where in 3D space your camera is placed and where it is pointing. You place the camera in the required position with the command:
Position Camera CamXPos#,CamYPos#,CamZPos#
...where CamXPos# is the X co-ordinate in 3D space, CamYPos# is the Y co-ordinate in 3D space and CamZPos# is the Z co-ordinate in 3D space.
You will notice that I've placed the hash (#) symbol after each of the three parameters. This is because the Position Camera command requires that the three parameters all be floating point values. As such, you should always use values like 15.0 instead of 15 (or float variables with the # symbol on the end), even though it does still work with integers.
Note: 3D space units are floats not integers because you need the ability to move objects (and the camera) distances less than 1 3D unit. If you use integers, then moving things around can result in jerky movement or things simply moving too fast. Sometimes you need to be able to move an object 0.2 3D units and the only way to do that is if you make variables which are used for 3D movement floats.
You can supply the command with literal parameters (actual numbers), like this:
Position Camera 140.0,10.0,250.0
...or use float variables like this:
Position Camera CamX#,CamY#,CamZ#
The first is OK if your camera is fixed and never moves, but if the action in your game moves, it won't be long before it's a small speck in the distance. So, the ability to move the camera is pretty essential.
You set where the camera is looking with the command:
Point Camera 3DXPos#,3DYPos#,3DZPos#
...where 3DXPos# is the X co-ordinate in 3D space, 3DYPos# is the Y co-ordinate in 3D space and CamZPos# is the Z co-ordinate in 3D space. As with Position Camera, the parameters should be float values/variables even though integers will work, so stick to floats.
Once again, you can use literal float values for the parameters, but if the camera is required to look at different places, you will need to use float variables. A common use of Point Camera is to track an object by using the Object Position functions:
Point Camera Object Position X(1),Object Position Y(1),Object Position Z(1)
...will always point the camera at the object with the number in the brackets - in this example, object 1. However, in most programs, you will probably already have the object's X, Y and Z positions stored in variables and you would be able to use something like:
Point Camera ObjPosX#,ObjPosY#,ObjPosZ#
With these two commands you can move the camera around your 3D world and point it wherever you wish. But what if you don't have any co-ordinates (or objects) to point the camera at?
Well, you also have commands to rotate the camera in the same way as rotating objects. The main one is Rotate Camera which is used like this:
Rotate Camera CamAngleX#,CamAngleY#,CamAngleZ#
This command lets you specify the X, Y and Z angles of the camera and the variables are usually pre-calculated by getting the current angles with the Camera Angle X(), Camera Angle Y() and Camera Angle Z() functions then adding or subtracting a value from them. For example:
CamAngleY# = WrapValue(Camera Angle Y() + 0.1)
Rotate Camera CamAngleX#,CamAngleY#,CamAngleZ#
This will get the camera's current Y angle into the float variable CamAngleY# then add 0.1 to it. The resulting value in CamAngleY# is then used in the Rotate Camera command.
Angles can be between 0º and 359º (never 360º as that's the same direction as 0º) so the WrapValue() function ensures that the resulting angle is 'wrapped' around to 1º if adding a value hits 360º - or to 359º if deducting a value results in a number less than 0 (zero).
Note: It is assumed above that Camera Angle X() and Camera Angle Z() are also calculated (or given a fixed value at the start of your program). If you don't they will by defult both equal 0.0 (zero).
The most basic game camera control is the First Person 'MouseLook' where the camera shows where the player is looking - as if through the game character's eyes. First Person is the meaning of the FP in FPS (First Person Shooter).
This is done by simply taking the movement of the mouse on the 2D X axis (left and right), translating it to the 3D Y axis and taking the movement of the mouse on the 2D Y axis (up and down), translating it to the 3D X axis. If this sounds strange, consider this:
3D has three axis - X, Y and Z. If you rotate the camera around the Y axis (which runs vertically through the camera), the camera will look left/right (turning like a flag pole in a hole at a golf course). So, to turn the camera left and right by using the mouse X position moving along the 2D X axis, we have to alter the camera's Y axis.
If we want to make the camera look up and down, we have to alter the camera's X axis - which is like the twist grip accellerator on a motorcycle (towards you and away from you). Therefore, if the mouse is moved away from you, altering the mouse Y position, we have to translate the movement to the camera's X axis.
Let's see the MouseLook snippet:
+ Code Snippet
Rem TDK's Camera Snippets Rem Snippet 1 - 1st Person, Standard MouseLook Gosub Setup Do Gosub MouseLook If SpaceKey() If MType = 0 MType = 1: MM$ = "CurveAngle Smoothing" Else MType = 0: MM$ = "No Smoothing" Endif Repeat Until SpaceKey() = 0 Endif Sync Text 5,5,"Mouse Method: "+MM$ Loop MouseLook: CX#=Camera Angle X(): CY#=Camera Angle Y(): CZ#=Camera Angle Z() If MType = 0 CY#=Wrapvalue(CY#+mousemovex()) CX#=Wrapvalue(CX#+mousemovey()) Else CY# = CurveAngle(CY#+mousemovex(),CY#,10.0) CX# = CurveAngle(CX#+mousemovey(),CX#,10.0) Endif Rotate Camera CX#,CY#,CZ# Return Setup: Set Display Mode 800,600,32 Sync On: Sync Rate 0 Make Matrix 1,500,500,20,20 CamX# = 250.0: CamY# = 1.0: CamZ# = 250.0 Position Camera CamX#,CamY#,CamZ# Hide Mouse MType = 0: MM$ = "No Smoothing" Return
The first thing we do is Gosub the Setup procedure which sets the screen mode and required syncing. Next it creates a matrix for a reference point while moving around and then hides the mouse. The Return on the end of the Setup procedure sends the program back to the Gosub Setup line and continues with the next line - dropping into the main Do..Loop.
The first line of the main Do..Loop is Gosub MouseLook which jumps to the MouseLook subroutine.
There, it grabs the camera's current X, Y and Z angles into float variables. We don't actually need the Z angle as we only need to rotate the camera up/down on the X axis and left/right on the Y axis, but it's useful to have for using with the Rotate Camera command later.
You will notice that there are two places where we say CX#= and CY#=, which looks a bit odd. That's simply because where CX# and CY# is set the second time, we have to add the MouseMove value to CX# and CY# so we must at that point have already grabbed their current values using the Camera Angle functions.
The way most people do MouseLook is what you will see on lines 21 and 22:
CY# = Wrapvalue(CY#+MouseMoveX())
CX# = Wrapvalue(CX#+MouseMoveY())
...and is what was described above - add the amount of mouse movement to the current camera angle using WrapValue to keep the resulting angle within a legal range.
However, although it works, you will find that sharp movements of the mouse can look quite jerky. So, instead, I use another Dark Basic function called CurveAngle() which will generate angles moving from a start value to an end value at a certain speed:
So, if your camera's X angle is say 30.0º and you move the mouse quickly right, the WrapValue() version will simply add a large value to the 30º and the camera will suddenly be pointing in the new direction.
Using CurveAngle(), you would set EndAngle# to 30.0º plus the amount the mouse moved, StartAngle# to 30.0º and Delay# to the speed you want the transition to take.
The end result is that the camera turning will be nice and smooth instead of being jerky. The lower the number used for Delay#, the quicker the camera gets to the new position and the higher the number, the slower (smoother) the movement is.
To demonstrate this, I've included the code for both versions in this snippet so you can see the difference this small modification makes. When the program is running, you can press the Space Bar to toggle between the smoothed CurveAngle() part of the snippet and the non-smoothed WrapValue() part.
Obviously the code for both versions won't be required in your program as I'm pretty sure you will want to use only the CurveAngle() version when you've seen it in action.
To quickly explain the rest of the additional code:
In the main program Do..Loop, we check to see if the Space Bar is being pressed with the SpaceKey() function, (this way of using it implies that you are checking for it returning true - which we are). If it is, we check the current state of the MType variable to see if it is currently 0.
The first time the Space Bar is pressed, MType will be 0 as we set it to 0 in the Setup subroutine, so as we have just turned it on we now have to set it to 1. As smoothing is now on, we set the string variable MM$ - which is for the on-screen message - to "CurveAngle Smoothing".
If MType doesn't equal 0 then smoothing is currently on, so we need to turn it off by setting MType back to 0 and change the message string variable MM$ back to "No Smoothing".
The Repeat..Until at the end of the If SpaceKey() block simply traps program control until the Space Bar is released. Without this, the smoothing would simply switch on and off continuously while the Space Bar was pressed. With it, the Space Bar only switches the state once and won't switch again until the Space Bar has been released and pressed again.
In the MouseLook subroutine, after we get the camera's X, Y and Z angles, we check the MType variable to see if it is currently 0 (smoothing off) or not (smoothing on) and use the WrapValue() or CurveAngle() code accordingly before rotating the camera using the new angles.
OK, that's fine for looking around, but what about moving around in a First Person game?
Well, you basically have two options to control camera movement. Ignoring joysticks and other peripherals which other users may not have, they are the mouse and the keyboard.
There are also two camera positioning commands, Move Camera and Position Camera - the easiest method being the Move Camera command:
Move Camera Distance#
This moves the camera the distance you specify (which should be a float value) in the direction the camera is pointing. So, the camera must be either pointed at something or it's angles correctly set before it is moved. The first moving example snippet we will look at is moving the camera with the mouse buttons - which is a very small modification to the first code snippet (minus all the smoothing code):
+ Code Snippet
Rem TDK's Camera Snippets Rem Snippet 2 - 1st Person, Mouse Buttons Move, MouseLook Gosub Setup Do Gosub MouseLook If MouseClick()=1 Then Move Camera 0.1 If MouseClick()=2 Then Move Camera -0.1 Sync Loop MouseLook: CX#=Camera Angle X(): CY#=Camera Angle Y(): CZ#=Camera Angle Z() CY# = CurveAngle(CY#+mousemovex(),CY#,10.0) CX# = CurveAngle(CX#+mousemovey(),CX#,10.0) Rotate Camera CX#,CY#,CZ# Return Setup: Set Display Mode 800,600,32 Sync On Sync Rate 0 Make Matrix 1,500,500,20,20 CamX# = 250.0 CamY# = 1.0 CamZ# = 250.0 Position Camera CamX#,CamY#,CamZ# Hide Mouse Return
This is exactly the same as the smoothed MouseLook code but with two extra lines:
If MouseClick()=1 Then Move Camera 0.1
If MouseClick()=2 Then Move Camera -0.1
The first line checks the state of the mouse buttons using the MouseClick() function. Each mouse button uses a binary bit to store it's on/off state. A byte is 8 binary bits and the mouse uses the first three (least significant) of these 8 bits set to 0 or 1 to store the button positions.
000 - No mouse buttons pressed
001 - Left mouse button pressed
010 - Right mouse button pressed
100 - Middle mouse button pressed
011 - Left and right mouse buttons both pressed
111 - Left, right and middle buttons all pressed.
The value MouseClick() returns is simply the decimal equivalent of the binary numbers created with these three bits. So...
If MouseClick() returns 0 then no mouse buttons are being pressed (000).
If MouseClick() returns 1 then the left mouse button is being pressed (001).
If MouseClick() returns 2 then the right mouse button is being pressed (010).
If MouseClick() returns 3 then the left AND right mouse buttons are being pressed (011).
If MouseClick() returns 4 then the middle mouse button is being pressed (100).
And so on for all the permutations...
As such, the first line above moves the camera 0.1 3D units towards the direction it is facing when the left mouse button is pressed and the second line moves the camera 0.1 3D units away from the direction it is facing when the right mouse button is pressed.
As well as using Move Camera, you also have the Position Camera command which is slightly more involved as you have to calculate the X, Y and Z co-ordinates in 3D space you want to move the camera to. This method can make working with Dark Basic's 3D Collision commands easier, but for now I'll leave you to experiment on your own with moving the camera using the Position Camera method. For now, Move Camera should be sufficient for most situations.
Note: If your program needs the camera's X, Y and Z co-ordinates, you can still use Move Camera - just use the three functions Camera Position X(), Camera Position Y() and Camera Position Z() to get them - after moving the camera.
Using The Keyboard To Move
In the last snippet, the camera was moved using the left and right mouse buttons. If you want to use the keyboard, the modification required is quite simple. All you have to do is decide which keys do what.
So, we'll make a very small alteration to the last snippet to simply use the cursor up and down keys instead of the mouse buttons.
+ Code Snippet
Rem TDK's Camera Snippets Rem Snippet 3 - 1st Person, Cursor Keys Move, MouseLook Gosub Setup Do Gosub MouseLook If UpKey()=1 Then Move Camera 0.1 If DownKey()=1 Then Move Camera -0.1 Sync Loop MouseLook: CX#=Camera Angle X(): CY#=Camera Angle Y(): CZ#=Camera Angle Z() CY# = CurveAngle(CY#+mousemovex(),CY#,10.0) CX# = CurveAngle(CX#+mousemovey(),CX#,10.0) Rotate Camera CX#,CY#,CZ# Return Setup: Set Display Mode 800,600,32 Sync On: Sync Rate 0 Make Matrix 1,500,500,20,20 CamX# = 250.0: CamY# = 1.0: CamZ# = 250.0 Position Camera CamX#,CamY#,CamZ# Hide Mouse Return
It's identical to the previous snippet, but lines 7 and 8 in the main Do..Loop check for the cursor up and down keys using the UpKey() and DownKey() functions, adding the movement values accordingly - just like when using the mouse buttons.
Notice that you can 'fly' anywhere with both this and the previous code snippet. To prevent going below floor level, (or indeed entirely disabling the ability to fly), you would need to incorporate a check height routine - which when using a matrix involves the Get Ground Height() function.
One thing you'll probably notice about the last snippet is that with the mouse in one hand steering and the other on the cursor up/down keys, it doesn't feel very comfortable.
The First Person games 'standard' for the keyboard is usually 'WASD' with W to move forwards, S to move back with A and D as 'strafing' left and right respectively. (Strafing is where you move left or right without turning to face the direction you are moving). Much better keys to use than the cursors.
Having said that, I suppose that if you are left-handed, even WASD isn't ideal so it's always best to allow the user to define the keys they want to use.
Anyway, with apologies to all left-handers out there, let's implement the WASD keys to our First Person camera...
+ Code Snippet
Rem TDK's Camera Snippets Rem Snippet 4 - 1st Person, WASD Keys Move, MouseLook Gosub Setup Do Gosub MouseLook Gosub PlayerMove Sync Loop PlayerMove: If KeyState(17) Then Move Camera 0.02: Rem 'W' If KeyState(30): Rem 'A' Rotate Camera CX#,WrapValue(Camera Angle Y()-90.0),CZ# Move Camera 0.02 Rotate Camera CX#,WrapValue(Camera Angle Y()+90.0),CZ# Endif If KeyState(31) Then Move Camera -0.02: Rem 'S' If KeyState(32): Rem 'D' Rotate Camera CX#,WrapValue(Camera Angle Y()+90.0),CZ# Move Camera 0.02 Rotate Camera CX#,WrapValue(Camera Angle Y()-90.0),CZ# Endif Rotate Camera CX#,CY#,CZ# CamPosX# = Camera Position X() CamPosZ# = Camera Position Z() CamPosY# = Get Ground Height(1,CamPosX#,CamPosZ#)+1.5 Position Camera CamPosX#,CamPosY#,CamPosZ# Return MouseLook: CX# = Camera Angle X(): CY# = Camera Angle Y(): CZ# = Camera Angle Z() CY# = CurveAngle(CY#+mousemovex(),CY#,10.0) CX# = CurveAngle(CX#+mousemovey(),CX#,10.0) Rotate Camera CX#,CY#,CZ# Return Setup: Set Display Mode 800,600,32 Sync On: Sync Rate 0 CLS RGB(0,100,0): Rem Clear the screen with green colour For N = 1 To 1000: Rem Add a few dots Ink RGB(0,Rnd(200),0),0 Dot Rnd(256),Rnd(256) Next N Get Image 1,0,0,256,256: Rem Grab an image for the matrix texture Make Matrix 1,500,500,40,40: Rem Create a matrix Prepare Matrix Texture 1,1,1,1: Rem use the grabbed image to texture the matrix CamX# = 250.0: CamY# = 1.0: CamZ# = 250.0 Position Camera CamX#,CamY#,CamZ# Hide Mouse Return
The first thing to mention is that the code for moving has been placed in it's own subroutine (PlayerMove) and called by a Gosub. You should always try to keep the main Do..Loop as simple and uncluttered as possible.
With just two lines to check the keyboard or mouse buttons, it's OK to place the code inside the Do..Loop. But the new code required for player movement is 17 lines and really should be in it's own subroutine, so that's what I've done.
The second thing I did was make the matrix solid with a bit of colour and a add few dots so we can see that we are moving. Lastly, (because this example demonstrates strafing), I removed the ability to fly. Strafing implies a character with a gun and I'm pretty sure his (or her) feet would be firmly planted on the ground!
This matrix is flat, but using Get Ground Height would keep the camera the same height from the ground even if the matrix wasn't flat.
OK, let's look at the changes in more detail...
In the Setup subroutine, CLS clears the screen. However, if you put an RGB() function value as a parameter, CLS will clear the screen using the colour supplied - in this case a mid-shade of green.
The For..Next loop counts from 1 to 1000 and each time around the loop, sets the ink colour using a randomly chosen value for the green component between 0 and 200. The result is a random shade of green which is used on the next line to plot a dot randomly in a 256x256 pixel section in the top left corner of the screen.
What we end up with is a mid-green 256x256 section of the screen covered with randomly placed dots - all various shades of green.
The Get Image line grabs this section of the screen into image number 1 to use as a texture for the matrix. But, as we set the Sync mode to manual at the start of the program with Sync On, we don't actually see any of this happen as we don't use the Sync command to refresh the screen. Useful yes?
Finally, we create matrix 1 500x500 3D units wide and deep, and made up of 40 tiles across and down. The Prepare Matrix Texture commands tells DB to use the texture we grabbed earlier. There are 4 parameters - 1,1,1,1.
The first 1 is the number of the matrix to apply the texture to.
The second and third number 1's are the number of textures across and down the image used, (one image can contain multiple textures on a 2x2, 3x3 etc grid). Our grabbed image contains a single texture so we have 1 across and 1 down.
The last 1 is the image number which contains the textures. For example, had we earlier said:
Get Image 999,0,0,256,256
..the graphic would have been grabbed into image number 999 and to work, the Prepare Matrix Texture line would have to have been:
Prepare Matrix Texture 1,1,1,999
The MouseLook subroutine has not changed, but we now have a new subroutine - PlayerMove. Here, we check for the W, A, S and D keys. But hey! What's all this Keystate mumbo jumbo?
Well, there are numerous ways to get info from the keyboard - like Inkey$(), ScanCode() and some keys like the cursor keys and Space Bar even have their own functions (UpKey(), SpaceKey() etc).
ScanCode() is the best one for general use, but it has a drawback.
If you press more than one key at a time, only the value of the last one to be pressed is returned. This is all well and good until you come to press W and A together to move forwards and strafe left at the same time. It only returns one or the other.
KeyState on the other hand will work even when two keys are pressed together.
So, in the PlayerMove subroutine, four If statements are used to check for the WASD keys. W has a scancode of 17 while A, S and D have the codes 30, 31 and 32 respectively.
If Keystate(17) returns true (1) then we know the W key is being pressed so we move forwards 0.02.
If Keystate(30) returns true (1) then we know the A key is being pressed so we strafe left. Doing that is very simple. Remember a little earlier I said that due to manual Syncing we could do things before a Sync without seeing them? Well this simple strafing method relies on that ability.
To strafe left, we rotate the camera 90º to the left, move forwards and then rotate the camera 90º to the right. Then and only then do we issue a Sync. The end result is that we only see the new view after moving - and when the camera is pointing in the direction it was before. You never see the turning left and right on your screen.
If Keystate(31) returns true (1) then we know the S key is being pressed so we move backwards 0.02.
If Keystate(32) returns true (1) then we know the D key is being pressed so we strafe right. This is exactly the same as strafing left, but we rotate 90º right, move and then rotate 90º left back to the original direction.
Finally we update the camera's angles using all the newly calculated values.
As stated earlier, because we are using Keystate, you can press any combination of the WASD keys and they will be read correctly. You can even move diagonally.
I'm sure you've also spotted the addition of a few more lines of code after the Rotate Camera line. These are the lines which 'glue' our player to the floor.
The Get Ground Height() function takes three parameters and returns the exact height of the matrix at any given X/Z position on it.
So, having moved the camera, we use
CamPosX# = Camera Position X()
CamPosZ# = Camera Position Z()
..to get the camera's current X and Z position for the Get Ground Height function, which stores the height found in the float variable CamPosY# like this:
CamPosY# = Get Ground Height(1,CamPosX#,CamPosZ#)+1.5
The first parameter (1) is the matrix number so you can have more than one matrix on the screen and still be able to choose which one to check the ground height of - you just supply the required matrix number.
The following two parameters are the X and Z positions on the matrix you want to know the height of - the camera's current position.
Note: Get Ground Height does not return the correct values if you have used Position Matrix to move the matrix from 0,0,0. If you move it, you must use offset values set to the amount you moved it on the X and Z plane.
At the end of this line we add 1.5 to the returned height to fix the player's 'eye level' above the matrix. Without this, on a flat matrix the players eye level would actually be level with the matrix and all you would see of the matrix is a line.
Note: The value you add need not be 1.5. This value effectively reflects the player character's height. The higher this value, the taller a character appears to be compared to other characters in the game. As such, a tall character has to look down on shorter characters.
The only thing to bear in mind is that the DBC camera will start to see throught the matrix when looking down if the height above the matrix is less than 1.0 because that's the minimum value that can be used with the Set Camera Range command. (It can be less than 1.0 in DBPro).
Finally, after having calculated the height of the player character, we use Position Camera to reposition the camera the required height above the matrix. As before, because this is all done before the Sync at the end of the main Do..Loop, we don't ever get to see all the camera jiggling that's going on every time it's moved.
Our main Do.Loop is now perfectly uncluttered and our program is very easy to follow - jump to and execute the MouseLook code, return, then jump to and execute the PlayerMove code, return and do a Sync which refreshes the screen with all of our changes. Repeat.
Adding something else - like collision is as simple as adding a collision subroutine such as PlayerCollision and adding a Gosub PlayerCollision line in the main Do..Loop.
Next, we'll cover Third Person camera control.
Third Person Camera
Third person camera mode positions the camera as the third person in the scene looking down on the player's character.
As such, you would think (like I did) that it ought therefore to be called 'second person' not 'third person', but it's name comes from the fact that it was commonly used in combat games where your character is the first fighter, the opponent is the second and the camera is showing the view of a third person watching the action.
Either way, the fact is that in third person view, the camera shows your 3D world including your game character, rather than just what your game character can see. As such, when the game character object moves, your camera has to stay with it or it will disappear into the distance never to be seen again!
There are lots of different ways to do this - many named after the games which originally implemented them. So you can have Final Fantasy camera control which would work differently to say the Black And White camera control method.
The method you use is entirely up to you and it can be made to do whatever you want. In this tutorial I'll just cover the basic third person controls and you can tweak them to your particular requirements. So what's the difference between them?
Well basically, in your game you may want the camera to follow your character around automatically. Alternatively, you may want a free roam camera where you can go anywhere and view action going on in any location - like in Black And White.
If the camera auto-tracks your player's character, does it follow directly behind or do you want the camera to swing round when you move the mouse left and right. Maybe you want to control the height of the camera by moving the mouse forwards and backwards or zoom in and out with the mouse wheel.
Maybe the right mouse button needs to be held down when you move the mouse to circle around the player's character object. The permutations really are endless.
Basic Mouse Steer With Set Camera To Follow
Our first third person example is a very basic 'steer a cube on a matrix' snippet which uses the mouse to steer an auto-moving object and the Set Camera To Follow() function to track it at a fixed distance, angle and height.
Copy and paste this into DB and when you run it, just move the mouse left and right to steer the cube on the matrix. The camera will smoothly follow the cube around.
+ Code Snippet
Rem TDK's Camera Snippets Rem Snippet 5 - 3rd Person, Cube Steered With Mouse, Set Camera To Follow Gosub Setup Do Gosub ControlObject Gosub TrackCam Sync Center Text 400,0,"Steer The Cube With The Mouse" Loop ControlObject: Y# = CurveAngle(Object Angle Y(1)+MouseMoveX(),Object Angle Y(1),5) Yrotate Object 1,Y# Move Object 1,0.02 ObjX# = Object Position X(1): ObjY# = Object Position Y(1): ObjZ# = Object Position Z(1) Return TrackCam: Set Camera To Follow ObjX#,ObjY#,ObjZ#,WrapValue(Object Angle Y(1)+TrackAngle#),Camdist#,Camheight#,Camsmooth#,ColFlag Return Setup: Set Display Mode 800,600,32 Sync On: Sync Rate 0 CLS RGB(0,100,0): Rem Clear the screen with green colour For N = 1 To 1000: Rem Add a few dots Ink RGB(0,Rnd(200),0),0 Dot Rnd(256),Rnd(256) Next N Get Image 1,0,0,256,256: Rem Grab an image for the matrix texture Make Matrix 1,500,500,40,40: Rem Create a matrix Prepare Matrix Texture 1,1,1,1: Rem use the grabbed image to texture the matrix ObjX# = 250.0: ObjY# = 1.0: ObjZ# = 250.0 Make Object Cube 1,1 Color Object 1,RGB(255,0,0) Position Object 1,250,Get Ground Height(1,250,250)+1.0,250 Camdist# = 15.0 TrackAngle# = 0.0: Rem 0 - Behind, 180 - Front, 90 & 270 - Sides CamHeight# = 5.0 Camsmooth# = 100.0 ColFlag = 0 Hide Mouse Return
As with the previous snippet, the Setup subroutine initialises the program and creates a matrix for a floor.
The ControlObject subroutine calculates the object's new Y angle with the CurveAngle() function receiving the cube's required angle after adding the amount of mouse movement, the cube's current angle and transition speed. The result is placed in float variable Y#.
The cube (object 1) is then rotated to the new Y angle and moved forwards 0.02 3D units.
Finally, the cube's current X, Y and Z positions are grabbed and stored in ObjX#, ObjY# and ObjZ# as they are needed later.
In the TrackCam subroutine, the useful Set Camera To Follow function does it's magic. The ObjX#, ObjY# and ObjZ# stored just after moving the cube is used by the Set Camera To Follow function as the target to follow.
I'm not going to go into any more detail about this function here as none of the parameters are altered by the program and if you are unfamiliar with it, it's covered in detail in the Set Camera To Follow tutorial which you can find here:
WASD Object Steer With Set Camera To Follow
The next snippet is probably starting to resemble the type of camera control method you've (hopefully) read through all of this tutorial to get to. With it, you can control an object (in this example a box is used as a placeholder) with the W and S keys used to move the object forwards and backwards and the A and D keys to turn left and right. The mouse is used to control the camera along with the Set Camera To Follow function.
+ Code Snippet
Rem TDK's Camera Snippets Rem Snippet 6 - 3rd Person, Cube Steered With WASD/ZQSD/ZASD, Set Camera To Follow, Mouse CamPos Gosub Setup Do Gosub ControlBox Gosub TrackCam Sync Center Text 400,0,"Control The Cube With WASD Keys And The Camera With The Mouse" Loop ControlBox: If KeyState(17) Then Move Object 1,0.02: Rem 'W' If KeyState(30): Rem 'A' Y# = CurveAngle(Object Angle Y(1)-1.0,Object Angle Y(1),5) Yrotate Object 1,Y# Endif If KeyState(31) Then Move Object 1,-0.02: Rem 'S' If KeyState(32): Rem 'D' Y# = CurveAngle(Object Angle Y(1)+1.0,Object Angle Y(1),5) Yrotate Object 1,Y# Endif ObjX# = Object Position X(1): ObjY# = Object Position Y(1): ObjZ# = Object Position Z(1) Return TrackCam: MMx = MouseMoveX(): MMy = MouseMoveY() MMz = MouseZ(): Rem <<< Enable only this line if using DBPro Rem MMz = MouseMoveZ(): Rem <<< Enable only this line if using DB Classic TrackAngle# = WrapValue(TrackAngle#+(MMx/4)): Rem Rotate Camera Around Object On Y Axis Camheight# = Camheight#-(MMy/4): Rem Move Camera Up/Down If Camheight#<1.5 Then Camheight# = 1.5: Rem Lowest point camera can go If Camheight#>15.0 Then Camheight# = 15.0: Rem Highest point camera can go If MMz <> OldMMz If MMz > OldMMz Camdist# = Camdist#-0.8: Rem Zoom In/Out If Camdist# < 1.0 Then Camdist# = 1.0 Else Camdist# = Camdist#+0.8: Rem Zoom In/Out If Camdist# > 30.0 Then Camdist# = 30.0 Endif OldMMz = MMz Endif Set Camera To Follow ObjX#,ObjHeight#,ObjZ#,Object Angle Y(1)+TrackAngle#,Camdist#,Camheight#,Camsmooth#,ColFlag Point Camera ObjX#,ObjY#,ObjZ# Return Setup: Set Display Mode 800,600,32 Sync On: Sync Rate 0 CLS RGB(0,100,0): Rem Clear the screen with green colour For N = 1 To 1000: Rem Add a few dots Ink RGB(0,Rnd(200),0),0 Dot Rnd(256),Rnd(256) Next N Get Image 1,0,0,256,256: Rem Grab an image for the matrix texture Make Matrix 1,500,500,40,40: Rem Create a matrix Prepare Matrix Texture 1,1,1,1: Rem use the grabbed image to texture the matrix ObjX# = 250.0: ObjY# = 1.0: ObjZ# = 250.0 Make Object Box 1,1.0,0.5,2.0 Color Object 1,RGB(255,0,0) Position Object 1,250.0,0.5,250.0 Camdist# = 15.0 TrackAngle# = 0.0: Rem 0 - Behind, 180 - Front, 90 & 270 - Sides CamHeight# = 5.0 Camsmooth# = 100.0 ColFlag = 0 Hide Mouse Return
OK, when you run this program, use the W key to move the box in the direction it's facing and S to back up. The camera will track along with it. Use the A and D keys to steer and the camera will obediently follow the box round corners, maintaining the angle between the camera and the box.
Move the mouse left and right and the camera will rotate around the box. Move the mouse forwards and backwards and the camera will move up and down. Use the mouse scroll wheel and the camera will zoom in and out on the box.
In this snippet, the ControlBox subroutine uses KeyState() to act on the W, A, S and D keys just like in the previous first person WASD example, but turning the object instead of controlling the camera.
The real change to this snippet from those previously described is with the TrackCam subroutine where the Set Camera To Follow function is now joined by the code for moving the camera.
As the Set Camera To Follow parameters are variables rather than fixed values we can alter the content of those variables in real time and feed them into the function.
The TrackAngle# variable is the angle between the object being followed and the camera. If this is set to 0 (zero) the camera follows directly behind the box. If this value is 180 then the camera is positioned in front of the box, looking at it but moving backwards. When set to 90 or 270, the camera is placed at the side of the box - like a camera mounted on the roof of a car, tracking horses in a horse race.
So, if we add the left/right mouse movement to the current tracking angle we can swing around the object as it moves. To do this, the first line of the subroutine grabs the mouse movement values and then the X value is added to TrackAngle# and used in the Set Camera To Follow function.
A similar thing happens on the next line with Camheight# only using the mouse movement Y value to increase and decrease the camera height. But, as we don't have a 0 to 359 range like with TrackAngle#, we can't use WrapValue() so we need a couple of lines to impose minimum and maximum height restrictions so we don't send the camera down through the floor or up too high. That's what the two If Camheight# lines do.
If Camheight#<1.5 Then Camheight# = 1.5: Rem Lowest point camera can go
If Camheight#>15.0 Then Camheight# = 15.0: Rem Highest point camera can go
The first line checks the newly calculated camera height and if it goes below 1.5 it is set back to 1.5. So, the camera can never go below 1.5.
The second line checks the newly calculated camera height and if it goes above 15.0 it is set back to 15.0. So, the camera can never go above 15.0.
The next section is for the zoom function to work with the mouse scroll wheel.
** Important ***
DB Classic reads the value from the wheel using the MouseMoveZ() function whereas DB Pro does the same thing using the MouseZ() function. For that reason you have to rem out the unwanted line of the following two in the TrackCam subroutine:
MMz = MouseZ(): Rem <<< Enable this line if using DBPro
Rem MMz = MouseMoveZ(): Rem <<< Enable this line if using DB Classic
If you use DB Pro then rem out only the second line which uses MouseMoveZ() and if you are a Classic user, rem out just the first line which uses MouseZ().
When our program is first run, MouseMoveZ() in DBC and MouseZ() in Pro initially returns 0 (zero). Move the mouse wheel towards you and the numbers go down (become negative). The more you scroll, the bigger the negative number.
Scroll the wheel away from you and the numbers increase - eventually resulting in positive numbers. The important thing to realise is that when you stop scrolling the wheel the value doesn't return to 0, it stays at what it was - unlike the equivalent MouseMoveX() and MouseMoveY() functions. To see this, copy and paste this small snippet into DB:
+ Code Snippet
Set Text Opaque Do Text 10,10,Str$(MouseMoveX())+" " Text 10,26,Str$(MouseMoveZ())+" " Text 10,42,Str$(MouseZ())+" " Loop
Run it and move the mouse left and right. Notice that MouseMoveX() shows the amount moved and when you stop it shows 0? Now scroll the mouse wheel.
This time, the second value if using DBC (or the third value if using DB Pro) changes but doesn't go back to 0 when you stop moving the wheel.
This is not good for our zoom as a) the value not resetting to 0 doesn't tell us that the scrolling has stopped and b) the value gets increasingly larger (or smaller) making the value unusable for the amount to zoom - unlike when using MouseMoveX() and MouseMoveY().
So, we need to address these problems.
The first - when to zoom and when not to - is done by using a second variable, OldMMz to shadow the MouseMoveZ() variable, MMz.
When the scroll wheel is moved, we compare MMz with OldMMz using If MMz <> OldMMz and only if they are not the same do we know the wheel has been scrolled and it's OK to do the zoom.
After zooming we set OldMMz to MMz with OldMMz = MMz. If the wheel is not scrolled again, because OldMMz equals MMz we don't scroll any more.
But, as soon as we scroll the wheel, MMz will contain a new value and be different to OldMMz - allowing the zooming code to be executed. Once again, after the zoom, the OldMMz variable is set to MMz so it doesn't scroll again until the mouse wheel is moved.
Without this simple check, if we scrolled the mouse wheel the zoom would start but never stop!
The next problem is what direction to zoom and the answer to this one is another simple check using an If..Else..Endif:
+ Code Snippet
If MMz > OldMMz Camdist# = Camdist#-0.8: Rem Zoom In If Camdist# < 1.0 Then Camdist# = 1.0 Else Camdist# = Camdist#+0.8: Rem Zoom Out If Camdist# > 30.0 Then Camdist# = 30.0 Endif
We know that the program only executes these lines if MMz does not equal OldMMz. As we know that MMz is the current movement value of the scroll wheel and OldMMz was the last movement value of the scroll wheel then all we have to do is compare them to see if the last wheel movement caused the value to go up or down.
If it went up (MMz is now greater than OldMMz) then we must want to zoom in. If it went down (MMz is now less than OldMMz) then we must want to zoom out. That's exactly what the above code does.
The final problem is how much to zoom in as the values returned from MouseMoveZ() (or MouseZ()) are of little use.
The answer is to zoom a fixed amount in and out regardless of the amount the scroll wheel is moved. A bit of a cheat I know, but the end result is just as good. In the snippet I chose 0.8 for the zoom value though you can increase or decrease this value if you want a faster or slower zoom.
The two 'If Camdist#' lines are there to stop you zooming into the object or zooming out too far - in a similar fashion to the way we restricted the camera height earlier. You therefore can't zoom in any closer than 1.0 3D units or further out than 30.0 3D units.
The last new line is the Point Camera after the Set Camera To Follow line. This is required to point the camera at the object when the camera's height is altered - as doing so doesn't alter the camera's angles. Neither does Set Camera To Follow, so we have to do it manually.
Well that just about wraps up this introduction to cameras tutorial and all that remains for me to say is that if I come across any other useful or interesting camera control methods I'll add them to the end of this tutorial.
Return To Programming For Beginners Part 5 - Link To Be Added
|Back to top
Joined: Fri Oct 30th 2009
Location: Germany, Bavaria
very good tutorial, like all of you
It's not a bug it's a feature
|Back to top
|Back to top
Joined: Aug 26th 2002
|Back to top
Joined: Mon Nov 2nd 2009
Quick question on Rotate Camera command
You publish the command is
rotate camera x,y,z
with the understanding that:
x = horizontal axis
y = vertical axis
However, I believe it should be
rotate camera y,x,z because of the following:
+ Code Snippet
rx#=wrapvalue(rx#+mousemovex()) ry#=wrapvalue(ry#+mousemovey()) rotate camera ry#,rx#,0.0
if you move your mouse left to right you will look left to right.
if you move your mouse up and down you will look up and down.
if the code were
+ Code Snippet
rx#=wrapvalue(rx$+mousemovex()) ry#=wrapvalue(ry#+mousemovey()) rotate camera rx#,ry#,rz#
then when you move the mouse left and right you will actually look up and down and when the mouse is moved up/down you would look left and right.
anyway, hope this helps others who might run into the problem of things not looking in the right direction based on mouse movement.
|Back to top
No, the details given were correct.
rotate camera x,y,z
x = rotation around the x axis (ie, the horizontal axis, or looking up/down)
y = rotation around the y axis (ie, the vertical axis, or looking left/right)
z = rotation around the z axis (ie, the 'forward' axis, or rolling left/right)
|Back to top
Joined: Mon Nov 2nd 2009
Ok so basically if you have a block your putting an axle through the X (horizontal axis) instead of through the Y (vertical axis) which makes it where increases/decreases of the X value result in rotation vertically and not horizontally.
A little backwards but ok I get it.
Everywhere else where I have done 3D programming the X implies an axle on the vertical plane and increases/decreases in X rotates left and right not up and down.
Cool... will try to adjust my thinking when using DBPro.
|Back to top
Joined: Sun Jan 16th 2005
Hope this doesn't count as a hijack, but I just wanted to throw in my version of a 3rd person camera. Rather than directly controlling the camera, an imaginary camera target is positioned, which is also really simple to switch between following a particular rts unit and free map exploration
This is an rts style camera. I have left directions commented in the subroutine header, but in a nutshell:
Camera Scrolling : Arrow Keys or screen boundary
Camera Pan : Mouse movement while the middle button is pressed
Camera Zoom : scroll wheel
a. it helps someone, or
b. someone can see where my code could be tighter and I get the help
+ Code Snippet
cameracontrol: Remstart This subroutine will control the camera as follows: Middle Button - Activate pan mode, moving the mouse in the x direction will rotate the camera around the current camera target. Moving the mouse in the y direction will elevate the camera Camera Scrolling - with either the boundary zones aroudn the screen or with the arrow keys Scroll Wheel - zooms in and out between maxrange# and minrange# remend campitch#=camera angle x() camhead#=camera angle y() boundary=20 `the width in pixels of the camera scrolling zone around the screen camspeed#=0.25 `how fast the camera can scroll camrotspd#=0.25 `how fast the camera rotates maxpitch#=80 `max and min camera pitch angles minpitch#=10 maxrange#=30 `max and min camera ranges minrange#=5 `Camera Zoom on scrollwheel camrange#=camrange#-(mousemovez()/100) if camrange#<minrange# then camrange#=minrange# if camrange#>maxrange# then camrange#=maxrange# `Camera Panning with mouse while middle button is clicked tempx#=mousemovex() tempy#=mousemovey() if mouseclick()=4 camhead#=wrapvalue(camhead#+(tempx#*camrotspd#)) campitch#=wrapvalue(campitch#+(tempy#*camrotspd#/2)) endif if campitch#>maxpitch# then campitch#=maxpitch# if campitch#<minpitch# then campitch#=minpitch# rotate camera campitch#, camhead#, 0 `Camera scrolling by either arrowkeys or boundary zones (note: I DONT want the camera to scroll while I am panning it if not (mouseclick()=4) if mousey()<boundary cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y())) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y())) endif if mousey()>(screen height() - boundary) cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y()+180)) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y()+180)) endif if mousex()<boundary cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y()-90)) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y()-90)) endif if mousex()>(screen width() - boundary) cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y()+90)) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y()+90)) endif endif `But I DO still want to be able to scroll using the arrowkeys, at the same time as I pan the view (with middle mouse button clicked) if mousex()>boundary and mousex()<(screen width()-boundary) and mousey()>boundary and mousey()<(screen height()-boundary) if upkey() cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y())) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y())) endif if downkey() cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y()+180)) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y()+180)) endif if leftkey() cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y()-90)) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y()-90)) endif if rightkey() cameratargetx#=cameratargetx# + (camspeed#*sin(camera angle y()+90)) cameratargetz#=cameratargetz# + (camspeed#*cos(camera angle y()+90)) endif endif `This next bit will override all of the previous stuff, only if the camerafollow mode is set to '1' if camerafollow = 1 cameratargetx# = object position x(camtargetobj) cameratargety# = object position y(camtargetobj) cameratargetz# = object position z(camtargetobj) endif `determine the new camera position based on the range, heading and pitch to the camera target camx#=cameratargetx#-(camrange#*sin(camhead#)*cos(campitch#)) camy#=cameratargety#+(camrange#*sin(campitch#)) camz#=cameratargetz#-(camrange#*cos(camhead#)*cos(campitch#)) position camera camx#,camy#,camz# return
This is not the Sig you are looking for....
|Back to top
Sorry, but it has been so long since anyone replied to this Thread that it has been automatically locked.
You may read it but not reply.
You may read it but not reply.
Enter a word or phrase to search our Forum for: