using System; using System.Collections.Generic; namespace Otter { /// /// Movement Component that adds platforming movement behavior to an Entity. This is built for /// fixed framerate applications. Make sure you have the Axis, JumpButton, and Collider assigned /// before using it! If you want to use jump through platforms, you'll also need to use the /// JumpThroughCollider, which should be a 1 pixel tall collider at the bottom of your Entity. /// public class PlatformingMovement : Movement { #region Private Fields int jumpBuffer = 0; int ledgeBuffer = 0; List Speeds = new List(); #endregion #region Public Fields /// /// The main input speed of the platforming movement. /// public Speed Speed; /// /// Any extra speed applied (from boosts, dashes, springs, conveyors, etc) /// public Speed ExtraSpeed; /// /// The target speed that the input speed will approach (used for Axis input) /// public Speed TargetSpeed; /// /// The acceleration applied from gravity. /// public float Gravity; /// /// The multiplication factor on the applied gravity. /// public float GravityMultiplier = 1; /// /// The burst of speed applied when jumping. /// public float JumpStrength = 1500; /// /// If the object is currently on the ground (Y+1 overlaps the ground.) /// public bool OnGround = true; /// /// How many jumps are left to use. /// public int JumpsLeft = 0; /// /// The maximum number of jumps each time the object touches the ground. /// public int JumpsMax = 1; /// /// The maximum amount of frames to buffer jump input for the next available moment to jump. /// public int JumpBufferMax = 4; /// /// The maximum number of frames to allow the object to jump after leaving the ground. /// public int LedgeBufferMax = 4; /// /// Determines if the object is capable of jumping. /// public bool JumpEnabled = true; /// /// Determines if a double jump should add jump strength to the current jump, or set the Y /// speed to the jump speed. For example, if true then an object traveling downward will /// jump up at full jump strength, if false it will jump at it's downward speed minus jump /// strenght. /// public bool HardDoubleJump = true; /// /// How much to dampen the Y speed when the object releases the jump button in the air. /// public float JumpDampening = 0.75f; /// /// If the object is in the air because it jumped (instead of falling off a ledge, etc) /// public bool HasJumped = false; /// /// Determines if the object can control its jump height by releasing jump while in the air. /// public bool VariableJumpHeight = true; /// /// Determines if the movement should listen to the Axis for input. /// public bool UseAxis = true; /// /// Determines if the movement should have gravity applied to it. /// public bool ApplyGravity = true; /// /// The Button used for the jumping input. /// public Button JumpButton; /// /// The axis used for movement input. /// public Axis Axis; /// /// An action that is triggered on a successful jump. /// public Action OnJump; public Collider JumpThroughCollider; /// /// The dictionary of acceleration values. /// public Dictionary Acceleration = new Dictionary(); /// /// The default acceleration value to use if none are set. /// public static int DefaultAccleration = 150; /// /// Determines if holding Down while pushing Jump will cause the Entity to drop through jump through platforms instead of jumping. /// public bool DownJumpDrop = true; #endregion #region Public Properties /// /// The current acceleration value. /// public int CurrentAccel { get; protected set; } /// /// If the Collider is currently against a wall on the left. /// public bool AgainstWallLeft { get; private set; } /// /// If the Collider is currently against a wall on the right. /// public bool AgainstWallRight { get; private set; } /// /// If the Collider is currently against a ceiling above it. /// public bool AgainstCeiling { get; private set; } /// /// True for one update after the object has jumped. /// public bool JustJumped { get; private set; } /// /// The list of tags to treat as jump through platforms. /// public List CollisionsJumpThrough { get; private set; } /// /// The total X speed. /// public float SumSpeedX { get { float r = 0; foreach (var s in Speeds) { r += s.X; } return r; } } /// /// The total Y speed. /// public float SumSpeedY { get { float r = 0; foreach (var s in Speeds) { r += s.Y; } return r; } } #endregion #region Constructors /// /// Create a new PlatformingMovement. /// /// The maximum X input speed. /// The maximum Y speed from jumping and gravity. /// The acceleration caused by gravity. public PlatformingMovement(float xSpeedMax, float ySpeedMax, float gravity) { Speed = new Speed(xSpeedMax, ySpeedMax); ExtraSpeed = new Speed(xSpeedMax, ySpeedMax); Speeds.Add(Speed); Speeds.Add(ExtraSpeed); TargetSpeed = new Speed(int.MaxValue, int.MaxValue); Gravity = gravity; JustJumped = false; CollisionsJumpThrough = new List(); Acceleration.Add(AccelType.Ground, DefaultAccleration); Acceleration.Add(AccelType.Air, DefaultAccleration / 4); } #endregion #region Public Methods /// /// Register a collision tag to treat as jump through platforms. /// /// Tags to register. public void AddJumpThrough(params int[] tags) { foreach (var t in tags) { CollisionsJumpThrough.Add(t); } } /// /// Register a collision tag to treat as jump through platforms. /// /// Tags to register. public void AddJumpThrough(params Enum[] tags) { AddJumpThrough(Util.EnumToIntArray(tags)); } /// /// Updates the movement. /// public override void Update() { base.Update(); if (Entity == null) return; JustJumped = false; OnGround = Collider.Collide(Entity.X, Entity.Y + 1, CollisionsSolid) != null; if (!OnGround && JumpThroughCollider != null) { // Check for a jump through platform too OnGround = JumpThroughCollider.Overlap(Entity.X, Entity.Y + 1, CollisionsJumpThrough); if (OnGround) { var platformCollider = JumpThroughCollider.Collide(Entity.X, Entity.Y + 1, CollisionsJumpThrough); OnGround = !JumpThroughCollider.Overlap(Entity.X, Entity.Y, platformCollider); } } AgainstWallLeft = Collider.Collide(Entity.X - 1, Entity.Y, CollisionsSolid) != null; AgainstWallRight = Collider.Collide(Entity.X + 1, Entity.Y, CollisionsSolid) != null; AgainstCeiling = Collider.Collide(Entity.X, Entity.Y - 1, CollisionsSolid) != null; if (OnGround) { HasJumped = false; JumpsLeft = JumpsMax; ledgeBuffer = LedgeBufferMax; } if (!OnGround) { ledgeBuffer = (int)Util.Approach(ledgeBuffer, 0, 1); } jumpBuffer = (int)Util.Approach(jumpBuffer, 0, 1); if (JumpEnabled) { if (JumpButton.Pressed) { if (DownJumpDrop && JumpThroughCollider != null) { // Drop through platforms with Down + Jump var jumping = true; if (Axis.Y > 0.5f) { if (!Collider.Overlap(Entity.X, Entity.Y + 1, CollisionsSolid)) { if (JumpThroughCollider.Overlap(Entity.X, Entity.Y + 1, CollisionsJumpThrough)) { Entity.Y += 1; jumping = false; } } } if (jumping) { jumpBuffer = JumpBufferMax; } } else { jumpBuffer = JumpBufferMax; } } if (JumpButton.Up) { jumpBuffer = 0; } if (jumpBuffer > 0) { if (JumpsLeft > 0) { if (OnJump != null) { OnJump(); } if (HardDoubleJump) { Speed.Y = -JumpStrength; } else { Speed.Y -= JumpStrength; } HasJumped = true; JustJumped = true; JumpsLeft--; jumpBuffer = 0; } } } if (!OnGround && Speed.Y < 0) { if (HasJumped) { if (!JumpButton.Down && VariableJumpHeight) { Speed.Y *= JumpDampening; } } } if (!OnGround && !HasJumped && ledgeBuffer == 0) JumpsLeft = JumpsMax - 1; if (OnGround) { CurrentAccel = Acceleration[AccelType.Ground]; } else { CurrentAccel = Acceleration[AccelType.Air]; } if (UseAxis) { TargetSpeed.X = Axis.X * Speed.MaxX; Speed.X = Util.Approach(Speed.X, TargetSpeed.X, CurrentAccel); if (Speed.X < 0 && AgainstWallLeft) { Speed.X = 0; } if (Speed.X > 0 && AgainstWallRight) { Speed.X = 0; } } if (ApplyGravity) { if (!OnGround) Speed.Y += Gravity * GravityMultiplier; } MoveXY((int)SumSpeedX, (int)SumSpeedY, Collider); } public override void MoveCollideX(Collider collider) { base.MoveCollideX(collider); Speed.X = 0; ExtraSpeed.X = 0; } public override void MoveCollideY(Collider collider) { base.MoveCollideY(collider); if (SumSpeedY > 0) OnGround = true; Speed.Y = 0; ExtraSpeed.Y = 0; } public override void MoveY(int speed, Collider collider = null) { MoveBufferY += speed; while (Math.Abs(MoveBufferY) >= SpeedScale) { int move = Math.Sign(MoveBufferY); if (collider != null) { bool freeToMove = true; Collider c = null; if (move > 0) { c = collider.Collide(Entity.X, Entity.Y + move, CollisionsSolid); if (c == null) { if (JumpThroughCollider != null) { var hasPlatformBelow = JumpThroughCollider.Overlap(Entity.X, Entity.Y + move, CollisionsJumpThrough); if (hasPlatformBelow) { hasPlatformBelow = false; // Check to see if only bottom 1 pixel is overlapping jump throughs var platform = JumpThroughCollider.Collide(Entity.X, Entity.Y + move, CollisionsJumpThrough); if (!JumpThroughCollider.Overlap(Entity.X, Entity.Y, platform)) { // This makes sense just trust me ok hasPlatformBelow = true; } } if (hasPlatformBelow) { freeToMove = false; c = JumpThroughCollider.Collide(Entity.X, Entity.Y + move, CollisionsJumpThrough); } } } else { freeToMove = false; } } else { c = collider.Collide(Entity.X, Entity.Y + move, CollisionsSolid); if (c != null) { freeToMove = false; } } if (freeToMove) { Entity.Y += move; MoveBufferY = (int)Util.Approach(MoveBufferY, 0, SpeedScale); } else { MoveBufferY = 0; MoveCollideY(c); } } if (collider == null || CollisionsSolid.Count == 0) { Entity.Y += move; MoveBufferY = (int)Util.Approach(MoveBufferY, 0, SpeedScale); } } } #endregion } #region Enums /// /// The different acceleration types. /// public enum AccelType { Ground, Air } #endregion }