Okay, so for my first 3D Unity project I had the wonderful and totally not problematic idea of implementing a 3d platformer with a gravity change gimmick as the main mechanic. So far I’ve been able to more or less implement every aspect of it except for the player input. My approach to it was to basically make calculations relative to the world axis and then rotate the resulting vector to accommodate to the direction of gravity, however not having a clue of how stuff like quaternions work I kind of made a big mess of code.
This is the whole player movement script using the Character Controller component (and Brackeys tutorials haha very professional yes):
using UnityEngine;
//This script controls the player and player camera, powered by Cinemachine. And Brackeys tutorials. Had to do some changes though since the Brackeys tutorials didn't
//apply for any other gravity direction that wasn't straight down (obviously).
//Basically I need to make sure all the rotations and player movements are done according to the gravity direction; horizontal movement should be perpendicular to it
//and vertical movement should be parallel. What we can do is find the angle between the default gravity (downwards) and the gravity pull being applied currently,
//aswell as the axis of said rotation (a vector perpendicular to both the default and current gravity). This makes everything super complicated and hard to manage but
//it should hopefully work somewhat.
public class PlayerMovement : MonoBehaviour
{
#region Variables
[SerializeField]
float speed;
[SerializeField]
float jumpHeight;
[SerializeField]
Transform cameraPos;
[SerializeField]
CharacterController controller;
[SerializeField]
Transform groundCheck;
[SerializeField]
float groundCheckSize = 0.5f;
[SerializeField]
LayerMask groundMask;
[SerializeField]
float rotationSmoothing;
[SerializeField]
GameObject playerCamera;
float gravity = 0f; //default gravity is none
Vector3 gravityDir = new Vector3(0, 0, 0);
float gravityAngle; //angle respective to the default gravity angle (downwards).
float oldGravityAngle = 0f; //previous gravity angle used for smoothing when changing gravity planes
Vector3 gravityRotationAxis;
bool grounded;
float horizAxis, vertAxis, rotateVelocity;
Vector3 dir, moveDir;
Vector3 velocity;
#endregion
//setters and getters bc i might need those maybe
public float GetGravityMagnitude() { return gravity; }
public Vector3 GetGravityDir() { return gravityDir; }
public Vector3 GetVelocity() { return velocity; }
public bool IsGrounded() { return grounded; }
public void SetGravityMagnitude(float value) { gravity = value; }
public void SetGravityDir(Vector3 dir)
{
gravityDir = dir;
gravityAngle = Vector3.Angle(Vector3.down, gravityDir);
gravityRotationAxis = Vector3.Cross(Vector3.down, gravityDir);
}
private void Update() //god do i hate having so much stuff in update but it is what it is
{
grounded = Physics.CheckSphere(groundCheck.position, groundCheckSize, groundMask); //check if theres ground below the player
if (grounded) Debug.Log("movin on the ground");
//if there is reset their velocity (velocity is used for gravity) unless they're jumping, in which case we add upward momentum
if (grounded && velocity != Vector3.zero)
velocity = Vector3.zero;
//get player input (removes input smoothing right now, might look into whether i want input smoothing or not)
horizAxis = Input.GetAxisRaw("Horizontal");
vertAxis = Input.GetAxisRaw("Vertical");
//make it perpendicular to the gravity direction using the angle and axis calculated beforehand
dir = Quaternion.AngleAxis(gravityAngle, gravityRotationAxis) * new Vector3(horizAxis, 0f, vertAxis).normalized;
//applying the rotation to everything that needs it from now on, its much easier to do calculations based on the world axis and then rotating than trying to
//do the calculations based on the gravity direction
if (Input.GetButton("Jump") && grounded)
{
velocity.y = Mathf.Sqrt(jumpHeight * -2f * -gravity);
velocity = Quaternion.AngleAxis(gravityAngle, gravityRotationAxis) * velocity;
}
if (dir.magnitude > 0) //if the player has made any input
{
//get their move angle and add the camera angle (so the player moves according to the camera position)
float angle = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg + cameraPos.eulerAngles.y;
float smoothedAngle = Mathf.SmoothDampAngle(transform.eulerAngles.y, angle, ref rotateVelocity, rotationSmoothing); //apply angle smoothing
//float smoothedGravityAngle = Mathf.SmoothDampAngle(Vector3.Angle(Vector3.down, -transform.up), gravityAngle, ref rotateVelocity, rotationSmoothing);
//an attempt to smooth the gravity angle but it was janky af, should probably do it outside the if anyway...
transform.rotation = Quaternion.Euler(0, smoothedAngle, 0) * Quaternion.AngleAxis(gravityAngle, gravityRotationAxis); //apply the rotation
//get the direction vector based on rotation (weird math shit uuh)
moveDir = (Quaternion.Euler(0, angle, 0) * Quaternion.AngleAxis(gravityAngle, gravityRotationAxis) * Vector3.forward);
//i really hope this works well with gravity changes because i have no fucking clue how quaternions work
controller.Move(moveDir.normalized * speed * Time.deltaTime); //move the player
}
velocity += gravityDir.normalized * gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
}
Sorry for the big wall of spaghetti but I commented everything to try to explain what each part does, or at least my thought process behind it.
In any case, this code works as expected when the gravity goes straight down of course, but player movement get super janky when changing the gravity direction. Is this approach correct? If so, what is the issue? And if not, what would be a better approach?
Thanks in advance for the help!
Edit: I forgot to mention the game is in 3rd person and features a freelook camera built using Cinemachine, set to follow the player’s rotation depending on the gravity.
Edit2: I don’t know what the hell happened but now the player doesn’t move and pressing WASD in the air makes it float…