using System;
using Spine;
using Spine.Unity;
using UnityEngine;
using UnityEngine.InputSystem;
namespace CharacterController
{
[RequireComponent(typeof(Rigidbody2D), typeof(PlayerInput))]
public class PlatformingController : MonoBehaviour
{
[SerializeField] private PlayerStats stats;
[SerializeField, SpineAnimation] private string run;
[SerializeField, SpineAnimation] private string idle;
[SerializeField, SpineAnimation] private string faceLeft;
[SerializeField, SpineAnimation] private string faceRight;
[Header("Hack to fix a bug with new RigidBody2D.Slide method")]
[SerializeField] private Vector2 ceilingCheckOffset;
[SerializeField] private float ceilingCheckSize;
[SerializeField] private LayerMask checkForCeiling;
private Rigidbody2D.SlideMovement _slideSettings;
private Rigidbody2D _rbody;
private PlayerInput _input;
private SkeletonAnimation _anim;
private Vector2 _currentVelocity;
private float _currentVelocityX;
private Vector2 _moveInput;
private bool _grounded;
private float _lastGroundedTime;
private float _jumpInputTime;
private bool _isJumping;
private float _jumpStartTime;
private Collider2D[] _wallJumpOverlap = new Collider2D[2];
private ContactFilter2D _wallJumpContactFilter;
private float _wallSide;
private bool _onWall;
private float _lastOnWallTime;
private bool _wallJumping;
private Vector2 _surfaceAnchor;
private bool _touchingCeiling;
private Collider2D[] _ceilingOverlap = new Collider2D[2];
private ContactFilter2D _ceilingContactFilter;
private void Awake()
{
_anim = GetComponentInChildren<SkeletonAnimation>();
_surfaceAnchor = Vector2.down / 4f;
_wallJumpContactFilter = new ContactFilter2D
{
layerMask = stats.WallCheckLayers,
useLayerMask = true
};
_ceilingContactFilter = new ContactFilter2D
{
layerMask = checkForCeiling,
useLayerMask = true
};
_rbody = GetComponent<Rigidbody2D>();
_slideSettings = new Rigidbody2D.SlideMovement
{
gravity = Vector2.zero,
surfaceAnchor = _surfaceAnchor
// using the calculated gravity:
// this makes slopes work properly
// jumping does not work, the player sticks to the floor
//gravity = new Vector2(0, stats.Gravity),
// multiplying it by Time.fixedDeltaTime
// this makes jumping work properly
// slopes do not work, the player bounces down them
//gravity = new Vector2(0, stats.Gravity * Time.fixedDeltaTime),
};
}
private void OnEnable()
{
_input = GetComponent<PlayerInput>();
_input.actions["Move"].performed += Move;
_input.actions["Move"].canceled += Move;
_input.actions["Jump"].performed += Jump;
_input.actions["Jump"].canceled += Jump;
}
private void OnDisable()
{
_input.actions["Move"].performed -= Move;
_input.actions["Move"].canceled -= Move;
_input.actions["Jump"].performed -= Jump;
_input.actions["Jump"].canceled -= Jump;
}
private void Move(InputAction.CallbackContext ctx)
{
_moveInput = ctx.ReadValue<Vector2>();
}
private void Jump(InputAction.CallbackContext ctx)
{
if (ctx.ReadValueAsButton() && (_grounded || Time.time - _lastGroundedTime < stats.InputBuffer))
StartJump();
else if (ctx.ReadValueAsButton() && !_grounded)
_jumpInputTime = Time.time;
else if (!ctx.ReadValueAsButton() && _isJumping)
EndJump();
if (!_grounded && ctx.ReadValueAsButton() && (_onWall || Time.time - _lastOnWallTime < stats.WallStickTime))
WallJump();
}
private async void WallJump()
{
_lastOnWallTime = -1;
if (Mathf.Approximately(Mathf.Sign(_moveInput.x), _wallSide))
_currentVelocity = stats.WallJumpForce;
else
_currentVelocity = stats.WallLeapForce;
_wallJumping = true;
await Awaitable.WaitForSecondsAsync(stats.WallJumpPlayerInputTime);
_wallJumping = false;
}
private void StartJump()
{
_grounded = false;
_isJumping = true;
_jumpStartTime = Time.time;
_currentVelocity.y = stats.JumpForce;
}
private void EndJump()
{
_isJumping = false;
if (Time.time - _jumpStartTime < stats.TimeForMaxJump)
{
// Calculate jump force for min jump height
float minJumpForce = Mathf.Sqrt(2 * Mathf.Abs(stats.Gravity) * stats.JumpHeightMin);
_currentVelocity.y = Mathf.Min(_currentVelocity.y, minJumpForce);
}
}
private void FixedUpdate()
{
_touchingCeiling = Physics2D.OverlapCircle(_rbody.position + ceilingCheckOffset, ceilingCheckSize,
_ceilingContactFilter, _ceilingOverlap) > 0;
if (Time.time - _jumpInputTime < stats.InputBuffer && _grounded)
StartJump();
_currentVelocity.x = GetHorizontalVelocity();
if (!_grounded || _isJumping)
_currentVelocity.y += stats.Gravity * GetGravityModifier() * Time.fixedDeltaTime;
else if (_currentVelocity.y < 0)
_currentVelocity.y = 0;
if (_isJumping && !_grounded)
if (Time.time - _jumpStartTime >= stats.TimeForMaxJump)
_isJumping = false; // Stop jumping after reaching max jump time
if (_currentVelocity.y > 0)
_slideSettings.surfaceAnchor = Vector2.zero;
else
_slideSettings.surfaceAnchor = _surfaceAnchor;
if (!_touchingCeiling)
{
var results = _rbody.Slide(_currentVelocity, Time.fixedDeltaTime, _slideSettings);
_grounded = results.surfaceHit.transform != null;
}
else
{
_currentVelocity.y = -1f;
_isJumping = false;
_rbody.MovePosition(_rbody.position + _currentVelocity * Time.fixedDeltaTime);
}
if (_grounded)
{
_jumpInputTime = -1;
_lastGroundedTime = Time.time;
}
_onWall = Physics2D.OverlapCircle(_rbody.position + stats.WallJumpCheckOffset * Mathf.Sign(_currentVelocity.x), stats.WallJumpCheckSize,
_wallJumpContactFilter, _wallJumpOverlap) > 0;
if (_onWall)
{
_lastOnWallTime = Time.time;
_wallSide = Mathf.Sign(_currentVelocity.x);
}
if (_onWall && MovingAndInputMatch() && _currentVelocity.y < 0)
_currentVelocity.y = Mathf.Max(_currentVelocity.y, -stats.WallSlideSpeed);
if (Mathf.Abs(_currentVelocity.x) > 0.1f && _anim.state.GetCurrent(0)?.Animation?.Name != run)
_anim.state.SetAnimation(0, run, true);
else if (Mathf.Abs(_currentVelocity.x) < 0.1f && _anim.state.GetCurrent(0)?.Animation?.Name != idle)
_anim.state.SetAnimation(0, idle, true);
if (Mathf.Sign(_currentVelocity.x)>0) _anim.state.SetAnimation(1, faceRight, true);
else if (Mathf.Sign(_currentVelocity.x)<0) _anim.state.SetAnimation(1, faceLeft, true);
}
private float GetGravityModifier()
{
if (Mathf.Abs(_currentVelocity.y) < stats.JumpPeakThreshold)
return stats.JumpPeakGravity;
if (_currentVelocity.y < 0)
return stats.FallGravity;
return 1;
}
private bool MovingAndInputMatch()
{
return Mathf.Approximately(Mathf.Sign(_currentVelocity.x), Mathf.Sign(_moveInput.x));
}
private float GetHorizontalVelocity()
{
if (_wallJumping)
return _currentVelocity.x;
var targetSpeed = _moveInput.x * stats.TargetRunSpeed;
return Mathf.SmoothDamp(_currentVelocity.x, targetSpeed, ref _currentVelocityX,
GetAcceleration(targetSpeed));
}
private float GetAcceleration(float targetSpeed)
{
// we're trying to turn
if (!Mathf.Approximately(Mathf.Sign(targetSpeed), Mathf.Sign(_currentVelocity.x)))
return _grounded ? stats.TurnSpeed : stats.TurnSpeedAir;
if (Mathf.Abs(targetSpeed) > Mathf.Epsilon) // we're trying to move
{
if (_grounded)
return stats.Acceleration;
if (Mathf.Abs(_currentVelocity.y) < stats.JumpPeakThreshold) // we're at the peak of the jump
return stats.PeakAcceleration;
return stats.AirAcceleration;
}
else // we're trying to stop
{
if (_grounded)
return stats.Deceleration;
if (Mathf.Abs(_currentVelocity.y) < stats.JumpPeakThreshold) // we're at the peak of the jump
return stats.PeakDeceleration;
return stats.AirDeceleration;
}
}
private void OnValidate()
{
_rbody = GetComponent<Rigidbody2D>();
stats.CalculateForces();
}
private void OnDrawGizmos()
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(_rbody.position + stats.WallJumpCheckOffset, stats.WallJumpCheckSize);
Gizmos.DrawWireSphere(_rbody.position + ceilingCheckOffset, ceilingCheckSize);
}
}
}