How to create "Gravity Roads" in a FPS?
For a student project, I have to figure out a way to create "gravity roads". It would be roads to which the player would stick (kinda like cars do in F-zero or other sci-fi racers). A small subtlety is that those roads would also change the gravity accordingly. The demo/exercise will have to be played with standard FPS controls. Jump can be disabled but keeping it would be extra.
I think i have the basic principle : read the normal of the collided polygon, and rotate the gravity and character controller accordingly. I spent some hours to get this :
void OnControllerColliderHit(ControllerColliderHit hit)
{
if ( hit.controller.isGrounded ) {
if (hit.collider.tag == "GravityRoad")
{
isOnGravRoad = true;
Debug.Log(currentGravity + " " + currentGravity.magnitude);
if (hit.normal.normalized != -currentGravity.normalized)
{
currentGravity = -(hit.normal.normalized) * 9.81f;
Physics.gravity = currentGravity;
}
}
else
{
isOnGravRoad = false;
}
}
}
The gravity seems to update. But my character does not rotate and is unable to keep climbing the gravity road once the slope becomes too important.
My theory is that I can't do this with the FPS character provided in default packages, and that I have to code my own. Any input would be welcome, thank you.
Answer by hexagonius · Oct 31, 2015 at 10:29 AM
You're correct about the CharacterController. It's the one "special" collider in Unity, that doesn't allow rotation, you will need an ordinary capsule collider to mimic that, OR, you fake it by making the CharacterController round by setting it's hight to 0 (or nearly zero, not sure what happens with 0) and only rotate the visual according to surface normal. Using a rigidbody you'd definitely have a lot more work first. I'd also avoid jumping and implement following surfaces first.
I am trying to code a whole new controller. Starting slowly, I want to use a sphere as my "player" and try to make it roll on my "gravity road" with the arrow keys.
I am using SphereCast to detect the ground. It seemed to work until i realized the normal of the collided polygon was somehow always vertical, even when the road under the sphere clearly was not.
The road uses $$anonymous$$eshCollider, the sphere uses SphereCollider with Rigidbody.
Here is a screenshot : I draw the normal with a red line. It clearly always remain vertical and doesn't change. https://www.dropbox.com/s/q64q6r8uvol3wf7/unityDebug.jpg?dl=0
Any idea on what is causing this?
Answer by Hyakkidouran · Nov 05, 2015 at 09:18 AM
Thanks for helping, @Hexagonius. At this point I might as well share my new code. it's probably full of rookie mistakes I might learn how to correct. I copied a few extracts from the original FPS controller and tried to go from there.
using System;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
using UnityStandardAssets.Utility;
using Random = UnityEngine.Random;
namespace Assets.Scripts
{
public class FPS_Control : MonoBehaviour
{
[SerializeField] private bool m_IsWalking;
[SerializeField] private float m_WalkSpeed;
[SerializeField] private float m_RunSpeed;
[SerializeField] private float m_StickToGroundForce;
[SerializeField] private bool m_UseFovKick;
[SerializeField] private FOVKick m_FovKick = new FOVKick();
[SerializeField] private float m_GravityMultiplier;
private Camera m_Camera;
private bool m_Jump;
private float m_YRotation;
private Vector2 m_Input;
private Vector3 m_MoveDir = Vector3.zero;
private CollisionFlags m_CollisionFlags;
private bool isGrounded = false;
private Transform m_PhysicalBody;
private Rigidbody m_PhysicalBody_rigidBody;
private SphereCollider m_PhysicalBody_hitbox;
Vector3 currentGravity;
bool isOnGravRoad = false;
// Use this for initialization
private void Start()
{
m_PhysicalBody = transform.Find("TestPlayer_Sphere");
m_PhysicalBody_rigidBody = m_PhysicalBody.GetComponent<Rigidbody>();
m_PhysicalBody_hitbox = m_PhysicalBody.GetComponent<SphereCollider>();
currentGravity = Physics.gravity;
}
private void FixedUpdate()
{
m_PhysicalBody_rigidBody.WakeUp();
float speed;
GetInput(out speed);
// always move along the camera forward as it is the direction that it being aimed at
Vector3 desiredMove = transform.forward * m_Input.y + transform.right * m_Input.x;
// get a normal for the surface that is being touched to move along it
RaycastHit hitInfo;
Physics.SphereCast(m_PhysicalBody.position, m_PhysicalBody_hitbox.radius, currentGravity.normalized, out hitInfo,
(m_PhysicalBody_hitbox.radius+2f) / 2f);
desiredMove = Vector3.ProjectOnPlane(desiredMove, hitInfo.normal).normalized;
Debug.DrawLine(m_PhysicalBody.position + Vector3.zero, m_PhysicalBody.position + desiredMove * 20, Color.green, 1f, false);
// attempt to change the gravity depending on surface
if (hitInfo.collider != null)
{
if (hitInfo.collider.tag == "GravityRoad")
{
isOnGravRoad = true;
if (hitInfo.normal.normalized != -currentGravity.normalized)
{
currentGravity = -(hitInfo.normal.normalized) * 9.81f;
Physics.gravity = currentGravity;
}
}
else
{
isOnGravRoad = false;
}
}
//Debug.Log(hitInfo.normal);
Debug.DrawLine(m_PhysicalBody.position + Vector3.zero, m_PhysicalBody.position + hitInfo.normal*20, Color.red, 1f, false);
m_MoveDir.x = desiredMove.x * speed;
m_MoveDir.z = desiredMove.z * speed;
if (isGrounded)
{
m_MoveDir.y = -m_StickToGroundForce;
}
else
{
m_MoveDir += Physics.gravity * m_GravityMultiplier * Time.fixedDeltaTime;
}
m_PhysicalBody_rigidBody.MovePosition(m_PhysicalBody_rigidBody.position + m_MoveDir);
UpdateCameraPosition(speed);
}
private void GetInput(out float speed)
{
// Read input
float horizontal = CrossPlatformInputManager.GetAxis("Horizontal");
float vertical = CrossPlatformInputManager.GetAxis("Vertical");
bool waswalking = m_IsWalking;
// On standalone builds, walk/run speed is modified by a key press.
// keep track of whether or not the character is walking or running
m_IsWalking = !Input.GetKey(KeyCode.LeftShift);
// set the desired speed to be walking or running
speed = m_IsWalking ? m_WalkSpeed : m_RunSpeed;
m_Input = new Vector2(horizontal, vertical);
// normalize input if it exceeds 1 in combined length:
if (m_Input.sqrMagnitude > 1)
{
m_Input.Normalize();
}
// handle speed change to give an fov kick
// only if the player is going to a run, is running and the fovkick is to be used
if (m_IsWalking != waswalking && m_UseFovKick && m_PhysicalBody_rigidBody.velocity.sqrMagnitude > 0)
{
StopAllCoroutines();
StartCoroutine(!m_IsWalking ? m_FovKick.FOVKickUp() : m_FovKick.FOVKickDown());
}
}
}
}
Answer by hexagonius · Nov 05, 2015 at 07:20 AM
what value is your debug red line based of? If it's the normal from the collision object, that's always the direction the collision came in from. usually you need to raycast against the collider in that spot to get the actual surface normal
Your answer
Follow this Question
Related Questions
how can I align a rotating block that falls onto another with the correct face? 2 Answers
how would I make a capsule rotate and go down a platform on the y axis? 0 Answers
Rotate the player around the z axis, using a mouse look type script 0 Answers
Glider sim with CharacterController (no rigid body allowed) 0 Answers