Handling Input
TinyFFR comes with a built-in API for reacting to user input via keyboard, mouse, and gamepad. This page will demonstrate how to use those devices to control a free-flying camera.
Continuing "Hello Cube"
This tutorial will mostly be concerned with showing you how to move a Camera according to input captured via keyboard & mouse and/or gamepad.
If you wish you can integrate these examples directly with the hello cube tutorial and/or the treasure chest tutorial from the previous page, you can simply manipulate the camera resource that's already created + added to the scene.
Initial Setup#
For all of the code below we will handle our input in a dedicated class "CameraInputHandler". We will invoke two static methods each frame from inside our application loop:
TickKbm()to handle keyboard/mouse input, and,TickGamepad()to handle game controller input.
We will also define some static fields that will track the camera state across frames.
static class CameraInputHandler {
const float CameraMovementSpeed = 1f; // (1)!
static Angle _currentHorizontalAngle = Angle.Zero; // (2)!
static Angle _currentVerticalAngle = Angle.Zero; // (3)!
static Direction _currentHorizontalPlaneDir = Direction.Forward; // (4)!
public static void TickKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) { // (5)!
// TODO
}
public static void TickGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) { // (6)!
// TODO
}
}
-
This is just a constant that sets the camera's speed.
You can modify it if you wish to slow down or speed up the camera!
-
_currentHorizontalAnglewill be used to keep track of the current angle the camera is pointing at on the horizontal plane (i.e. forward/left/backward/right etc.).Arbitrarily, we will define
Direction.Forwardas being 0°; otherwise any non-zero value is the rotation in a clockwise direction around theDownaxis.For example, 0° means our camera is looking forward, 90° means our camera is looking right, 180° is looking behind, and 270° is looking to the left.
Remember, this is just the horizontal plane angle. We will combine it with the vertical (up/down) angle to create the final look direction for the camera.
-
_currentVerticalAnglewill be used to keep track of the current up/down angle for the camera.Arbitrarily, we will define no up/down tilt as being 0°; otherwise any non-zero value is the rotation around the axis that is currently pointing leftward from where the camera is looking.
(If this isn't clear: Pick something up on your desk and imagine it's the camera. "Point" it in various directions, and add a pen or pencil always sticking out of its left side no matter which way it's pointing. That's the axis we're rotating around with
_currentVerticalAngle. Rotate your "camera" around this axis and you'll understand how this creates an up/down tilt.)For example, 0° is facing straight forward, 90° is facing fully downward at our feet, -90° is facing fully up in to the sky.
And again, remember, we will combine this angle with the horizontal to create the final look direction for the camera.
-
Finally, we'll also store the actual horizontal view direction as well as the angle.
Although we can always get this value easily by using
_currentHorizontalAngleto calculate it, storing the calculated value is a performance optimisation as we'll use it repeatedly each frame.We set it to
Direction.Forwardinitially as that matches ourAngle.Zerovalue for_currentHorizontalAngle. -
We will pass three things to
TickKbm();- An
ILatestKeyboardAndMouseInputRetrieverinstance that we will use to get the latest keyboard + mouse input state; - A reference to our
camera; - The amount of time passed this frame (in seconds).
You'll see how to get the
ILatestKeyboardAndMouseInputRetrieverin the next code snippet below. - An
-
We will pass the same three things to
TickGamepad()as we did toTickKbm()excepting the first parameter, which is now anILatestGameControllerInputStateRetrieverinstance instead of anILatestKeyboardAndMouseInputRetriever.As you might have guessed, this interface lets us get game controller state rather than keyboard/mouse state.
We'll invoke these methods inside our application loop.
We also need to lock our cursor inside the window while running to make sure it can't escape outside when moving our mouse to turn the camera; so we set window.LockCursor to true before entering the loop:
window.LockCursor = true; // (1)!
while (!loop.Input.UserQuitRequested) {
var deltaTime = (float) loop.IterateOnce().TotalSeconds;
CameraInputHandler.TickKbm(loop.Input.KeyboardAndMouse, camera, deltaTime);
CameraInputHandler.TickGamepad(loop.Input.GameControllersCombined, camera, deltaTime);
renderer.Render();
}
-
When set to
true, the mouse cursor will be "locked inside" our window. This is useful when you want to use the mouse to control a camera as it stops the cursor from escaping the application frame.If you're still not sure what the purpose of this is, set it to
false(the default) and observe the difference.
Double Input
In this example, we call both TickKbm() and TickGamepad() every frame. This does mean that we're technically manipulating the camera twice per frame: Once for the keyboard/mouse input and once for the gamepad.
This may or may not matter for your application, but one implication is that if we move the camera with the gamepad and the keyboard at the same time, it will move at double the speed.
Improving this will depend on how exactly you wish to handle various input sources for your application.
What is GameControllersCombined?
GameControllersCombined returns an ILatestGameControllerInputStateRetriever that represents every game controller connected to the system, combined. For example, you can press the "A" button on one controller, and the "B" button on a second controller, and both button press events will be reflected in the GameControllersCombined retriever.
Also, GameControllersCombined will always be valid, will never be null, and will never throw any exceptions; even if there are no controllers connected to the system or if the user adds or removes controllers.
The main purpose of GameControllersCombined is to allow you to simply support anyone using your application to connect any controller and begin using it, without having to worry about configuring the "correct" controller.
If you prefer to work with specific controllers however, you can enumerate them with loop.Input.GameControllers-- you can use this property to inspect every controller currently connected to the system.
Mouse Camera Panning#
Now we've got our "scaffolding" out of the way, let's add camera panning with the mouse. We're going to define a single method to handle this called AdjustCameraViewDirectionKbm() inside our CameraInputHandler class:
static void AdjustCameraViewDirectionKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
const float MouseSensitivity = 0.05f; // (1)!
var cursorDelta = input.MouseCursorDelta; // (2)!
_currentHorizontalAngle += cursorDelta.X * MouseSensitivity; // (3)!
_currentVerticalAngle += cursorDelta.Y * MouseSensitivity; // (4)!
_currentHorizontalAngle = _currentHorizontalAngle.Normalized; // (5)!
_currentVerticalAngle = _currentVerticalAngle.Clamp( // (6)!
-Angle.QuarterCircle,
Angle.QuarterCircle
);
_currentHorizontalPlaneDir =
Direction.Forward * (_currentHorizontalAngle % Direction.Down); // (7)!
var cameraLeft = Direction.FromDualOrthogonalization( // (8)!
Direction.Up,
_currentHorizontalPlaneDir
);
var verticalTiltRot = _currentVerticalAngle % cameraLeft;
camera.SetViewAndUpDirection( // (9)!
_currentHorizontalPlaneDir * verticalTiltRot,
Direction.Up * verticalTiltRot
);
}
- This const just sets how fast the camera should pan around, and will depend a bit on your mouse's DPI setting. Feel free to adjust this value to taste.
-
input.MouseCursorDeltareturns anXYPair<int>which indicates how many pixels the mouse cursor moved this frame.You don't need to know everything about the
XYPair<T>type right now, all you need to know is that it has anXproperty and aYproperty (as its name implies). In this case theXproperty tells us how many pixels the mouse moved in the left/right direction, and theYproperty tells us how many pixels it moved in the up/down direction.The window's "origin" point is its top-left corner, so:
- A positive
Xvalue means the cursor moved right. A negativeXvalue means the cursor moved left. - A positive
Yvalue means the cursor moved down. A negativeYvalue means the cursor moved up.
- A positive
-
Here we're adding
cursorDelta.X * MouseSensitivitydegrees to_currentHorizontalAngle.- If the user has not moved the mouse left or right this frame,
_currentHorizontalAnglewill not change. - If the user has moved the mouse to the right,
_currentHorizontalAnglewill be increased. - If the user has moved the mouse to the left,
_currentHorizontalAnglewill be decreased.
- If the user has not moved the mouse left or right this frame,
-
Here we're adding
cursorDelta.Y * MouseSensitivitydegrees to_currentVerticalAngle.- If the user has not moved the mouse up or down this frame,
_currentVerticalAnglewill not change - If the user has moved the mouse down,
_currentVerticalAnglewill be increased. - If the user has moved the mouse up,
_currentVerticalAnglewill be decreased.
- If the user has not moved the mouse up or down this frame,
-
On this step we're normalizing our horizontal angle. Normalizing just means we're making sure it stays within the range 0° to 360°.
For example, if
_currentHorizontalAngleis 370°, after normalization it will be 10°. If it was -30°, after normalization it will be 330°.Although this doesn't actually affect the math in any way, normalizing means we don't accrue floating-point error over time. Imagine if a user keeps panning around to the right for minutes on end; eventually they'll make
_currentHorizontalAnglereally high, at which point a 32-bit float may become too inaccurate and the camera will start "skipping" as it pans.Normalizing the value every frame gets rid of this issue.
-
Here we clamp our vertical angle between -90° and 90° (
Angle.QuarterCircleis just a static readonly for 90°).The point of this is to make sure that as we pan the camera up and down we never "flip over backwards" or "somersault forwards" and end up upside-down. By clamping this value to only ever be 90° up or 90° down, we make sure the viewer can only ever look directly up or down but no further.
As a side effect, like normalizing the horizontal angle above, this also helps prevent floating-point errors.
-
Now we set our
_currentHorizontalPlaneDiraccording to the newly-calculated_currentHorizontalAngle.This line is simply rotating
Direction.Forwardby_currentHorizontalAnglearound theDownaxis (clockwise):(_currentHorizontalAngle % Direction.Down)creates a rotation: The current horizontal angle aroundDown.Direction.Forward * (_currentHorizontalAngle % Direction.Down)is rotatingDirection.Forwardby that rotation. The multiply-operator is defined between aDirectionand aRotationand produces anotherDirectionwhich is the rotated input direction.
To help visualize this, stick a pencil "forward" towards your monitor. Now imagine rotating it by a number of degrees around the up/down axis. The new direction it's facing is what we're storing on this line in
_currentHorizontalPlaneDir. -
In the previous line we calculated the horizontal view direction for the camera. However, we also need to know how to tilt that direction up or down according to the vertical angle.
verticalTiltRotis a rotation we're calculating on the next line that we will use to tilt our horizontal view direction up or down by rotating it. To create that rotation, we first need to find the camera's "left-hand" axis (i.e. the direction that points to the left of the camera).Direction.FromDualOrthogonalization()is a static method on theDirectiontype that finds a direction that is orthogonal to two other directions. For example,Direction.FromDualOrthogonalization(Direction.Left, Direction.Up)will returnDirection.Forward.In this case we want to find a direction that is orthogonal to both the
Updirection and our horizontal camera direction. This will return our "left-hand" axis that points out to the left of our look direction.We then define a rotation as the
_currentVerticalAnglearound this left-hand axis.FromDualOrthogonalization(): Left or right?You might be wondering how we know that
Direction.FromDualOrthogonalization(Direction.Up, _currentHorizontalPlaneDir)gives us the left-hand camera direction; after all the right-hand direction is also an equally valid answer to the question of finding an orthogonal direction (it's orthogonal to bothUpand_currentHorizontalPlaneDiralso, just like the left-hand direction).The answer is that
FromDualOrthogonalization()follows the right-hand-rule:- Using your right hand, point your index finger towards the direction of the first argument (in this case
Up). - Using the same hand, now point your middle finger towards the direction of the second argument (in this case
_currentHorizontalPlaneDir). - Finally, on that hand, extend your thumb out so it's orthogonal to both your index and middle fingers: This is the direction
FromDualOrthogonalization()will return.
If you ever want to find the "opposite" answer, just swap the arguments around.
- Using your right hand, point your index finger towards the direction of the first argument (in this case
-
Finally, we invoke a method on our
cameracalledSetViewAndUpDirection()to set the view direction of the camera and its "up" direction at the same time.camera.ViewDirectionis the direction in which the camera is looking.camera.UpDirectionis the direction that is pointing "up" from the camera, i.e. this property determines which way up you're "holding" the camera.
For example, we could have a camera whose
ViewDirectionisForwardbut whoseUpDirectionisDown: This would be a camera looking forward but viewing the world "upside-down".To calculate the view direction, we specify
_currentHorizontalPlaneDir * verticalTiltRot: That's basically rotating our horizontal view direction around the camera's left-axis by the up/down tilt calculated previously.To calculate the up direction, we specify
Direction.Up * verticalTiltRot, which is just rotating theUpdirection by the same tilt.
Now just call this method inside TickKbm() and you'll be able to move the camera using the mouse:
public static void TickKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
AdjustCameraViewDirectionKbm(input, camera, deltaTime);
}
Keyboard Camera Movement#
Now let's make it so we can use the keyboard to move the camera around in our scene. Add another method, AdjustCameraPositionKbm():
static void AdjustCameraPositionKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
var positiveHorizontalYDir = camera.ViewDirection; // (1)!
var positiveHorizontalXDir = Direction.FromDualOrthogonalization( // (2)!
Direction.Up,
_currentHorizontalPlaneDir
);
var horizontalMovement = XYPair<float>.Zero; // (3)!
var verticalMovement = 0f; // (4)!
foreach (var currentKey in input.CurrentlyPressedKeys) { // (5)!
switch (currentKey) {
case KeyboardOrMouseKey.ArrowLeft:
horizontalMovement += (1f, 0f);
break;
case KeyboardOrMouseKey.ArrowRight:
horizontalMovement += (-1f, 0f);
break;
case KeyboardOrMouseKey.ArrowUp:
horizontalMovement += (0f, 1f);
break;
case KeyboardOrMouseKey.ArrowDown:
horizontalMovement += (0f, -1f);
break;
case KeyboardOrMouseKey.RightControl:
verticalMovement -= 1f;
break;
case KeyboardOrMouseKey.RightShift:
verticalMovement += 1f;
break;
}
}
var verticalMovementVect = Direction.Up * verticalMovement; // (6)!
var horizontalMovementVect = // (7)!
(positiveHorizontalXDir * horizontalMovement.X)
+ (positiveHorizontalYDir * horizontalMovement.Y);
var sumMovementVect = // (8)!
(horizontalMovementVect + verticalMovementVect)
.WithLength(CameraMovementSpeed * deltaTime);
camera.MoveBy(sumMovementVect); // (9)!
}
-
Overall, we're setting up controls to move our camera in a sum of three directions:
positiveHorizontalYDir,positiveHorizontalXDir, andDirection.Up.The horizontal directions are the two directions we will move the camera around when the user is holding any of the arrow keys: Camera forward and camera left. The vertical direction is just
Up.If we want to move the camera backwards/right/down we will just use a negative value for any of these directions.
On this line we're setting which way we want the camera to move when the user is holding the up arrow key. When the user holds the up arrow key we want the camera to move in the direction it's looking (i.e. 'camera forward'), so we simply set
positiveHorizontalYDirtocamera.ViewDirection. -
On the first line we set the first horizontal direction (e.g. 'camera forward').
On this line we set the other horizontal direction, which we want to be to the camera's left side (e.g. 'camera left'),
We calculate that left-side direction using our friend
Direction.FromDualOrthogonalization()again, to find the direction that is orthogonal to bothUpandpositiveHorizontalYDir('camera forward').Incidentally: We don't declare a
positiveVerticalDirvar anywhere because it's justDirection.Up. -
Here we define an
XYPair<float>calledhorizontalMovementand initialize it to zero. We will use this pair to store/calculate how far the camera should move in both of thepositiveHorizontal...Dirdirections according to the currently-held keyboard keys.After the foreach loop below completes, the pair's
XandYproperties will be1f,-1f, or0findicating a positive, negative, or zero movement in each horizontal direction. -
And here we define a
verticalMovementvalue as just afloatand also initialize it to zero. Again, we will set it to1f,-1f, or0fin the foreach loop to indicate whether we want the camera to move up, down, or not move at all vertically. -
This foreach loop is iterating through every keyboard and mouse key that the user is currently holding down in this frame.
We then switch over each key (
switch (currentKey) { ... }) and add or remove1fto/fromhorizontalMovement.X,horizontalMovement.Y, orverticalMovementdepending on which key is being held down.For example, if the user is holding the
ArrowUpkey, we add1ftohorizontalMovement.Y. Conversely, if the user is holding theArrowDownkey, we subtract1ffrom that same property. When the loop finishes we will know which directions through space the user wishes to move the camera.One nice thing about this approach is that "opposing" movement keys automatically cancel each other out. If the user is holding both
ArrowUpandArrowDownthe resultant value forhorizontalMovement.Ywill be0f. -
Here we create a
Vectindicating which way we want the camera to move in theUp/Downaxis by simply multiplyingverticalMovementbyDirection.Up.Because
verticalMovementis going to be either-1f,0f, or1f,verticalMovementVectwill be 1 meter up (Direction.Up * 1f), 1 meter down (Direction.Up * -1f), orVect.Zero(Direction.Up * 0f). -
Here we create a
Vectthat is just multiplyingXandYofhorizontalMovementbypositiveHorizontalXDirandpositiveHorizontalYDirrespectively.Because we know that
XandYwill only ever be-1f,0f, or1f, we know that this will only ever be either adding or removing 1 meter ofpositiveHorizontalXDirandpositiveHorizontalYDir(or nothing at all).By keeping all our multiplicands as ones or zeroes, we make sure our movement vector has equal proportions in every direction.
-
Here we add
verticalMovementVectandhorizontalMovementVecttogether and then make sure the resultant vect has a length ofCameraMovementSpeed * deltaTime.By multiplying
CameraMovementSpeedbydeltaTimewe make sure the camera's movement is the same regardless of the framerate our application runs at. It also has the effect of making theCameraMovementSpeed's unit easy to understand: It is now implicitly a value in meters/second.What happens if we call
WithLength()on a zero-lengthVect?When dealing with "raw" vector math, normalizing or resizing a zero-length vector is an undefined operation.
However,
Vectaccounts for this and has a well-defined behaviour: CallingWithLength()onVect.Zerosimply returnsVect.Zero.Therefore, in the case that
verticalMovementVectandhorizontalMovementVectwere bothVect.Zero(i.e. the user is not pressing any movement keys this frame), the call to.WithLength()will just return the same vector back (Vect.Zero). -
And finally here we move the camera with the
sumMovementVect.MoveBy()does as its name implies and moves the camera in the world by the given amount.
And like before, don't forget to actually call this method from inside TickKbm():
public static void TickKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
AdjustCameraViewDirectionKbm(input, camera, deltaTime);
AdjustCameraPositionKbm(input, camera, deltaTime); // (1)!
}
-
Note that we invoke this after
AdjustCameraViewDirectionKbm().This is important if you don't want your left/right/forward/back camera movement to always be one frame "out of sync" with which way the camera is looking.
Gamepad Camera Panning#
Next let's make it possible to move the camera using the right stick on a game controller. Let's create another static method, this time named AdjustCameraViewDirectionGamepad():
static void AdjustCameraViewDirectionGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
const float StickSensitivity = 100f; // (1)!
var horizontalRotationStrength =
input.RightStickPosition.GetDisplacementHorizontalWithDeadzone(); // (2)!
var verticalRotationStrength =
input.RightStickPosition.GetDisplacementVerticalWithDeadzone(); // (3)!
_currentHorizontalAngle +=
StickSensitivity * horizontalRotationStrength * deltaTime; // (4)!
_currentHorizontalAngle =
_currentHorizontalAngle.Normalized; // (5)!
_currentVerticalAngle -=
StickSensitivity * verticalRotationStrength * deltaTime; // (6)!
_currentVerticalAngle =
_currentVerticalAngle.Clamp(-Angle.QuarterCircle, Angle.QuarterCircle); // (7)!
_currentHorizontalPlaneDir =
Direction.Forward * (_currentHorizontalAngle % Direction.Down); // (8)!
var verticalTiltRot = _currentVerticalAngle // (9)!
% Direction.FromDualOrthogonalization(
Direction.Up,
_currentHorizontalPlaneDir
);
camera.SetViewAndUpDirection( // (10)!
_currentHorizontalPlaneDir * verticalTiltRot,
Direction.Up * verticalTiltRot
);
}
- This is just setting the maximum rotation speed of the camera, in degrees per second. Adjust to taste.
-
Here we store the value of
input.RightStickPosition.GetDisplacementHorizontalWithDeadzone()ashorizontalRotationStrength.input.RightStickPosition.DisplacementHorizontalgives us a normalized (1fto-1f) value indicating how far to the right the stick has been pushed. A value of1findicates fully to the right, a value of-1findicates fully to the left, a value of0findicates no displacement.input.RightStickPosition.GetDisplacementHorizontalWithDeadzone()gives us the same value but with a built-in deadzone, meaning the value will stay at0fa little longer until the user has pushed the control stick a little further away from the central position.The reason for using a deadzone is to eliminate so-called 'stick drift' where the centred position of a controller stick actually registers a small, slight value. Without using a deadzone, if your controller hardware is less than perfect, it will constantly rotate the camera by a small amount.
-
This is the same as the line above, except we're storing the vertical (up/down) stick displacement instead of the horizontal (left/right).
A value of
1findicates the stick is fully up,-1findicates fully down, and0findicates it is centred vertically (or within the deadzone). -
Here we add to
_currentHorizontalAngle. The amount we add, in degrees, is equal toStickSensitivity * deltaTime(giving us an angle/second), multiplied by thehorizontalRotationStrength(i.e. how far the user is moving the stick in the left/right axis).For example: If the stick is held fully to the right, we will add
StickSensitivitydegrees to the current horizontal angle per second. If it's held fully to the left, we will subtract that amount instead. -
Here we normalize
_currentHorizontalAnglefor the same reasons as before in the mouse example (eliminating floating-point error). -
On this line we're adjusting the
_currentVerticalAnglein the exact same we we did as for the horizontal angle above.Note that in this case we subtact rather than add the multiplied value, because
GetDisplacementVerticalWithDeadzone()returns a positive value for upward motion on the stick (and we decided that a positive vertical angle should indicate looking down).That being said, if you prefer an inverted camera control you could simply replace the
-=here with a+=. -
And here we clamp
_currentVerticalAnglefor the same reason again as in the mouse example. -
This line is simply calculating the
_currentHorizontalPlaneDirusing the new_currentHorizontalAngle.This code is exactly the same as in our mouse example.
-
This line is calculating a vertical tilt rotation.
This code is exactly the same as in our mouse example.
-
And finally, this line is setting the camera's view and up directions with the new values.
This code is exactly the same as in our mouse example.
And again, make sure to add this to TickGamepad():
public static void TickGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
AdjustCameraViewDirectionGamepad(input, camera, deltaTime);
}
Gamepad Camera Movement#
Finally, let's add a method AdjustCameraPositionGamepad() to allow us to move the camera using the gamepad's left stick and the two triggers:
static void AdjustCameraPositionGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
var verticalMovementMultiplier = // (1)!
input.RightTriggerPosition.GetDisplacementWithDeadzone()
- input.LeftTriggerPosition.GetDisplacementWithDeadzone();
var verticalMovementVect = verticalMovementMultiplier * Direction.Up; // (2)!
var horizontalMovementVect = Vect.Zero; // (3)!
var stickDisplacement = input.LeftStickPosition.GetDisplacementWithDeadzone(); // (4)!
var stickAngle = input.LeftStickPosition.GetPolarAngle(); // (5)!
if (stickAngle is { } horizontalMovementAngle) { // (6)!
var horizontalMovementDir = // (7)!
_currentHorizontalPlaneDir
* (Direction.Up % (horizontalMovementAngle - Angle.QuarterCircle));
horizontalMovementVect = horizontalMovementDir * stickDisplacement; // (8)!
}
var sumMovementVect = // (9)!
(horizontalMovementVect + verticalMovementVect)
.WithLength(CameraMovementSpeed * deltaTime);
camera.MoveBy(sumMovementVect); // (10)!
}
-
Here we calculate how much to move the camera in the vertical (up/down) direction.
input.RightTriggerPosition.GetDisplacementWithDeadzone()tells us how deeply the right trigger has been depressed, where0fis not at all and1fis completely depressed.input.LeftTriggerPosition.GetDisplacementWithDeadzone()gives us the same value but for the left trigger.As the name implies, each property incorporates a small deadzone so small values will return as
0f, to account for controller hardware issues.Our resultant
verticalMovementMultiplierwill be the right trigger displacement value minus the left trigger displacement value.Therefore, if only the right trigger is fully depressed,
verticalMovementMultiplierwill be1f; if only the left trigger is fully depressed, it will be-1f. -
Here we just create a 1 meter vect indicating the desired vertical movement of the camera by multiplying
Upby theverticalMovementMultiplierfrom above. -
We've already created the
verticalMovementVect; and in the next few lines we will define ourhorizontalMovementVect.Firstly, on this line, we'll define the
horizontalMovementVectvariable and initialize it toVect.Zero. -
Here we capture the normalized displacement of the left stick (with a deadzone) in to
stickDisplacement.Unlike previously with the right stick, we're not capturing the horizontal or vertical displacement, just the displacement in any direction. This equates to
1fif the stick is fully displaced in any direction (right, left, up, down, or anywhere in between); and0fif the stick is fully in the centre position. In other words, this property returns how far from the center position the stick is being pushed, regardless of direction.We don't care about displacement direction here because we're going to use the stick's angle in the next step.
-
GetPolarAngle()returns anAngle?that indicates the angle the stick is being pushed towards (ornullif it's not being pushed in any direction, i.e. it's in the centre position).An angle of 0° indicates the stick is being pushed exactly to the right, 90° exactly up, 180° exactly left, and 270° exactly down.
This convention comes from polar co-ordinate systems, hence why the method is named
GetPolarAngle(). -
Here we use a C# null-checking pattern to check that
stickAngleis notnull, and if is not, we assign the non-null value to an inline variablehorizontalMovementAngle.If
stickAngleisnull, we'll skip the next steps and leavehorizontalMovementVectasVect.Zero. -
If we've reached this line,
stickAnglewas notnullwhich means the user is moving the left stick. We will usehorizontalMovementAngleto calculate the movement direction the user wishes to move the camera towards.We calculate it by taking the
_currentHorizontalPlaneDir(remember, that's the direction the camera is facing on the horizontal plane) and rotating that around theUpaxis byhorizontalMovementAngle - 90°(that's the angle the stick is being pushed towards).(The reason we subtract 90° is because
GetPolarAngle()returns a value of 90° for the stick pointing straight up. If the stick is pointing straight up, we don't want to rotate our desired movement direction at all with respect to the camera's forward direction.) -
On the line above we worked out which direction the user wants to move the camera according to the left stick's polar angle.
On this line, we multiply that direction by the
stickDisplacementto create a horizontal movement vect. Remember,stickDisplacementtells us, on a scale of0fto1f, how far away from the center position the user has pushed the stick.Therefore, the further away from the center position the user moves the stick, the larger this vect will be.
-
Here we sum our
horizontalMovementVectandverticalMovementVectand make sure our resultant vect has a length ofCameraMovementSpeed * deltaTime.This code is exactly the same as in our keyboard example.
-
And finally here we move the camera according to our summed movement vect.
This code is exactly the same as in our keyboard example.
And of course, make sure you call this method from TickGamepad():
public static void TickGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
AdjustCameraViewDirectionGamepad(input, camera, deltaTime);
AdjustCameraPositionGamepad(input, camera, deltaTime);
}
Complete Example#
Here's the complete example that puts everything above together in one snippet:
window.LockCursor = true;
while (!loop.Input.UserQuitRequested) {
var deltaTime = (float) loop.IterateOnce().TotalSeconds;
CameraInputHandler.TickKbm(loop.Input.KeyboardAndMouse, camera, deltaTime);
CameraInputHandler.TickGamepad(loop.Input.GameControllersCombined, camera, deltaTime);
renderer.Render();
}
static class CameraInputHandler {
const float CameraMovementSpeed = 1f;
static Angle _currentHorizontalAngle = Angle.Zero;
static Angle _currentVerticalAngle = Angle.Zero;
static Direction _currentHorizontalPlaneDir = Direction.Forward;
public static void TickKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
AdjustCameraViewDirectionKbm(input, camera, deltaTime);
AdjustCameraPositionKbm(input, camera, deltaTime);
}
public static void TickGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
AdjustCameraViewDirectionGamepad(input, camera, deltaTime);
AdjustCameraPositionGamepad(input, camera, deltaTime);
}
static void AdjustCameraViewDirectionKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
const float MouseSensitivity = 0.05f;
var cursorDelta = input.MouseCursorDelta;
_currentHorizontalAngle += cursorDelta.X * MouseSensitivity;
_currentVerticalAngle += cursorDelta.Y * MouseSensitivity;
_currentHorizontalAngle = _currentHorizontalAngle.Normalized;
_currentVerticalAngle = _currentVerticalAngle.Clamp(-Angle.QuarterCircle, Angle.QuarterCircle);
_currentHorizontalPlaneDir = Direction.Forward * (_currentHorizontalAngle % Direction.Down);
var verticalTiltRot = _currentVerticalAngle % Direction.FromDualOrthogonalization(Direction.Up, _currentHorizontalPlaneDir);
camera.SetViewAndUpDirection(_currentHorizontalPlaneDir * verticalTiltRot, Direction.Up * verticalTiltRot);
}
static void AdjustCameraPositionKbm(ILatestKeyboardAndMouseInputRetriever input, Camera camera, float deltaTime) {
var positiveHorizontalYDir = camera.ViewDirection;
var positiveHorizontalXDir = Direction.FromDualOrthogonalization(Direction.Up, _currentHorizontalPlaneDir);
var horizontalMovement = XYPair<float>.Zero;
var verticalMovement = 0f;
foreach (var currentKey in input.CurrentlyPressedKeys) {
switch (currentKey) {
case KeyboardOrMouseKey.ArrowLeft:
horizontalMovement += (1f, 0f);
break;
case KeyboardOrMouseKey.ArrowRight:
horizontalMovement += (-1f, 0f);
break;
case KeyboardOrMouseKey.ArrowUp:
horizontalMovement += (0f, 1f);
break;
case KeyboardOrMouseKey.ArrowDown:
horizontalMovement += (0f, -1f);
break;
case KeyboardOrMouseKey.RightControl:
verticalMovement -= 1f;
break;
case KeyboardOrMouseKey.RightShift:
verticalMovement += 1f;
break;
}
}
var horizontalMovementVect = (positiveHorizontalXDir * horizontalMovement.X) + (positiveHorizontalYDir * horizontalMovement.Y);
var verticalMovementVect = Direction.Up * verticalMovement;
var sumMovementVect = (horizontalMovementVect + verticalMovementVect).WithLength(CameraMovementSpeed * deltaTime);
camera.MoveBy(sumMovementVect);
}
static void AdjustCameraViewDirectionGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
const float StickSensitivity = 100f;
var horizontalRotationStrength = input.RightStickPosition.GetDisplacementHorizontalWithDeadzone();
var verticalRotationStrength = input.RightStickPosition.GetDisplacementVerticalWithDeadzone();
_currentHorizontalAngle += StickSensitivity * horizontalRotationStrength * deltaTime;
_currentHorizontalAngle = _currentHorizontalAngle.Normalized;
_currentVerticalAngle -= StickSensitivity * verticalRotationStrength * deltaTime;
_currentVerticalAngle = _currentVerticalAngle.Clamp(-Angle.QuarterCircle, Angle.QuarterCircle);
_currentHorizontalPlaneDir = Direction.Forward * (_currentHorizontalAngle % Direction.Down);
var verticalTiltRot = _currentVerticalAngle % Direction.FromDualOrthogonalization(Direction.Up, _currentHorizontalPlaneDir);
camera.SetViewAndUpDirection(_currentHorizontalPlaneDir * verticalTiltRot, Direction.Up * verticalTiltRot);
}
static void AdjustCameraPositionGamepad(ILatestGameControllerInputStateRetriever input, Camera camera, float deltaTime) {
var verticalMovementMultiplier = input.RightTriggerPosition.GetDisplacementWithDeadzone() - input.LeftTriggerPosition.GetDisplacementWithDeadzone();
var verticalMovementVect = verticalMovementMultiplier * Direction.Up;
var horizontalMovementVect = Vect.Zero;
var stickDisplacement = input.LeftStickPosition.GetDisplacementWithDeadzone();
var stickAngle = input.LeftStickPosition.GetPolarAngle();
if (stickAngle is { } horizontalMovementAngle) {
var horizontalMovementDir = _currentHorizontalPlaneDir * (Direction.Up % (horizontalMovementAngle - Angle.QuarterCircle));
horizontalMovementVect = horizontalMovementDir * stickDisplacement;
}
var sumMovementVect = (horizontalMovementVect + verticalMovementVect).WithLength(CameraMovementSpeed * deltaTime);
camera.MoveBy(sumMovementVect);
}
}