using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using NaughtyAttributes; using UnityEngine.Timeline; #if UNITY_EDITOR using UnityEditor; #endif /// <summary> /// Created by Jeremy Bond for MI 231 at Michigan State University /// Built to work with a modified version of the GMTK Platformer Toolkit /// </summary> [CreateAssetMenu( fileName = "GMTK_Settings_[GameName]", menuName = "ScriptableObjects/GMTK_Settings", order = 1 )] public class Character_Settings_SO : ScriptableObject { static bool DEBUG_JUMP_LINE_CALCULATION = false; [Header( "Movement Stats" )] [SerializeField, Range( 0f, 20f )] [Tooltip( "Maximum movement speed" )] public float maxSpeed = 10f; [SerializeField, Range( 0f, 100f )] [Tooltip( "How fast to reach max speed" )] public float maxAcceleration = 52f; [SerializeField, Range( 0f, 100f )] [Tooltip( "How fast to stop after letting go" )] public float maxDeceleration = 52f; [SerializeField, Range( 0f, 100f )] [Tooltip( "How fast to stop when changing direction" )] public float maxTurnSpeed = 80f; [SerializeField, Range( 0f, 100f )] [Tooltip( "How fast to reach max speed when in mid-air" )] public float maxAirAcceleration = 0; [SerializeField, Range( 0f, 100f )] [Tooltip( "How fast to stop in mid-air when no direction is used" )] public float maxAirDeceleration = 0; [SerializeField, Range( 0f, 100f )] [Tooltip( "How fast to stop when changing direction when in mid-air" )] public float maxAirTurnSpeed = 80f; [SerializeField] [Tooltip( "Friction to apply against movement on stick" )] public float friction = 0; [Header( "Movement Options" )] [Tooltip( "When false, the charcter will skip acceleration and deceleration and instantly move and stop" )] public bool useAcceleration = true; // NOTE: CGSK jummp math comes from Math for Game Programmers: Building a Better Jump // https://www.youtube.com/watch?v=hG9SzQxaCm8&t=9m35s & https://www.youtube.com/watch?v=hG9SzQxaCm8&t=784s // Th = Xh/Vx V0 = 2H / Th G = -2H / (Th * Th) V0 = 2HVx / Xh G = -2H(Vx*Vx) / (Xh*Xh) [Header( "Jump Settings" )] public eJumpSettingsType jumpSettingsType = eJumpSettingsType.CGSK_Time; public enum eJumpSettingsType { CGSK_Distance, CGSK_Time, GMTK_GameMakersToolKit }; public bool showJumpLine = true; [Tooltip( "Maximum jump height" )] [Range( 1f, 10f )] public float jumpHeight = 4f; [ShowIf( "jumpSettingsType", eJumpSettingsType.CGSK_Time )] public CGSK_JumpSettings_Time jumpSettingsTime; [ShowIf( "jumpSettingsType", eJumpSettingsType.CGSK_Distance )] public CGSK_JumpSettings_Distance jumpSettingsDistance; [HideIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] public CGSK_JumpSettings_VariableHeight jumpSettingsVariableHeightCGSK; [ShowIf("jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] public CGSK_JumpSettings_VariableHeight jumpSettingsVariableHeightGMTK; [HideInInspector] internal CGSK_JumpSettings_VariableHeight jumpSettingsVariableHeight; // [HideIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] // [XnTools.ReadOnly][BoxGroup("CGSK Derived Jump Properties")] // public float jumpDistUp, jumpDurationUp, jumpVelUp, jumpGravUp, jumpDistDown, jumpDurationDown, jumpVelDown, jumpGravDown; [HideIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] [BoxGroup( "Derived Jump Properties" )] public CSSO_FloatUpDown jumpDist, jumpDuration, jumpVel, jumpGrav; [BoxGroup( "Derived Jump Properties" )] [SerializeField] [XnTools.ReadOnly] internal Vector2 maxJumpDistHeight, minJumpDistHeight; [ShowIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] public GMTK_JumpSettings jumpSettingsGMTK; [Header( "Jump Options" )] [SerializeField] [Tooltip( "The fastest speed the character can fall" )] public float speedLimit = 26.45f; [SerializeField, Range( 0f, 0.3f )] [Tooltip( "How long should coyote time last?" )] public float coyoteTime = 0.15f; [SerializeField, Range( 0f, 0.3f )] [Tooltip( "How far from ground should we cache your jump?" )] public float jumpBuffer = 0.15f; [Tooltip( "Max jumps between grounding. (2 for Double Jump, 3 for Triple Jump, etc.) " )] [Range( 1, 10 )] public int jumpsBetweenGrounding = 1; [Header( "Juice Settings - Squash and Stretch" )] [SerializeField] public bool squashAndStretch; [SerializeField, Tooltip( "Width Squeeze, Height Squeeze, Duration" )] public Vector3 jumpSquashSettings; [SerializeField, Tooltip( "Width Squeeze, Height Squeeze, Duration" )] public Vector3 landSquashSettings; [SerializeField, Tooltip( "How powerful should the effect be?" )] public float landSqueezeMultiplier; [SerializeField, Tooltip( "How powerful should the effect be?" )] public float jumpSqueezeMultiplier; [SerializeField] public float landDrop = 1; [Header( "Juice Settings - Tilting" )] [SerializeField] public bool leanForward; [SerializeField, Tooltip( "How far should the character tilt?" )] public float maxTilt; [SerializeField, Tooltip( "How fast should the character tilt?" )] public float tiltSpeed; // public float timeToJumpApex; // [ShowIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] // [SerializeField, Range( 0f, 5f )] // [Tooltip( "Gravity multiplier to apply when going up" )] // public float upwardMovementMultiplier = 1f; // [ShowIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] // [SerializeField, Range( 1f, 10f )] // [Tooltip( "Gravity multiplier to apply when coming down" )] // public float downwardMovementMultiplier = 6.17f; // [ShowIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] // [SerializeField, Range( 0, 1 )] // [Tooltip( "How many times can you jump in the air?" )] // public int maxAirJumps = 0; // [ShowIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] // [Tooltip( "Should the character drop when you let go of jump?" )] // public bool variableJumpHeight; // [ShowIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] // [SerializeField, Range( 1f, 10f )] // [Tooltip( "Gravity multiplier when you let go of jump" )] // public float jumpCutOff; private void OnValidate() { switch ( jumpSettingsType ) { case eJumpSettingsType.CGSK_Time: CalculateDerivedJumpValues_Time(); break; case eJumpSettingsType.CGSK_Distance: CalculateDerivedJumpValues_Distance(); break; case eJumpSettingsType.GMTK_GameMakersToolKit: CalculateDerivedJumpValues_GMTK(); break; } CalculateJumpLine(); } static private int jumpLineResolution = 64; // NOTE: This must be a positive even number internal Vector3[] jumpLinePoints; internal List<Vector3> minJumpLinePoints; // [HideIf( "jumpSettingsType", eJumpSettingsType.GMTK_GameMakersToolKit )] internal Vector3[] jumpStartMidEndPoints, minJumpStartMidEndPoints; internal Vector2 minTimeApexFull; internal void CalculateJumpLine() { if ( jumpSettingsType == eJumpSettingsType.GMTK_GameMakersToolKit ) { jumpLinePoints = null; return; } maxJumpDistHeight = Vector2.zero; Vector3 acc = new Vector3( 0, jumpGrav.up, 0 ); Vector3 newAcc = acc; Vector3 p = Vector3.zero; jumpLinePoints = new Vector3[jumpLineResolution]; jumpStartMidEndPoints = new Vector3[3]; jumpLinePoints[0] = p; jumpStartMidEndPoints[0] = p; Vector3 v = new Vector3( maxSpeed, jumpVel.up, 0 ); int numSteps = jumpLineResolution / 2; // Jumping Up float timeStepUp = jumpDuration.up / (float) numSteps; int i = 1; for ( ; i <= jumpLineResolution / 2; i++ ) { SimplifiedVelocityVerletIntegration( ref p, ref v, acc, timeStepUp ); // p.x += v.x * timeStep; // v.y += jumpGrav.up * timeStep; // p.y += v.y * timeStep; jumpLinePoints[i] = p; } jumpStartMidEndPoints[1] = p; maxJumpDistHeight.y = p.y; // Jumping Down acc.y = jumpGrav.down; float timeStepDown = jumpDuration.down / (float) ( numSteps - 1 ); for ( ; i < jumpLineResolution; i++ ) { SimplifiedVelocityVerletIntegration( ref p, ref v, acc, timeStepDown ); // p.x += v.x * timeStep; // v.y += jumpGrav.down * timeStep; // p.y += v.y * timeStep; jumpLinePoints[i] = p; } jumpStartMidEndPoints[2] = p; maxJumpDistHeight.x = p.x; // Calculate jump line if jump button is released immediately if ( !jumpSettingsVariableHeight.useVariableJumpHeight ) { minJumpLinePoints = null; return; } minJumpDistHeight = Vector2.zero; minTimeApexFull = Vector2.zero; v = new Vector3( maxSpeed, jumpVel.up, 0 ); newAcc = acc = new Vector3( 0, jumpGrav.up, 0 ); p = Vector3.zero; minJumpLinePoints = new List<Vector3>(); minJumpStartMidEndPoints = new Vector3[3]; minJumpLinePoints.Add( p ); minJumpStartMidEndPoints[0] = p; // We'll use timeStepUp, the timeStep from jumpDuration.up i = 0; float time = 0; int minJumpPhase = 0; // 0=up and button held, 1=button released, 2=down Vector3 debugAcc = new Vector3(acc.y, -1, -1); float minTimeStep = 0.01f; for ( ; time < 10; i++ ) { // time<10 or i<jumpLineResolution to keep it from getting out of hand time += minTimeStep; if ( minJumpPhase == 0 ) { // Up and button held if ( v.y <= 0 || time >= jumpSettingsVariableHeight.minJumpButtonHeldTime ) { if ( v.y <= 0 || jumpSettingsVariableHeight.upwardVelocityZeroing ) { v.y = 0; newAcc.y = jumpGrav.down; debugAcc.z = newAcc.y; minJumpStartMidEndPoints[1] = p; minJumpPhase = 2; minTimeApexFull.x = time; } else { newAcc.y = jumpGrav.up * jumpSettingsVariableHeight.gravUpMultiplierOnRelease; debugAcc.y = newAcc.y; minJumpPhase = 1; } } } else if ( minJumpPhase == 1 ) { // Still up, but button has been released if ( v.y <= 0 ) { // We're starting down newAcc.y = jumpGrav.down; debugAcc.z = newAcc.y; minJumpStartMidEndPoints[1] = p; minJumpDistHeight.y = p.y; minJumpPhase = 2; minTimeApexFull.x = time; } } else { // minJumpPhase == 2 // Moving down if ( p.y < 0 ) break; // This shouldn't ever happen because it should be caught after last SVVI call in minJumpPhase 2 } VelocityVerletIntegration( ref p, ref v, ref acc, newAcc, minTimeStep ); if ( p.y < 0 ) { p.y = 0; // This is a fudging of the numbers, but it should be ok. - JGB 2023-03-12 minJumpStartMidEndPoints[2] = p; minJumpLinePoints.Add( p ); minJumpDistHeight.x = p.x; minTimeApexFull.y = time; break; } minJumpLinePoints.Add( p ); } if (DEBUG_JUMP_LINE_CALCULATION) Debug.LogWarning( $"gUMOR: {jumpSettingsVariableHeight.gravUpMultiplierOnRelease:0.##}" + $"\tp0acc: {debugAcc.x:#,0.##}" + $"\tp1acc: {debugAcc.y:#,0.##}" + $"\tp2acc: {debugAcc.z:#,0.##}"); } // NOTE: Simplified Velocity Verlet Integration from Math for Game Programmers: Building a Better Jump // https://www.youtube.com/watch?v=hG9SzQxaCm8&t=23m2s void SimplifiedVelocityVerletIntegration( ref Vector3 pos, ref Vector3 vel, Vector3 acc, float deltaTime ) { pos += vel * deltaTime + acc * ( 0.5f * deltaTime * deltaTime ); // pos += vel*dT + 1/2*acc*dT*dT vel += acc * deltaTime; } void VelocityVerletIntegration( ref Vector3 pos, ref Vector3 vel, ref Vector3 acc, Vector3 newAcc, float deltaTime ) { pos += vel * deltaTime + acc * ( 0.5f * deltaTime * deltaTime ); // pos += vel*dT + 1/2*acc*dT*dT vel += (acc + newAcc) * (0.5f * deltaTime); acc = newAcc; } public float scale( float OldMin, float OldMax, float NewMin, float NewMax, float OldValue ) { float OldRange = ( OldMax - OldMin ); float NewRange = ( NewMax - NewMin ); float NewValue = ( ( ( OldValue - OldMin ) * NewRange ) / OldRange ) + NewMin; return ( NewValue ); } [System.Serializable] public class CGSK_JumpSettings_Time { [Header( "Classic Game Starter Kit - Time Jump Settings" )] [Tooltip( "The full duration of the shortest jump possible (by tapping the button)" )] public float fullJumpDurationMin = 0.5f; [Tooltip( "The full duration of the longest jump possible (by holding the button)" )] public float fullJumpDurationMax = 1f; [Tooltip( "The fraction of the jump that is going up" )] [Range( 0.05f, 0.95f )] public float jumpApexFraction = 0.6f; } private void CalculateDerivedJumpValues_Time() { jumpDuration.up = jumpSettingsTime.fullJumpDurationMax * jumpSettingsTime.jumpApexFraction; jumpDuration.down = jumpSettingsTime.fullJumpDurationMax - jumpDuration.up; jumpVel.up = jumpHeight * 2 / jumpDuration.up; jumpVel.down = jumpHeight * 2 / jumpDuration.down; // This is the velocity when the character lands. - GB 2023-03-10 jumpGrav.up = -2 * jumpHeight / ( jumpDuration.up * jumpDuration.up ); jumpGrav.down = -2 * jumpHeight / ( jumpDuration.down * jumpDuration.down ); jumpDist.up = jumpDuration.up * maxSpeed; jumpDist.down = jumpDuration.down * maxSpeed; jumpSettingsVariableHeight = jumpSettingsVariableHeightCGSK; } [System.Serializable] public class CGSK_JumpSettings_Distance { [Header( "Classic Game Starter Kit - Distance Jump Settings" )] [Tooltip( "The horizontal distance at full run speed of the shortest jump possible (by tapping the button)" )] public float fullJumpDistanceMin = 0.5f; [Tooltip( "The horizontal distance at full run speed of the longest jump possible (by holding the button)" )] public float fullJumpDistanceMax = 1f; [Tooltip( "The fraction of the jump that is going up" )] [Range( 0.05f, 0.95f )] public float jumpApexFraction = 0.6f; } private void CalculateDerivedJumpValues_Distance() { jumpDist.up = jumpSettingsDistance.fullJumpDistanceMax * jumpSettingsDistance.jumpApexFraction; jumpDist.down = jumpSettingsDistance.fullJumpDistanceMax - jumpDist.up; // Th = Xh / Vh jumpDuration.up = jumpDist.up / maxSpeed; jumpDuration.down = jumpDist.down / maxSpeed; // Vy = 2hVh / Xh jumpVel.up = 2 * jumpHeight * maxSpeed / jumpDist.up; jumpVel.down = 2 * jumpHeight * maxSpeed / jumpDist.down; // G = -2h(Vx*Vx) / (Xh*Xh) jumpGrav.up = -2 * jumpHeight * ( maxSpeed * maxSpeed ) / ( jumpDist.up * jumpDist.up ); jumpGrav.down = -2 * jumpHeight * ( maxSpeed * maxSpeed ) / ( jumpDist.down * jumpDist.down ); jumpSettingsVariableHeight = jumpSettingsVariableHeightCGSK; } [System.Serializable] public class CGSK_JumpSettings_VariableHeight { [Header( "Classic Game Starter Kit - Variable Jump Height" )] [Tooltip("Should the character jump differently based on how long the jump button is held?")] public bool useVariableJumpHeight = true; [Tooltip( "Should upward velocity be set to 0 when the jump button is released? (Like in Metroid for NES)" )] public bool upwardVelocityZeroing = false; [Tooltip( "The minimum amount of time that the jump button will be forced to be held" + " Set this to 0.1f if you want to ensure that the player can't release the button before 0.1 seconds have passed." + " 0.05f is the default value because 100ms is a typical shortest time for a button to be held." )] [Range( 0.05f, 2f )] public float minJumpButtonHeldTime = 0.05f; // 100ms is a typical shortest time for a button to be held.; [Tooltip( "The multiplier applied to jumpGrav.up to slow upward velocity faster after the jump button has been released." + "\nIf this is set to 1 and upwardVelocityZeroing=false, then it is the same as useVariableJumpHeight=false." + "\nIf this were extremely high, it would similar to upwardVelocityZeroing=true." )] [Range(1,20)] public float gravUpMultiplierOnRelease = 1; } // NOTE: CGSK jummp math comes from Math for Game Programmers: Building a Better Jump // https://www.youtube.com/watch?v=hG9SzQxaCm8&t=9m35s & https://www.youtube.com/watch?v=hG9SzQxaCm8&t=784s // Th = Xh/Vx V0 = 2H / Th G = -2H / (Th * Th) V0 = 2HVx / Xh G = -2H(Vx*Vx) / (Xh*Xh) [System.Serializable] public class GMTK_JumpSettings { [Header( "Jump Settings - GameMakers ToolKit" )] [SerializeField, Range( 1f, 10f )] [Tooltip( "This number is converted from the rather meaningless [1..10] to a time to jump apex of [0.2sec..1.25sec]" )] public float jumpDuration = 5; [SerializeField, Range( 1f, 10f )] [Tooltip( "Gravity multiplier to apply when coming down" )] public float downGravity = 6.17f; public bool doubleJump = false; [Tooltip( "Should the character drop when you let go of jump?" )] public bool variableJumpHeight; [SerializeField, Range( 1f, 10f )] [ShowIf("useVariableJumpHeight")] [Tooltip( "Gravity multiplier when you let go of jump and character is still moving up" )] public float jumpCutOff; } void CalculateDerivedJumpValues_GMTK() { // Jump Duration up is set by the [1..10] value from the GMTK app jumpDuration.up = scale( 1, 10, 0.2f, 1.25f, jumpSettingsGMTK.jumpDuration ); // These are the only derived values where the initial gravity is based on Physics2D.gravity - JGB 2023-03-12 jumpGrav.up = Physics2D.gravity.y; // downGravity is a multiplier on the up gravity jumpGrav.down = jumpGrav.up * jumpSettingsGMTK.downGravity; // Calculate jumpDuration.down from G = -2H / (Th * Th), which solves for Th to Th = √(-2H / G) jumpDuration.down = Mathf.Sqrt( -2 * jumpHeight / jumpGrav.down ); // Calculate jumpVel from V = 2H / Th jumpVel.up = 2 * jumpHeight / jumpDuration.up; jumpVel.down = 2 * jumpHeight / jumpDuration.down; // Calculate jumpDist from Th = Xh/Vx which is Vx = Xh/Th jumpDist.up = maxSpeed / jumpDuration.up; jumpDist.down = maxSpeed / jumpDuration.down; // Set double jump jumpsBetweenGrounding = jumpSettingsGMTK.doubleJump ? 2 : 1; jumpSettingsVariableHeightGMTK.useVariableJumpHeight = jumpSettingsGMTK.variableJumpHeight; jumpSettingsVariableHeightGMTK.gravUpMultiplierOnRelease = jumpSettingsGMTK.jumpCutOff; jumpSettingsVariableHeightGMTK.upwardVelocityZeroing = false; jumpSettingsVariableHeightGMTK.minJumpButtonHeldTime = 0.05f; // 100ms is a typical shortest time for a button to be held. jumpSettingsVariableHeight = jumpSettingsVariableHeightGMTK; } } [System.Serializable] public class CSSO_FloatUpDown { public float up, down; } #if UNITY_EDITOR [CustomPropertyDrawer( typeof( CSSO_FloatUpDown ) )] public class CSSO_FloatUpDown_Drawer : PropertyDrawer { static public GUIStyle styleLabelGray = null, styleLabelGrayBold = null; // SerializedProperty m_stat; // Draw the property inside the given rect public override void OnGUI( Rect position, SerializedProperty property, GUIContent label ) { // Init the SerializedProperty fields //if ( m_show == null ) m_show = property.FindPropertyRelative( "show" ); //if ( m_recNum == null ) m_recNum = property.FindPropertyRelative( "recNum" ); //if ( m_playerName == null ) m_playerName = property.FindPropertyRelative( "playerName" ); //if ( m_dateTime == null ) m_dateTime = property.FindPropertyRelative( "dateTime" ); CSSO_FloatUpDown fud = fieldInfo.GetValue( property.serializedObject.targetObject ) as CSSO_FloatUpDown; // Using BeginProperty / EndProperty on the parent property means that // prefab override logic works on the entire property. EditorGUI.BeginProperty( position, label, property ); // Draw label //position = EditorGUI.PrefixLabel( position, GUIUtility.GetControlID( FocusType.Passive ), GUIContent.none );// label ); if ( styleLabelGray == null) { styleLabelGray = new GUIStyle( EditorStyles.label ); styleLabelGray.richText = true; } string colorString = "#606060ff"; // Don't make child fields be indented var indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 1; EditorGUI.LabelField(position, $"<b><color={colorString}>{property.displayName}</color></b>", styleLabelGray ); EditorGUI.indentLevel = 8; EditorGUI.LabelField( position, $"<color={colorString}>up: {fud.up:0.0###}</color>", styleLabelGray ); EditorGUI.indentLevel = 14; EditorGUI.LabelField( position, $"<color={colorString}>down: {fud.down:0.0###}</color>", styleLabelGray ); // Set indent back to what it was EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } } // This was a bad idea because the CSSO doesn't know who the character is. Moved to CharacterJump // [CustomEditor( typeof(Character_Settings_SO) )] // public class CSSO_Editor : Editor { // private Character_Settings_SO csso; // // private void OnEnable() { // csso = (Character_Settings_SO) target; // } // // // private void OnSceneGUI() { // if ( csso == null || csso.jumpLinePoints == null ) return; // // // Handles.matrix = Matrix4x4.Translate(); // Not needed because jump will be shown at origin. // Handles.color = Color.green; // Handles.DrawAAPolyLine(4, csso.jumpLinePoints); // } // } #endif