Skip to content

Instantly share code, notes, and snippets.

@RednibCoding
Last active November 9, 2023 09:16
Show Gist options
  • Save RednibCoding/050990b7d3fab28455dafe883fdbfc3c to your computer and use it in GitHub Desktop.
Save RednibCoding/050990b7d3fab28455dafe883fdbfc3c to your computer and use it in GitHub Desktop.
MmoCharacterController for the flax engine
/*
How to use:
1. Create a CharacterController in your scene
2. Add an AnimatedModel (your player character) as child to the CharacterController
3. Adjust the size of the CharacterController capsule so it fits the size of your player character
4. Add a Camera as child to the CharacterController
5. Set the Layer of the CharacterController to "Player" (can be found under Properties->General->Layer while the PlayerController is selected)
6. Add this MmoCharacterController script to the CharacterController
*/
using System;
using FlaxEngine;
namespace Game
{
enum CameraRotationMode
{
None,
CameraOnly,
PlayerAndCamera,
}
/// <summary>
/// MmoRpgCharacterController Script.
/// </summary>
public class MmoRpgCharacterController : Script
{
// Mouse stuff
[Range(0.1f, 10f), Tooltip("Adjust the mouse sensitivity when rotating the camera with the mouse.")]
public float MouseSensitivity = 0.5f;
// Camera Distance from the player
[Range(10f, 1000f), Tooltip("The initial camera distance between the player and the camera.")]
public float CameraDistance = 280f;
[Range(10f, 100f), Tooltip("The minimum camera distance between the player and the camera.")]
public float MinCameraDistance = 50f;
[Range(100f, 1000f), Tooltip("The maximum camera distance between the player and the camera.")]
public float MaxCameraDistance = 500f;
[Range(0f, 1000f), Tooltip("The minimum distance between the camera and the player before the player gets invisible")]
public float PlayerVisibilityThreshold = 35f;
[Range(100f, 1000f), Tooltip("Determines against what the camera can collide.")]
public LayersMask CameraCollisionMask;
[Range(1f, 1000f), Tooltip("The zoom speed determines how fast the camera zooms.")]
public float ZoomSpeed = 300f;
[Tooltip("When true, the camera will rotate back behind the player.")]
public bool CameraAutoReset = true;
[Range(1f, 100f), Tooltip("The speed how fast the camera will rotate back behind the player.")]
public float CameraResetSpeed = 5.0f; // Adjust this value to change the speed of the camera reset
// Movement
[Range(1f, 1000f), Tooltip("How fast the player will move.")]
public float PlayerSpeed = 300;
[Range(1f, 10000f), Tooltip("The gravity determines how fast the player will fall to the ground.")]
public float PlayerGravity = 981;
[Range(1f, 10000f), Tooltip("The higher the jump force, the higher the player will jump.")]
public float PlayerJumpForce = 500;
[Range(1f, 100f), Tooltip("How much the movement gets dumped down while the player is in air.")]
public float AirDampingFactor = 0.80f;
[Tooltip("Determines whether the player should move forward when both (left and right) mouse buttons are pressed.")]
public bool PlayerMovesWhenBothMouseButtonsDown = true;
[HideInEditor]
// Expose it to the outside
public bool IsGrounded;
// The player character
// The normalized movement vector you can use to determine in which direction the player moves
// This vector is local to the player and its look direction
[HideInEditor]
public Vector3 MovementVector;
private AnimatedModel playerModel;
// Camera rotation
private float cameraRotationX = 0;
private float cameraRotationY = 0;
private Vector3 lastGroundedMoveDirection = Vector3.Zero;
private float rotationDifference = 0f;
private CameraRotationMode rotationMode { get; set; } = CameraRotationMode.None;
// Fall speed
private float yVelocity;
// CharacterController
private CharacterController self;
// Camera
private Camera camera;
// Where the camera will look to
private Actor cameraTarget;
/// <inheritdoc/>
public override void OnStart()
{
self = Actor.As<CharacterController>();
// Get the player model
playerModel = Actor.GetChild<AnimatedModel>();
if (playerModel == null)
throw new Exception("Player model is missing: add an AnimatedModel to the player controller.");
// Get and orient the camera
camera = Actor.GetChild<Camera>();
if (camera == null)
throw new Exception("Camera is missing: add a camera to the player controller.");
camera.Layer = self.Layer;
// Create the camera target
cameraTarget = self.AddChild<EmptyActor>();
cameraTarget.LocalPosition = new Vector3(0, self.Height + self.Center.Y, 0);
cameraTarget.Layer = self.Layer;
if (CameraCollisionMask == null)
throw new Exception("Camera collision mask is missing: Please define a camera collision mask");
camera.Parent = cameraTarget;
camera.Layer = self.Layer;
camera.LookAt(cameraTarget.Position);
camera.LocalTransform = new Transform(new Vector3(0f, 0f, -CameraDistance));
}
public override void OnFixedUpdate()
{
var moveInput = new Vector3();
// Move forward when both left and right mouse buttons are pressed
if (Input.GetMouseButton(MouseButton.Left) && Input.GetMouseButton(MouseButton.Right) && PlayerMovesWhenBothMouseButtonsDown)
{
moveInput.Z = 1;
}
moveInput.Z += Input.GetAxis("Vertical");
moveInput.X = Input.GetAxis("Horizontal");
moveInput = Vector3.ClampLength(moveInput, 1);
Float3 eulerAngles;
switch (rotationMode)
{
case CameraRotationMode.PlayerAndCamera:
eulerAngles = cameraTarget.EulerAngles;
break;
case CameraRotationMode.None:
case CameraRotationMode.CameraOnly:
eulerAngles = playerModel.EulerAngles;
break;
default:
eulerAngles = playerModel.EulerAngles;
break;
}
if (rotationMode == CameraRotationMode.None)
{
float rotationSpeed = 2.0f;
eulerAngles.Y += moveInput.X * rotationSpeed;
playerModel.EulerAngles = eulerAngles;
}
var lookRotationYaw = new Vector3(0, eulerAngles.Y, 0);
Vector3 lookDirection;
if (rotationMode == CameraRotationMode.None || rotationMode == CameraRotationMode.CameraOnly)
{
// Rotate only the forward/backward movement (Z-axis) by the character's current rotation
lookDirection = Vector3.Transform(new Vector3(0, 0, moveInput.Z), Quaternion.Euler(lookRotationYaw));
}
else
{
playerModel.EulerAngles = lookRotationYaw;
lookDirection = Vector3.Transform(moveInput, Quaternion.Euler(lookRotationYaw));
}
// Normalize the lookDirection vector
lookDirection.Normalize();
if (self.IsGrounded)
{
lastGroundedMoveDirection = lookDirection * PlayerSpeed * Time.DeltaTime;
self.Move(lastGroundedMoveDirection);
}
else
{
self.Move(lastGroundedMoveDirection);
}
// Apply gravity
yVelocity -= PlayerGravity * Time.DeltaTime;
self.Move(new Vector3(0, yVelocity, 0) * Time.DeltaTime);
if (self.IsGrounded)
{
yVelocity = -250;
if (Input.GetAction("Jump"))
{
yVelocity = PlayerJumpForce;
}
}
}
/// <inheritdoc/>
public override void OnUpdate()
{
updateCameraRotation();
updateCameraDistance();
updateMovementVector();
updateCameraObstacle();
}
private void updateCameraObstacle()
{
var cameraTargetPosition = cameraTarget.Position;
var cameraPosition = camera.Position;
var direction = cameraPosition - cameraTargetPosition;
var distance = CameraDistance;
direction.Normalize();
var ray = new Ray(cameraTargetPosition, direction);
float offset = 20.0f; // Set the desired offset value here
if (Physics.RayCast(ray.Position, ray.Direction, out var hit, distance, CameraCollisionMask))
{
// Move the camera to the hit point
Vector3 hitPoint = hit.Point;
camera.Position = hitPoint - direction * offset;
}
else
{
// Make sure the camera is at the correct distance if there are no obstacles
camera.LocalPosition = new Vector3(0f, 0f, -CameraDistance);
}
// Calculate the distance between the camera and the the player
float camDistance = Vector3.Distance(playerModel.Position, camera.Position);
// Toggle the visibility of the character based on the distance
playerModel.IsActive = camDistance > PlayerVisibilityThreshold;
}
// Update the players movement vector
// MovementVector can then be used in other scripts to do stuff depending on the movement vector of the player
// The MovementVector is normalized and local to the player and his look direction
private void updateMovementVector()
{
IsGrounded = self.IsGrounded;
// Get the character's global velocity
Vector3 globalVelocity = self.Velocity;
// Get the character's direction vectors
Vector3 forwardDirection = playerModel.Transform.Forward;
Vector3 backwardDirection = playerModel.Transform.Backward;
Vector3 leftDirection = playerModel.Transform.Left;
Vector3 rightDirection = playerModel.Transform.Right;
// Calculate the dot product of the direction vectors and global velocity
float forwardVelocity = Vector3.Dot(forwardDirection, globalVelocity);
float backwardVelocity = Vector3.Dot(backwardDirection, globalVelocity);
float leftVelocity = Vector3.Dot(leftDirection, globalVelocity);
float rightVelocity = Vector3.Dot(rightDirection, globalVelocity);
// Get the vertical velocity
float verticalVelocity = globalVelocity.Y;
// Define a threshold for detecting movement in each direction
float threshold = 0.1f;
// Set the MovementVector components based on the calculated velocities
MovementVector.Z = (forwardVelocity > threshold) ? 1 : (backwardVelocity > threshold) ? -1 : 0;
MovementVector.X = (rightVelocity > threshold) ? 1 : (leftVelocity > threshold) ? -1 : 0;
if (self.IsGrounded)
MovementVector.Y = 0;
else
// Set the MovementVector.Y based on the vertical velocity
MovementVector.Y = (verticalVelocity > threshold) ? 1 : (verticalVelocity < -threshold) ? -1 : 0;
}
private void updateCameraDistance()
{
float scrollWheel = Input.MouseScrollDelta * 10;
if (scrollWheel != 0)
{
CameraDistance -= scrollWheel * ZoomSpeed * Time.DeltaTime;
CameraDistance = Mathf.Clamp(CameraDistance, MinCameraDistance, MaxCameraDistance);
camera.LocalPosition = new Vector3(0f, 0f, -CameraDistance);
}
}
private bool transitioningBack = false;
private void updateCameraRotation()
{
CameraRotationMode previousRotationMode = rotationMode;
if (Input.GetMouseButton(MouseButton.Right))
rotationMode = CameraRotationMode.PlayerAndCamera;
else if (Input.GetMouseButton(MouseButton.Left))
rotationMode = CameraRotationMode.CameraOnly;
else
rotationMode = CameraRotationMode.None;
if (previousRotationMode != CameraRotationMode.PlayerAndCamera && rotationMode == CameraRotationMode.PlayerAndCamera)
{
rotationDifference = 0f;
}
if (previousRotationMode != CameraRotationMode.CameraOnly && rotationMode == CameraRotationMode.CameraOnly)
{
cameraRotationX = cameraTarget.EulerAngles.Y;
cameraRotationY = cameraTarget.EulerAngles.X;
}
if (previousRotationMode == CameraRotationMode.CameraOnly && rotationMode == CameraRotationMode.None)
{
transitioningBack = CameraAutoReset;
if (!CameraAutoReset)
{
rotationDifference = cameraTarget.EulerAngles.Y - playerModel.EulerAngles.Y;
}
else
{
rotationDifference = 0f;
}
}
switch (rotationMode)
{
case CameraRotationMode.None:
{
Screen.CursorLock = CursorLockMode.None;
Screen.CursorVisible = true;
Vector3 characterEulerAngles = playerModel.EulerAngles;
if (transitioningBack)
{
float angleDifference = Mathf.DeltaAngle(cameraRotationX, characterEulerAngles.Y);
float rotationSpeed = CameraResetSpeed * Time.DeltaTime;
cameraRotationX = Mathf.MoveTowardsAngle(cameraRotationX, characterEulerAngles.Y, Mathf.Abs(angleDifference) * rotationSpeed);
if (Mathf.Abs(angleDifference) < 0.1f)
{
transitioningBack = false;
}
}
else
{
cameraRotationX = characterEulerAngles.Y + rotationDifference;
}
cameraTarget.EulerAngles = new Vector3(cameraTarget.EulerAngles.X, cameraRotationX, 0);
break;
}
case CameraRotationMode.CameraOnly:
{
Screen.CursorLock = CursorLockMode.Locked;
Screen.CursorVisible = false;
// Update character rotation based on the keyboard input
if (playerModel != null)
{
Vector3 characterEulerAngles = playerModel.EulerAngles;
float rotationSpeed = 2.0f;
characterEulerAngles.Y += Input.GetAxis("Horizontal") * rotationSpeed;
playerModel.EulerAngles = characterEulerAngles;
}
// Update camera rotation based on the mouse input
cameraRotationX += Input.GetAxis("Mouse X") * MouseSensitivity;
cameraRotationY += Input.GetAxis("Mouse Y") * MouseSensitivity;
cameraRotationY = Mathf.Clamp(cameraRotationY, -90, 90);
cameraTarget.EulerAngles = new Vector3(cameraRotationY, cameraRotationX, 0);
break;
}
case CameraRotationMode.PlayerAndCamera:
{
Screen.CursorLock = CursorLockMode.Locked;
Screen.CursorVisible = false;
cameraRotationX += Input.GetAxis("Mouse X") * MouseSensitivity;
cameraRotationY += Input.GetAxis("Mouse Y") * MouseSensitivity;
cameraRotationY = Mathf.Clamp(cameraRotationY, -90, 90);
cameraTarget.EulerAngles = new Vector3(cameraRotationY, cameraRotationX, 0);
break;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment