- Home /
C# player rigidbody, permaTransfer and moving platform
Hello,
I'm trying to make a game by myself, it's going to be a doom-like game with modern physics engine. That's why I try to make a CharacterMotor script which use a rigidbody and a capsule collider (instead of a character controller).
The PlayerMotor script is almost done, I used the RigidbodyWalkerPlus.js script as a base, re-coded it in C#, so it supports multijumps, walljumps, and PermaLocked on moving platforms. But PermaLocked is not really realistics since it makes the player follow the platform even when he's not on it.
I need to make PermaTransfer work in order to achieve my game. So what happened when I switch to PermaTransfer is that my character teleports when switching of platform. It seams it goes where it is suppose to go but when it is in the air the velocity of the platform (from when the character leaves it) is not applied.
To sum up when my character is on the ground and jump on the moving platform it works fine, and when he is on the platform and jump on the ground, he teleports.
Here is the Player setup
Here is the PlayerMotor.cs script:
[code=CSharp]
using UnityEngine;
using System.Collections;
[AddComponentMenu("Player/FPS Input Controller")]
[RequireComponent ( typeof(CapsuleCollider), typeof (Rigidbody) )]
public class PlayerMotor : MonoBehaviour {
bool canControl = true;
private bool canSendMessages = false ;
// The current global direction we want the character to move in.
public Vector3 inputMoveDirection = Vector3.zero;
// Is the jump button held down? We use this interface instead of checking
// for the jump button directly so this script can also be used by AIs.
public bool inputJump = false;
private bool grounded = true;
private Vector3 groundNormal = Vector3.up ;
private Vector3 lastGroundNormal = Vector3.zero;
private bool walled = false;
private Vector3 wallNormal = Vector3.zero;
private Vector3 lastWallNormal = Vector3.zero;
private Transform tr;
/*CLASSES*/
public S_CharacterMotorJumping jumping = new S_CharacterMotorJumping();
public S_CharacterMotorWalljumping walljumping = new S_CharacterMotorWalljumping();
public S_CharacterMotorMovement movement = new S_CharacterMotorMovement();
public S_CharacterController controller = new S_CharacterController();
public S_CharacterMotorMovingPlatform platform = new S_CharacterMotorMovingPlatform();
void OnCollisionStay ( Collision colInfo ){
foreach ( ContactPoint contact in colInfo.contacts ) {
// If we're actually touching the ground, not just some wall.
if ( Vector3.Angle ( contact.normal, Vector3.up ) < controller.slopeLimit ) {
grounded = true;
if (contact.normal.y > 0 && contact.normal.y > groundNormal.y ) {
if ((contact.point - movement.lastHitPoint).sqrMagnitude > 0.001 || lastGroundNormal == Vector3.zero){
groundNormal = contact.normal;
} else {
groundNormal = lastGroundNormal;
}
platform.hitPlatform = contact.otherCollider.transform;
movement.hitPoint = contact.point;
movement.frameVelocity = Vector3.zero;
}
} else {
walled = true;
if ((contact.point - movement.lastHitPoint).sqrMagnitude > 0.001f || lastWallNormal == Vector3.zero){
wallNormal = contact.normal;
} else {
wallNormal = lastWallNormal;
}
}
}
}
[System.Serializable]
public class S_CharacterMotorMovement {
// The maximum horizontal speed when moving
public float maxSpeed = 20.0f;
// How much control does the character have in the air? 0 = none, 1 = full.
public float airControlAmt = 1 ;
// Curve for multiplying speed based on slope (negative = downwards)
public AnimationCurve slopeSpeedMultiplier = new AnimationCurve(new Keyframe(-90, 1), new Keyframe(0, 1), new Keyframe(90, 0));
// not used atm
// How fast does the character change speeds? Higher is faster.
public float maxGroundAcceleration = 200.0f;
public float maxAirAcceleration = 20.0f;
// The gravity for the character
public float gravity = 40.0f;
public float maxFallSpeed = 100.0f;
// The last collision flags returned from controller.Move
[System.NonSerialized] public CollisionFlags collisionFlags;
// We will keep track of the character's current velocity,
[System.NonSerialized] public Vector3 velocity;
// This keeps track of our current velocity while we're not grounded
[System.NonSerialized] public Vector3 frameVelocity = Vector3.zero;
[System.NonSerialized] public Vector3 hitPoint = Vector3.zero;
[System.NonSerialized] public Vector3 lastHitPoint = new Vector3(Mathf.Infinity, 0, 0);
}
// We will contain all the jumping related variables in one helper class for clarity.
[System.Serializable]
public class S_CharacterMotorJumping {
// Can the character jump?
public bool enabled = true;
// How many times can the player jump ?
// Default: 1. Doublejump: 2. Etc.
public int multijumps = 1 ;
[System.NonSerialized] public bool multijumpsGetExtraHeight = true ;
[System.NonSerialized] public int jumpCounter = 0 ;
// Cooldown for jumps so controls can't just rapid fire jumps.
// This is for convenience.
public float jumpCooldown = 0.2f; // Change this one.
[System.NonSerialized] public float jumpCooldownTimer = 0.0f; // Don't change this one.
// How high do we jump when pressing jump and letting go immediately
public float baseHeight = 2.0f;
// We add extraHeight units (meters) on top when holding the button down longer while jumping
public float extraHeight = 4.1f;
// How much does the character jump out perpendicular to the surface on walkable surfaces?
// 0 means a fully vertical jump and 1 means fully perpendicular.
public float perpAmount = 0.0f;
// How much does the character jump out perpendicular to the surface on too steep surfaces?
// 0 means a fully vertical jump and 1 means fully perpendicular.
public float steepPerpAmount = 0.5f;
// For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view.
// Very handy for organization!
// Are we jumping? (Initiated with jump button and not grounded yet)
// To see if we are just in the air (initiated by jumping OR falling) see the grounded variable.
[System.NonSerialized] public bool jumping = false;
[System.NonSerialized] public bool holdingJumpButton = false;
// the time we jumped at (Used to determine for how long to apply extra jump power after jumping.)
[System.NonSerialized] public float lastStartTime = 0.0f;
[System.NonSerialized] public float lastButtonDownTime = -100f;
[System.NonSerialized] public Vector3 jumpDir = Vector3.up;
}
[System.Serializable]
public class S_CharacterMotorWalljumping {
public bool enable = true;
public float baseHeight = 2.0f;
// We add extraHeight units (meters) on top when holding the button down longer while walljumping
public float extraHeight = 3.1f;
// How far do we walljump when pressing jump and letting go immediately
public float perpAmount = 0.5f;
}
private IEnumerator SubtractNewPlatformVelocity () {
// When landing, subtract the velocity of the new ground from the character's velocity
// since movement in ground is relative to the movement of the ground.
// If we landed on a new platform, we have to wait for two FixedUpdates
// before we know the velocity of the platform under the character
if (platform.enabled &&
(platform.movementTransfer == S_MovementTransferOnJump.InitTransfer ||
platform.movementTransfer == S_MovementTransferOnJump.PermaTransfer)
){
if (platform.newPlatform) {
Transform platformTmp = this.platform.activePlatform;
yield return new WaitForFixedUpdate();
yield return new WaitForFixedUpdate();
if (grounded && platformTmp == this.platform.activePlatform)
yield return new WaitForFixedUpdate();
}
movement.velocity -= this.platform.platformVelocity;
}
}
public enum S_MovementTransferOnJump {
/*
* InitTransfer and PermaTransfer are bugged
* I don't know how to fix them
*/
None, // The jump is not affected by velocity of floor at all.
InitTransfer, // Jump gets its initial velocity from the floor, then gradualy comes to a stop.
PermaTransfer, // Jump gets its initial velocity from the floor, and keeps that velocity until landing.
PermaLocked // Jump is relative to the movement of the last touched floor and will move together with that floor.
}
[System.Serializable]
public class S_CharacterMotorMovingPlatform {
public bool enabled = true;
public S_MovementTransferOnJump movementTransfer = S_MovementTransferOnJump.PermaTransfer;
[System.NonSerialized] public Transform hitPlatform;
[System.NonSerialized] public Transform activePlatform;
[System.NonSerialized] public Vector3 activeLocalPoint;
[System.NonSerialized] public Vector3 activeGlobalPoint;
[System.NonSerialized] public Quaternion activeLocalRotation;
[System.NonSerialized] public Quaternion activeGlobalRotation;
[System.NonSerialized] public Matrix4x4 lastMatrix;
[System.NonSerialized] public Vector3 platformVelocity;
[System.NonSerialized] public bool newPlatform;
}
[System.Serializable]
public class S_CharacterController {
// mimics Char controller for those few times where one is needed
public float height = 2.0f;
public float radius = 0.5f;
public float slopeLimit = 45f;
public float stepOffset = 0.3f;
public float skinWidth = 0.08f;
public float minMoveDistance = 0.0f;
public Vector3 center = Vector3.zero ;
}
void Awake () {
rigidbody.freezeRotation = true;
rigidbody.useGravity = false;
tr = transform;
}
/*void Update () {
// we use FPSInputController
}*/
void FixedUpdate (){
PlatformChange();
Vector3 velChange = Vector3.zero;
// Get desired velocity change
velChange += ApplyInputVelocityChange ( rigidbody.velocity ) ;
// Get jump stuff applied
velChange += ApplyGravityAndJumping ( rigidbody.velocity ) ;
// we reset the walled value
walled = false;
// Get platform stuff applied
ApplyPlatformVelocity ( ) ;
// Apply a force that attempts to reach our target velocity
rigidbody.AddForce( velChange, ForceMode.VelocityChange ) ;
grounded = false;
// reset hitPlatform before checking for collisions
platform.hitPlatform = null;
}
private Vector3 ApplyInputVelocityChange (Vector3 velocity){
if (!canControl){
inputMoveDirection = Vector3.zero;
}
// Find desired velocity
Vector3 desiredVelocity = Vector3.zero;
// Log Max velocity allowed
float maxVelocityChange = GetMaxAcceleration(grounded) * Time.deltaTime;
if ( grounded && TooSteep() ){
// do nothing. Let the rigidbody handle the velocity
// todo: this should really be more gradual, rather than just going from full control to 0 control @ toosteep.
} else if ( velocity.sqrMagnitude > movement.maxFallSpeed * movement.maxFallSpeed) {
// Player is moving really really fast (maybe from an explosion).
// Let this happen, but add some drag
desiredVelocity = velocity * ( 1f - .05f) ;
} else {
desiredVelocity = GetDesiredHorizontalVelocity();
}
if (platform.enabled && platform.movementTransfer == S_MovementTransferOnJump.PermaTransfer) {
desiredVelocity += movement.frameVelocity;
desiredVelocity.y = 0;
}
if (grounded){
desiredVelocity = AdjustGroundVelocityToNormal(desiredVelocity, groundNormal);
} else {
velocity.y = 0f;
}
// Enforce max velocity change
Vector3 velocityChangeVector = (desiredVelocity - velocity);
if (velocityChangeVector.sqrMagnitude > maxVelocityChange * maxVelocityChange){
velocityChangeVector = velocityChangeVector.normalized * maxVelocityChange;
}
// If we're in the air, only apply the amount of control we have
if ( ! grounded ){
velocityChangeVector *= movement.airControlAmt ;
}
// If we're on the ground and don't have control we do apply it - it will correspond to friction.
if ( ! ( grounded || canControl ) ){
velocityChangeVector = Vector3.zero ;
}
if (grounded) {
//todo: add a func so rigidbody adjusts for upcoming stair / ramp
// needs to move up by needed amt
// does extra vel.y need to be added when going down?
}
return velocityChangeVector ;
}
private Vector3 ApplyGravityAndJumping (Vector3 velocity){
// Init the vel change at zero
Vector3 velChange = Vector3.zero ;
if (
inputJump &&
!jumping.holdingJumpButton &&
jumping.lastButtonDownTime < 0f &&
canControl &&
( Time.time > jumping.jumpCooldownTimer ) &&
( ( jumping.jumpCounter < jumping.multijumps ) || walled )
){
// Record time of button down. Used to trigger delayed jumps, rather than just firing jump func here.
jumping.lastButtonDownTime = Time.time ;
}
// Jump only if the jump button was pressed down in the last 0.2 seconds.
// We use this check instead of checking if it's pressed down right now
// because players will often try to jump in the exact moment when hitting the ground after a jump
// and if they hit the button a fraction of a second too soon and no new jump happens as a consequence,
// it's confusing and it feels like the game is buggy.
if ( Time.time - jumping.lastButtonDownTime < 0.2f ){
jumping.jumping = true;
jumping.lastStartTime = Time.time;
jumping.lastButtonDownTime = -100f;
jumping.holdingJumpButton = true;
jumping.jumpCounter++ ;
jumping.jumpCooldownTimer = jumping.jumpCooldown + Time.time ;
// Calculate the jumping direction
if ( grounded && TooSteep() ){
jumping.jumpDir = Vector3.Slerp(Vector3.up, groundNormal, jumping.steepPerpAmount);
} else {
jumping.jumpDir = Vector3.Slerp(Vector3.up, groundNormal, jumping.perpAmount);
}
if ( walled && !grounded && walljumping.enable ){
jumping.jumpDir = Vector3.Slerp(Vector3.up, wallNormal, walljumping.perpAmount);
}
// Apply the jumping force to the velocity. Cancel any vertical velocity first.
velocity.y = 0f;
Vector3 v = rigidbody.velocity;
v.y = 0f;
rigidbody.velocity = v;
if (walled && !grounded){
velChange += jumping.jumpDir * CalculateJumpVerticalSpeed (walljumping.baseHeight);
} else {
velChange += jumping.jumpDir * CalculateJumpVerticalSpeed (jumping.baseHeight);
}
// Apply inertia from platform
if (platform.enabled &&
(platform.movementTransfer == S_MovementTransferOnJump.InitTransfer ||
platform.movementTransfer == S_MovementTransferOnJump.PermaTransfer)
) {
movement.frameVelocity = platform.platformVelocity;
movement.velocity += platform.platformVelocity;
}
if ( canSendMessages ){
SendMessage("OnJump", SendMessageOptions.DontRequireReceiver);
}
}
// When jumping up we don't apply gravity for some time when the user is holding the jump button.
// This gives more control over jump height by pressing the button longer.
if ( jumping.jumping && jumping.holdingJumpButton ){
// If we can jump an extra height (if we're on our first jump, or multijump extra height is ok'd)
// Calculate the duration that the extra jump force should have effect.
// If we're still less than that duration after the jumping time, apply the force.
if (
(jumping.jumpCounter == 1 || (jumping.multijumpsGetExtraHeight && jumping.jumpCounter < jumping.multijumps ) ) &&
( Time.time < jumping.lastStartTime + jumping.extraHeight / CalculateJumpVerticalSpeed ( jumping.baseHeight ) )
){
// Negate the gravity we just applied, except we push in jumpDir rather than jump upwards.
velChange += jumping.jumpDir * movement.gravity * Time.deltaTime;
}
}
// Apply gravity no matter what we're doing.
velChange.y += -movement.gravity * Time.deltaTime;
// Make sure we don't fall any faster than maxFallSpeed. This gives our character a terminal velocity.
velocity.y = Mathf.Max (velocity.y, -movement.maxFallSpeed);
if ( grounded && ! inputJump ){
jumping.jumpCounter = 0;
}
if ( ! inputJump ){
jumping.holdingJumpButton = false ;
}
if ( ! canControl ){
jumping.holdingJumpButton = false;
jumping.lastButtonDownTime = -100f;
jumping.jumpCounter = 0;
}
return velChange ;
}
private void ApplyPlatformVelocity (){
// Moving platform support
Vector3 moveDistance = Vector3.zero;
if (MoveWithPlatform()) {
Vector3 newGlobalPoint = platform.activePlatform.TransformPoint(platform.activeLocalPoint);
moveDistance = (newGlobalPoint - platform.activeGlobalPoint);
if (moveDistance != Vector3.zero)
rigidbody.MovePosition(rigidbody.position + moveDistance); //controller.Move(moveDistance);
// Support moving platform rotation as well:
Quaternion newGlobalRotation = platform.activePlatform.rotation * platform.activeLocalRotation;
Quaternion rotationDiff = newGlobalRotation * Quaternion.Inverse(platform.activeGlobalRotation);
float yRotation = rotationDiff.eulerAngles.y;
if (yRotation != 0) {
// Prevent rotation of the local up vector
tr.Rotate(0, yRotation, 0);
}
}
// Reset variables that will be set by collision function
groundNormal = Vector3.zero;
if (platform.enabled && (platform.activePlatform != platform.hitPlatform) ) {
if (platform.hitPlatform != null) {
platform.activePlatform = platform.hitPlatform;
platform.lastMatrix = platform.hitPlatform.localToWorldMatrix;
platform.newPlatform = true;
}
} else if (!grounded && IsGroundedTest()) { // We were not grounded but just landed on something
grounded = true;
jumping.jumping = false;
SubtractNewPlatformVelocity();
SendMessage("OnLand", SendMessageOptions.DontRequireReceiver);
}
// Moving platforms support
if (MoveWithPlatform()) {
// Use the center of the lower half sphere of the capsule as reference point.
// This works best when the character is standing on moving tilting platforms.
platform.activeGlobalPoint = tr.position + Vector3.up * (float)(controller.center.y - controller.height * 0.5 + controller.radius);
platform.activeLocalPoint = platform.activePlatform.InverseTransformPoint(platform.activeGlobalPoint);
// Support moving platform rotation as well:
platform.activeGlobalRotation = tr.rotation;
platform.activeLocalRotation = Quaternion.Inverse(platform.activePlatform.rotation) * platform.activeGlobalRotation;
}
}
private void PlatformChange(){
if (platform.enabled) {
Vector3 lastVelocity = Vector3.zero;
if (platform.activePlatform != null) {
if (!platform.newPlatform) {
lastVelocity = platform.platformVelocity;
platform.platformVelocity = (
platform.activePlatform.localToWorldMatrix.MultiplyPoint3x4(platform.activeLocalPoint)
- platform.lastMatrix.MultiplyPoint3x4(platform.activeLocalPoint)
) / Time.deltaTime;
}
platform.lastMatrix = platform.activePlatform.localToWorldMatrix;
platform.newPlatform = false;
} else {
platform.platformVelocity = lastVelocity;
}
}
}
private Vector3 GetDesiredHorizontalVelocity (){
// Find desired velocity
Vector3 desiredLocalDirection = tr.InverseTransformDirection(inputMoveDirection);
float maxSpeed = movement.maxSpeed;
if (grounded){
// Modify max speed on slopes based on slope speed multiplier curve
var movementSlopeAngle = Mathf.Asin(movement.velocity.normalized.y) * Mathf.Rad2Deg;
maxSpeed *= movement.slopeSpeedMultiplier.Evaluate(movementSlopeAngle);
}
Vector3 horizontalVelocity = tr.TransformDirection ( desiredLocalDirection ) ;
horizontalVelocity.y = 0f;
horizontalVelocity = horizontalVelocity.normalized * maxSpeed ;
return horizontalVelocity ;
}
private Vector3 AdjustGroundVelocityToNormal (Vector3 hVelocity, Vector3 groundNormal) {
Vector3 sideways = Vector3.Cross(Vector3.up, hVelocity);
return Vector3.Cross(sideways, groundNormal).normalized * hVelocity.magnitude;
}
private bool IsGroundedTest () {
return (groundNormal.y > 0.01f);
}
float GetMaxAcceleration (bool grounded) {
// Maximum acceleration on ground and in air
if (grounded){
return movement.maxGroundAcceleration;
} else {
return movement.maxAirAcceleration;
}
}
float CalculateJumpVerticalSpeed (float targetJumpHeight) {
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt (2 * targetJumpHeight * movement.gravity);
}
bool IsJumping () {
return jumping.jumping;
}
bool IsTouchingCeiling () {
return (movement.collisionFlags & CollisionFlags.CollidedAbove) != 0;
}
bool IsGrounded () {
return grounded;
}
bool TooSteep () {
return (groundNormal.y <= Mathf.Cos(controller.slopeLimit * Mathf.Deg2Rad));
}
bool MoveWithPlatform () {
return (
platform.enabled
&& (grounded || platform.movementTransfer == S_MovementTransferOnJump.PermaLocked)
&& platform.activePlatform != null
);
}
Vector3 GetDirection () {
return inputMoveDirection;
}
void SetControllable (bool controllable) {
canControl = controllable;
}
void SetVelocity (Vector3 velocity) {
grounded = false;
movement.velocity = velocity;
movement.frameVelocity = Vector3.zero;
if ( canSendMessages ){
SendMessage("OnExternalVelocity");
}
}
}
[/code]
here is the FPSInputController.cs script
[code=CSharp]
using UnityEngine;
using System.Collections;
[AddComponentMenu("Character/FPS Input Controller")]
[RequireComponent (typeof (PlayerMotor))]
public class FPSInputController : MonoBehaviour {
private PlayerMotor motor;
// Use this for initialization
void Awake () {
motor = gameObject.GetComponent(typeof(PlayerMotor)) as PlayerMotor;
}
// Update is called once per frame
void Update () {
// Get the input vector from keyboard or analog stick
Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
if (directionVector != Vector3.zero) {
// Get the length of the directon vector and then normalize it
// Dividing by the length is cheaper than normalizing when we already have the length anyway
float directionLength = directionVector.magnitude;
directionVector = directionVector / directionLength;
// Make sure the length is no bigger than 1
directionLength = Mathf.Min(1, directionLength);
// Make the input vector more sensitive towards the extremes and less sensitive in the middle
// This makes it easier to control slow speeds when using analog sticks
directionLength = directionLength * directionLength;
// Multiply the normalized direction vector by the modified length
directionVector = directionVector * directionLength;
}
// Apply the direction to the CharacterMotor
motor.inputMoveDirection = transform.rotation * directionVector;
motor.inputJump = Input.GetButton("Jump");
}
}
[/code]
And the regular MouseLook.cs script
sorry here is the $$anonymous$$ouseLook.cs script
[code=CSharp]
using UnityEngine;
using System.Collections;
/// $$anonymous$$ouseLook rotates the transform based on the mouse delta.
/// $$anonymous$$inimum and $$anonymous$$aximum values can be used to constrain the possible rotation
/// To make an FPS style character:
/// - Create a capsule.
/// - Add the $$anonymous$$ouseLook script to the capsule.
/// -> Set the mouse look to use LookX. (You want to only turn character but not tilt it)
/// - Add FPSInputController script to the capsule
/// -> A Character$$anonymous$$otor and a CharacterController component will be automatically added.
/// - Create a camera. $$anonymous$$ake the camera a child of the capsule. Reset it's transform.
/// - Add a $$anonymous$$ouseLook script to the camera.
/// -> Set the mouse look to use LookY. (You want the camera to tilt up and down like a head. The character already turns.)
[AddComponent$$anonymous$$enu("Camera-Control/$$anonymous$$ouse Look")]
public class $$anonymous$$ouseLook : $$anonymous$$onoBehaviour {
public enum RotationAxes { $$anonymous$$ouseXAndY = 0, $$anonymous$$ouseX = 1, $$anonymous$$ouseY = 2 }
public RotationAxes axes = RotationAxes.$$anonymous$$ouseXAndY;
public float sensitivityX = 15F;
public float sensitivityY = 15F;
public float $$anonymous$$imumX = -360F;
public float maximumX = 360F;
public float $$anonymous$$imumY = -60F;
public float maximumY = 60F;
float rotationY = 0F;
void Update ()
{
if (axes == RotationAxes.$$anonymous$$ouseXAndY)
{
float rotationX = transform.localEulerAngles.y + Input.GetAxis("$$anonymous$$ouse X") * sensitivityX;
rotationY += Input.GetAxis("$$anonymous$$ouse Y") * sensitivityY;
rotationY = $$anonymous$$athf.Clamp (rotationY, $$anonymous$$imumY, maximumY);
transform.localEulerAngles = new Vector3(-rotationY, rotationX, 0);
}
else if (axes == RotationAxes.$$anonymous$$ouseX)
{
transform.Rotate(0, Input.GetAxis("$$anonymous$$ouse X") * sensitivityX, 0);
}
else
{
rotationY += Input.GetAxis("$$anonymous$$ouse Y") * sensitivityY;
rotationY = $$anonymous$$athf.Clamp (rotationY, $$anonymous$$imumY, maximumY);
transform.localEulerAngles = new Vector3(-rotationY, transform.localEulerAngles.y, 0);
}
}
void Start ()
{
// $$anonymous$$ake the rigid body not change rotation
if (GetComponent<Rigidbody>())
GetComponent<Rigidbody>().freezeRotation = true;
}
}
[/code]