From 1acf3cd3e2dd878c52861f2107124224e994d349 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sat, 2 May 2020 15:32:44 -0500 Subject: [PATCH] coyote time --- Assets/Prefabs/Status Display.prefab | 159 ++++++++++++++++++- Assets/Scenes/SampleScene.unity | 4 +- Assets/Scripts/CameraController.cs | 56 ++++--- Assets/Scripts/MoveControllerDebugDisplay.cs | 29 ++++ Assets/Scripts/PlayerController.cs | 118 +++++++++++++- Packages/manifest.json | 2 +- 6 files changed, 338 insertions(+), 30 deletions(-) diff --git a/Assets/Prefabs/Status Display.prefab b/Assets/Prefabs/Status Display.prefab index d5a6e25..499ec37 100644 --- a/Assets/Prefabs/Status Display.prefab +++ b/Assets/Prefabs/Status Display.prefab @@ -498,13 +498,15 @@ RectTransform: - {fileID: 2864439304583037120} - {fileID: 5992904181450710442} - {fileID: 1513821230345505978} + - {fileID: 7929369720254856305} + - {fileID: 1178666589773800033} m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 240, y: 24} + m_SizeDelta: {x: 240, y: 96} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &6864253409301352422 MonoBehaviour: @@ -523,6 +525,7 @@ MonoBehaviour: rightDisplay: {fileID: 44462917099111718} aboveDisplay: {fileID: 8497902669476004809} belowDisplay: {fileID: 4292523660552820178} + jumpStateDisplay: {fileID: 1374171197480281285} --- !u!1 &4452007896964695948 GameObject: m_ObjectHideFlags: 0 @@ -600,6 +603,83 @@ MonoBehaviour: m_VerticalOverflow: 0 m_LineSpacing: 1 m_Text: False +--- !u!1 &5835746257598859911 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1178666589773800033} + - component: {fileID: 8448032696549684776} + - component: {fileID: 1374171197480281285} + m_Layer: 5 + m_Name: JumpState Status + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1178666589773800033 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5835746257598859911} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4296878337294020281} + m_RootOrder: 9 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 180, y: -108} + m_SizeDelta: {x: 120, y: 24} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8448032696549684776 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5835746257598859911} + m_CullTransparentMesh: 0 +--- !u!114 &1374171197480281285 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5835746257598859911} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: False --- !u!1 &8087166239981300898 GameObject: m_ObjectHideFlags: 0 @@ -677,3 +757,80 @@ MonoBehaviour: m_VerticalOverflow: 0 m_LineSpacing: 1 m_Text: False +--- !u!1 &8177252236619225150 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7929369720254856305} + - component: {fileID: 1633306603050821040} + - component: {fileID: 1453528757066824372} + m_Layer: 5 + m_Name: JumpState Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7929369720254856305 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8177252236619225150} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4296878337294020281} + m_RootOrder: 8 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 60, y: -108} + m_SizeDelta: {x: 120, y: 24} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1633306603050821040 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8177252236619225150} + m_CullTransparentMesh: 0 +--- !u!114 &1453528757066824372 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8177252236619225150} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Jump State diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 684881e..d58b969 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -545,7 +545,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: player: {fileID: 1313500787} - camera: {fileID: 0} + maxAcceleration: 1 + maxVelocity: 10 + smoothTime: 0.3 --- !u!20 &519420031 Camera: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/CameraController.cs b/Assets/Scripts/CameraController.cs index f7bb2aa..3cf8e12 100644 --- a/Assets/Scripts/CameraController.cs +++ b/Assets/Scripts/CameraController.cs @@ -2,25 +2,32 @@ using System.Collections.Generic; using UnityEngine; +[RequireComponent(typeof(Camera))] public class CameraController : MonoBehaviour { public Transform player; - public Camera camera; public float maxAcceleration = 1f; public float maxVelocity = 10f; private Frame frame; - private Vector3 velocity; - private Vector3 lastVelocity; - private Vector3 acceleration; - private Vector3 lastAcceleration; + // the camera's ideal position, which we will continually move towards but + // not necessarily snap to (to avoid jerky camera motion) + private Vector3 targetPosition; + + private Vector3 velocity = Vector3.zero; + public float smoothTime = 0.0025f; + // private Vector3 acceleration = Vector3.zero; + + // the values of position, velocity, and acceleration last frame + // private Vector3 lastPosition = Vector3.zero; + // private Vector3 lastVelocity = Vector3.zero; + // private Vector3 lastAcceleration = Vector3.zero; + + private Camera cam; // Start is called before the first frame update void Start() { - camera = gameObject.GetComponent(); - velocity.x = 0; - velocity.y = 0; - velocity.z = 0; + cam = gameObject.GetComponent(); } void Update() { @@ -33,23 +40,26 @@ public class CameraController : MonoBehaviour { } setupFrame(); - Vector3 targetPosition = transform.position; + targetPosition = transform.position; BoxCollider2D collider = player.GetComponent(); if (collider.bounds.max.x > frame.topRight.x) { - targetPosition.x = collider.bounds.max.x - frame.topRight.x; - } - if (collider.bounds.min.x < frame.topLeft.x) { - targetPosition.x = collider.bounds.min.x - frame.topLeft.x; + targetPosition.x += collider.bounds.max.x - frame.topRight.x; + } else if (collider.bounds.min.x < frame.topLeft.x) { + targetPosition.x += collider.bounds.min.x - frame.topLeft.x; } + if (collider.bounds.max.y > frame.topLeft.y) { - targetPosition.y = collider.bounds.max.y - frame.topLeft.y; - } - if (collider.bounds.min.y < frame.bottomLeft.y) { - targetPosition.y = collider.bounds.min.y - frame.bottomLeft.y; + targetPosition.y += collider.bounds.max.y - frame.topLeft.y; + } else if (collider.bounds.min.y < frame.bottomLeft.y) { + targetPosition.y += collider.bounds.min.y - frame.bottomLeft.y; } - + transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); + + // lastPosition = transform.position; + // lastVelocity = velocity; + // lastAcceleration = acceleration; } void OnDrawGizmosSelected() { @@ -83,10 +93,10 @@ public class CameraController : MonoBehaviour { int height = Screen.height; int width = Screen.width; - frame.topLeft = camera.ScreenToWorldPoint(new Vector3(width / 2 - width / 6, height / 2 + height / 3, 1)); - frame.topRight = camera.ScreenToWorldPoint(new Vector3(width / 2 + width / 6, height / 2 + height / 3, 1)); - frame.bottomLeft = camera.ScreenToWorldPoint(new Vector3(width / 2 - width / 6, height / 2 - height / 3, 1)); - frame.bottomRight = camera.ScreenToWorldPoint(new Vector3(width / 2 + width / 6, height / 2 - height / 3, 1)); + frame.topLeft = cam.ScreenToWorldPoint(new Vector3(width / 2 - width / 9, height / 2 + height / 3, 1)); + frame.topRight = cam.ScreenToWorldPoint(new Vector3(width / 2 + width / 9, height / 2 + height / 3, 1)); + frame.bottomLeft = cam.ScreenToWorldPoint(new Vector3(width / 2 - width / 9, height / 2 - height / 3, 1)); + frame.bottomRight = cam.ScreenToWorldPoint(new Vector3(width / 2 + width / 9, height / 2 - height / 3, 1)); } struct Frame { diff --git a/Assets/Scripts/MoveControllerDebugDisplay.cs b/Assets/Scripts/MoveControllerDebugDisplay.cs index b9dc650..83e92d7 100644 --- a/Assets/Scripts/MoveControllerDebugDisplay.cs +++ b/Assets/Scripts/MoveControllerDebugDisplay.cs @@ -9,13 +9,16 @@ public class MoveControllerDebugDisplay : MonoBehaviour { public Text rightDisplay; public Text aboveDisplay; public Text belowDisplay; + public Text jumpStateDisplay; + private PlayerController player; private MoveController moveController; // Start is called before the first frame update void Start() { if (target) { moveController = target.GetComponent(); + player = target.GetComponent(); } } @@ -27,6 +30,32 @@ public class MoveControllerDebugDisplay : MonoBehaviour { showBool(leftDisplay, moveController.collisions.left); showBool(rightDisplay, moveController.collisions.right); } + if (player) { + switch (player.jumpState) { + case PlayerController.JumpState.Grounded: + jumpStateDisplay.text = "Grounded"; + break; + case PlayerController.JumpState.CoyoteTime: + jumpStateDisplay.text = "CoyoteTime"; + break; + case PlayerController.JumpState.Ascending: + jumpStateDisplay.text = "Ascending"; + break; + case PlayerController.JumpState.Apex: + jumpStateDisplay.text = "Apex"; + break; + case PlayerController.JumpState.Descending: + jumpStateDisplay.text = "Descending"; + break; + case PlayerController.JumpState.Falling: + jumpStateDisplay.text = "Falling"; + break; + default: + jumpStateDisplay.text = "???"; + break; + } + + } } void showBool(Text label, bool value) { diff --git a/Assets/Scripts/PlayerController.cs b/Assets/Scripts/PlayerController.cs index 317a6ef..74c7c0c 100644 --- a/Assets/Scripts/PlayerController.cs +++ b/Assets/Scripts/PlayerController.cs @@ -11,24 +11,31 @@ public class PlayerController : MonoBehaviour { public float maxFallSpeed = -20f; public float timeToMaxRunSpeed = 0.15f; public float timeToMaxAirmoveSpeed = 0.25f; + public float floatTime = 0.025f; // amount of time spent at max jump height before falling again + public float coyoteTime = 0.025f; + + // this has to be public to be readable by the display, which is a code + // smell. + public JumpState jumpState; + private float jumpStateStart; private float jumpVelocity = 8f; private float gravity = -20f; private Vector3 velocity; private MoveController moveController; private float velocityXSmoothing; - private int frameCount = 0; void Start() { + setJumpState(JumpState.Falling); moveController = GetComponent(); gravity = -(2* jumpHeight) / Mathf.Pow(timeToJumpApex, 2); jumpVelocity = Mathf.Abs(gravity) * timeToJumpApex; } void Update() { - frameCount++; Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")); float targetX = input.x * moveSpeed; + Vector3 initialVelocity = velocity; if (moveController.collisions.above) { velocity.y = 0; @@ -38,22 +45,84 @@ public class PlayerController : MonoBehaviour { velocity.x = 0; } - if (moveController.isGrounded) { + switch (jumpState) { + case JumpState.Grounded: if (Input.GetButtonDown("Jump")) { + setJumpState(JumpState.Ascending); velocity.y = jumpVelocity; } else { velocity.y = gravity * Time.deltaTime; } velocity.x = Mathf.SmoothDamp(velocity.x, targetX, ref velocityXSmoothing, timeToMaxRunSpeed); - } else { + break; + + // case JumpState.Ascending: + // velocity.x = Mathf.SmoothDamp(velocity.x, targetX, ref velocityXSmoothing, timeToMaxAirmoveSpeed); + // velocity.y += gravity * Time.deltaTime; + // if (velocity.y < maxFallSpeed) { + // velocity.y = maxFallSpeed; + // } + + // // if we were rising in the last frame but will be falling in this + // // frame, we should zero out the velocity to float instead. + // if (initialVelocity.y >= 0 && velocity.y <= 0) { + // velocity.y = 0; + // setJumpState(JumpState.Apex); + // } + // break; + + case JumpState.Apex: + velocity.x = Mathf.SmoothDamp(velocity.x, targetX, ref velocityXSmoothing, timeToMaxAirmoveSpeed); + float timeFloating = Time.time - jumpStateStart; + float floatTimeRemaining = floatTime - timeFloating; + if (floatTimeRemaining < 0) { + velocity.y += gravity * -floatTimeRemaining; + setJumpState(JumpState.Descending); + } else { + velocity.y = 0; + } + break; + + case JumpState.CoyoteTime: + velocity.x = Mathf.SmoothDamp(velocity.x, targetX, ref velocityXSmoothing, timeToMaxRunSpeed); + if (Input.GetButtonDown("Jump")) { + setJumpState(JumpState.Ascending); + velocity.y = jumpVelocity; + } else { + float elapsedCoyoteTime = Time.time - jumpStateStart; + float coyoteTimeRemaining = coyoteTime - elapsedCoyoteTime; + if (coyoteTimeRemaining < 0) { + velocity.y += gravity * -coyoteTimeRemaining; + setJumpState(JumpState.Falling); + } else { + velocity.y = 0; + } + } + break; + + default: velocity.x = Mathf.SmoothDamp(velocity.x, targetX, ref velocityXSmoothing, timeToMaxAirmoveSpeed); velocity.y += gravity * Time.deltaTime; if (velocity.y < maxFallSpeed) { velocity.y = maxFallSpeed; } + + // if we were rising in the last frame but will be falling in this + // frame, we should zero out the velocity to float instead. + if (initialVelocity.y >= 0 && velocity.y <= 0) { + velocity.y = 0; + setJumpState(JumpState.Apex); + } + break; } moveController.Move(velocity * Time.deltaTime); + if (jumpState == JumpState.Grounded && !moveController.isGrounded) { + setJumpState(JumpState.CoyoteTime); + } + if (jumpState != JumpState.Grounded && moveController.isGrounded) { + setJumpState(JumpState.Grounded); + } } void OnDestroy() { @@ -64,4 +133,45 @@ public class PlayerController : MonoBehaviour { void OnTriggerEnter(Collider other) { } + + void setJumpState(JumpState state) { + jumpState = state; + jumpStateStart = Time.time; + } + + /* + + Possible JumpState transitions: + + Grounded -> Ascending : a normal jump + Grounded -> CoyoteTime : player has walked off ledge + CoyoteTime -> Ascending : player has jumped after leaving a ledge + CoyoteTime -> Falling : player has walked off of a ledge and is now falling + Ascending -> Apex : player has reached the top of their jump normally + Apex -> Descending : player has reached the top of their jump and is now falling + + */ + public enum JumpState { + // The player is on the ground + Grounded, + + // The player has not jumped; they have walked off of a platform without + // jumping + CoyoteTime, + + // The player is rising in a jump + Ascending, + + // The player has reached the apex of their jump, where they will + // briefly remain to give a feeling of lightness + Apex, + + // The player, having jumped, is in a controlled descent following the + // jump + Descending, + + // The player is descending but without control; they are falling but + // did not initially jump. + Falling, + } } diff --git a/Packages/manifest.json b/Packages/manifest.json index 13490d9..c4196a6 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -6,7 +6,7 @@ "com.unity.analytics": "3.3.5", "com.unity.collab-proxy": "1.2.16", "com.unity.ide.rider": "1.1.4", - "com.unity.ide.vscode": "1.1.4", + "com.unity.ide.vscode": "1.1.3", "com.unity.multiplayer-hlapi": "1.0.4", "com.unity.purchasing": "2.0.6", "com.unity.quicksearch": "1.6.0-preview.6",