플레이어의 인풋처리(Player.cs)와 물리처리를 제어(CharController2D.cs)할 스크립트를 작성하였다.
기본 구조는 다음과 같다. Player 스크립트에서 유저의 인풋을 받아 처리할 기능을 전달하면 CharController2D에서 이동, 중력, 충돌과 관련된 처리를 수행한다. ControllerParameterD에서 중력가속도, 점프력, 점프쿨타임 등을 정의하고 ControllerState2D에서 물리적 상태를 정의한다.
Player.cs
using System.Collections; using UnityEngine; using System; using UnityEngine.SceneManagement; public class Player : MonoBehaviour { private bool _isAttacking; private bool _isRunning; private bool _hasNewRecord; private bool _speedChangeStopped; private int _speedChangeCount; private float _attackTimer = 0; private float _attackTime; private float _touchTime; public float AttackFrequency; public double SpeedAcceleration; public float SpeedforSlowingDown; public int StageBGMID; public bool IsDead { get; private set; } public int DeadJumpCount { get; set; } public bool AttackedRight { get; private set; } public bool IsFullyCharged { get; set; } public bool HasTriggeredFullyCharged { get; private set; } public PlayerController2D _controller { get; private set; } public Rigidbody2D _rigidbody2D { get; set; } public Collider2D _collider2D { get; set; } public Animator Animator; public AudioSource StageBGM; public Transform CameraFollow; public Collider2D CeilingBounds; public Transform Foregrounds; public ForegroundScrolling foregroundScrolling; public Collider2D AttackTrigger; public GameObject Player_Body; public GameObject Player_Hair; public void Awake() { _isAttacking = false; _isRunning = false; _speedChangeStopped = false; _speedChangeCount = 1; _attackTimer = 0; _controller = GetComponent<PlayerController2D>(); _collider2D = GetComponent<BoxCollider2D>(); _rigidbody2D = GetComponent<Rigidbody2D>(); IsDead = false; AttackedRight = false; IsFullyCharged = false; HasTriggeredFullyCharged = false; DeadJumpCount = 0; Animator.speed = 1.0f; Player_Hair = GameObject.FindWithTag("Player_Hair"); Player_Hair.SetActive(true); AttackTrigger.enabled = false; } public void Update() { if (!IsDead) { if (LevelManager.Instance.IsGameStarted) { //배경 스크롤 var movement = (SpeedAcceleration * Time.smoothDeltaTime); foregroundScrolling.Scroll(CameraFollow.position, (float)movement); } //인풋 처리 HandleInput(); } else { //사망모션 처리 _controller.AnimateDeathMotion(); } Animator.SetBool("IsGrounded", _controller.State.IsGrounded); Animator.SetBool("JumpingUp", _controller.State.IsJumpingUp); Animator.SetBool("IsDoubleJumping", _controller.State.IsDoubleJumping); Animator.SetBool("IsDead", IsDead); } //사망 처리 public void Kill() { IsDead = true; Player_Hair.SetActive(false); CeilingBounds.enabled = false; GetComponent<BoxCollider2D>().offset = new Vector2(0, 0.33f); GetComponent<BoxCollider2D>().size = new Vector2(0.5f, 0.7f); _controller.CalculateDistanceBetweenRays(); _controller.PlatformMask = (1 << LayerMask.NameToLayer("Platforms")) | (1 << LayerMask.NameToLayer("Enemies")) | (1 << LayerMask.NameToLayer("Obstacles")); } //조작설정에 따른 입력처리 분기 public bool IsInputJump(Vector2 touchPosition) { bool result = false; if (SaveData.Controls.Equals("Vertical")) //조작설정이 'Vertical'이면 화면하단이 Jump, 화면상단이 Attack { result = Camera.main.ScreenToWorldPoint(touchPosition).y < 0.0f; } else //조작설정이 'Horizontal'이면 화면좌측이 Jump, 화면우측이 Attack { result = Camera.main.ScreenToWorldPoint(touchPosition).x < 0.0f; } return result; } //인풋 처리 private void HandleInput() { if (IsDead) return; //터치 인풋 if (Input.touchCount > 0) { Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { if (IsInputJump(touch.position)) { _controller.Jump(); } else { _controller.Attack(); } } return; } //키보드 인풋 if (Input.GetButtonDown("Jump")) { _controller.Jump(); } if (Input.GetButtonDown("Attack")) { _controller.Attack(); } if (Input.GetKeyDown(KeyCode.Escape)) { LevelManager.Instance.OnApplicationPause(!LevelManager.Instance.IsPaused); } } void OnTriggerEnter2D(Collider2D col) { if (col.CompareTag("CLEAR")) { LevelManager.Instance.LevelClear(); } } //노트 판정 및 효과음 재생 void OnTriggerStay2D(Collider2D col) { if (!col.CompareTag("NoteCollider") || IsDead || _controller.InputType == "") return; col.GetComponent<Note>().PlayNote(_controller.InputType); col.enabled = false; _controller.InputType = ""; } }
CharController2D.cs
using UnityEngine; using System.Collections; using System; public class CharController2D : MonoBehaviour { private const float SkinWidth = 0.02f; private const int TotalHorizontalRays = 8; private const int TotalVerticalRays = 4; private static readonly float SlopeLimitTangant = Mathf.Tan(75f * Mathf.Deg2Rad); private static readonly int ANISTS_CHARGE = Animator.StringToHash("Attack Layer.CHARGE"); public LayerMask PlatformMask; public Animator Animator; public Collider2D AttackCol; public ControllerParameters2D DefaultParameters; public ControllerState2D State { get; private set; } public ControllerParameters2D Parameters { get { return _overrideParameters ?? DefaultParameters; } } public GameObject StandingOn { get; private set; } public Vector2 Velocity { get { return _velocity; } } public Vector3 PlatformVelocity { get; private set; } public GameObject PlayerBody; public bool HandleCollisions { get; set; } public bool JumpInputSaved { get; set; } public bool IsFullyCharged { get; private set; } public bool HasTriggeredFullyCharged { get; private set; } public string InputType { get; set; } public float SpeedForSlowingDown; public DateTime JumpInputSavedTime { get; set; } public Vector2 _velocity; private Transform _transform; private Vector3 _localScale; private BoxCollider2D _boxCollider; private ControllerParameters2D _overrideParameters; private float _jumpCooltime; private int _jumpCount; private int _deadJumpCount; private bool _isAttacking; private bool _attackedRight; private float _attackCooltime; private float _attackColCooltime; private float _attackTime; private GameObject _lastStandingOn; private Vector3 _activeLocalPlatformPoint, _activeGlobalPlatformPoint; private Vector3 _raycastTopLeft, _raycastBottomRight, _raycastBottomLeft; private float _verticalDistanceBetweenRays, _horizontalDistanceBetweenRays, _colliderWidth, _colliderHeight; public void Awake() { _boxCollider = GetComponent<BoxCollider2D>(); _transform = transform; _localScale = transform.localScale; _jumpCount = 0; _deadJumpCount = 0; _isAttacking = false; _attackedRight = false; InputType = ""; HandleCollisions = true; IsFullyCharged = false; HasTriggeredFullyCharged = false; State = new ControllerState2D(); CalculateDistanceBetweenRays(); } public void CalculateDistanceBetweenRays() { _colliderWidth = _boxCollider.size.x * Mathf.Abs(transform.localScale.x) - (2 * SkinWidth); _horizontalDistanceBetweenRays = _colliderWidth / (TotalVerticalRays - 1); _colliderHeight = _boxCollider.size.y * Mathf.Abs(transform.localScale.y) - (2 * SkinWidth); _verticalDistanceBetweenRays = _colliderHeight / (TotalHorizontalRays - 1); } public void AddForce(Vector2 force) { _velocity += force; } public void SetForce(Vector2 force) { _velocity = force; } public void SetHorizontalForce(float x) { _velocity.x = x; } public void SetVerticalForce(float y) { _velocity.y = y; } public void SlowDownToStopForward(float deltatime) { _velocity.x -= deltatime; if (_velocity.x < 0) _velocity.x = 0; } public void SlowDownToStopBackward(float deltatime) { _velocity.x += deltatime; if (_velocity.x > 0) _velocity.x = 0; } //점프 public void Jump() { if (_jumpCooltime > 0) return; InputType = "jump"; if (State.IsGrounded) { SetVerticalForce(Parameters.JumpMagnitude); _jumpCount = 1; _jumpCooltime = 0.01f; } else { if (_jumpCount < 2) { State.IsDoubleJumping = true; SetVerticalForce(0); SetVerticalForce(Parameters.JumpMagnitude); _jumpCount++; _jumpCooltime = 0.01f; } else { JumpInputSaved = true; JumpInputSavedTime = DateTime.UtcNow; } } } //공격 public void Attack() { if (!_isAttacking) { AttackCol.enabled = true; _isAttacking = true; _attackCooltime = Parameters.AttackFrequency; _attackColCooltime = 0.3f; if (IsFullyCharged) { ChargeAttack(); } else { if (Time.time - _attackTime > 0.7f) { _attackedRight = false; } else { _attackedRight = !_attackedRight; } if (!_attackedRight) { AttackRight(); } else { AttackLeft(); } } } _attackTime = Time.time; } public void LateUpdate() { //중력 처리 _velocity.y += Parameters.Gravity * Time.deltaTime; //점프 쿨타임 처리 if (_jumpCooltime > 0) _jumpCooltime -= Time.smoothDeltaTime; //공격 쿨타임 처리 if (_isAttacking) { if (_attackCooltime > 0) { _attackCooltime -= Time.smoothDeltaTime; } else { _isAttacking = false; } } //공격 컬라이더 쿨타임 처리 if (_attackColCooltime > 0) { _attackColCooltime -= Time.smoothDeltaTime; } else { AttackCol.enabled = false; } if (IsFullyCharged && !HasTriggeredFullyCharged) FullyCharged(); //이동 처리 Move(Velocity * Time.deltaTime); } private void Move(Vector2 deltaMovement) { var wasGrounded = State.IsCollidingBelow; State.Reset(); if (HandleCollisions) { //Ray 원점 계산 CalculateRayOrigins(); if (deltaMovement.y < 0 && wasGrounded) HandleVerticalSlope(ref deltaMovement); //수평이동 처리 if (Mathf.Abs(deltaMovement.x) > 0.001f) MoveHorizontally(ref deltaMovement); //수직이동 처리 MoveVertically(ref deltaMovement); //위치 조정 CorrectHorizontalPlacement(ref deltaMovement, true); CorrectHorizontalPlacement(ref deltaMovement, false); } if (Time.deltaTime > 0) _velocity = deltaMovement / Time.deltaTime; _transform.Translate(deltaMovement, Space.World); _velocity.x = Mathf.Min(_velocity.x, Parameters.MaxVelocity.x); _velocity.y = Mathf.Min(_velocity.y, Parameters.MaxVelocity.y); if (State.IsMovingUpSlope) _velocity.y = 0; if (StandingOn != null) { _activeGlobalPlatformPoint = transform.position; _activeLocalPlatformPoint = StandingOn.transform.InverseTransformPoint(transform.position); if (_lastStandingOn != StandingOn) { if (_lastStandingOn != null) _lastStandingOn.SendMessage("ControllerExit2D", this, SendMessageOptions.DontRequireReceiver); StandingOn.SendMessage("ControllerEnter2D", this, SendMessageOptions.DontRequireReceiver); _lastStandingOn = StandingOn; } else if (_lastStandingOn != null) StandingOn.SendMessage("ControllerStay2D", this, SendMessageOptions.DontRequireReceiver); } else if (_lastStandingOn != null) { _lastStandingOn.SendMessage("ControllerExit2D", this, SendMessageOptions.DontRequireReceiver); _lastStandingOn = null; } } //이동하는 플랫폼의 경우 플레이어가 플랫폼과 같이 이동하도록 처리 private void HandlePlatforms() { if (StandingOn != null) { var newGlobalPlatformPoint = StandingOn.transform.TransformPoint(_activeLocalPlatformPoint); var moveDistance = newGlobalPlatformPoint - _activeGlobalPlatformPoint; if (moveDistance != Vector3.zero) { transform.Translate(moveDistance, Space.World); } PlatformVelocity = (newGlobalPlatformPoint - _activeGlobalPlatformPoint) / Time.deltaTime; } else { PlatformVelocity = Vector3.zero; } StandingOn = null; } //Ray원점 계산 private void CalculateRayOrigins() { var size = new Vector2(_boxCollider.size.x * Mathf.Abs(_localScale.x), _boxCollider.size.y * Mathf.Abs(_localScale.y)) / 2; var center = new Vector2(_boxCollider.offset.x * _localScale.x, _boxCollider.offset.y * _localScale.y); _raycastTopLeft = _transform.position + new Vector3(center.x - size.x + SkinWidth, center.y + size.y - SkinWidth); _raycastBottomRight = _transform.position + new Vector3(center.x + size.x - SkinWidth, center.y - size.y + SkinWidth); _raycastBottomLeft = _transform.position + new Vector3(center.x - size.x + SkinWidth, center.y - size.y + SkinWidth); } //수평 이동 private void MoveHorizontally(ref Vector2 deltaMovement) { var isGoingRight = deltaMovement.x > 0; var rayDistance = Mathf.Abs(deltaMovement.x) + SkinWidth; var rayDirection = isGoingRight ? Vector2.right : Vector2.left; var rayOrigin = isGoingRight ? _raycastBottomRight : _raycastBottomLeft; for (var i = 0; i < TotalHorizontalRays; i++) { var rayVector = new Vector2(rayOrigin.x, rayOrigin.y + (i * _verticalDistanceBetweenRays)); Debug.DrawRay(rayVector, rayDirection * rayDistance, Color.red); var rayCastHit = Physics2D.Raycast(rayVector, rayDirection, rayDistance, PlatformMask); if (!rayCastHit) continue; if (i == 0 && HandleHorizontalSlope(ref deltaMovement, Vector2.Angle(rayCastHit.normal, Vector2.up), isGoingRight)) break; deltaMovement.x = rayCastHit.point.x - rayVector.x; rayDistance = Mathf.Abs(deltaMovement.x); if (isGoingRight) { deltaMovement.x -= SkinWidth; State.IsCollidingRight = true; } else { deltaMovement.x += SkinWidth; State.IsCollidingLeft = true; } if (rayDistance < SkinWidth + 0.0001f) break; } } //수직 이동 private void MoveVertically(ref Vector2 deltaMovement) { if (deltaMovement.y < 0) { State.IsJumpingUp = false; State.IsDoubleJumping = false; } else { State.IsJumpingUp = true; } var isGoingUp = deltaMovement.y > 0; var rayDistance = Mathf.Abs(deltaMovement.y) + SkinWidth; var rayDirection = isGoingUp ? Vector2.up : -Vector2.up; var rayOrigin = isGoingUp ? _raycastTopLeft : _raycastBottomLeft; rayOrigin.x += deltaMovement.x; var standingOnDistance = float.MaxValue; for (var i = 0; i < TotalVerticalRays; i++) { var rayVector = new Vector2(rayOrigin.x + (i * _horizontalDistanceBetweenRays), rayOrigin.y); Debug.DrawRay(rayVector, rayDirection * rayDistance, Color.red); var raycastHit = Physics2D.Raycast(rayVector, rayDirection, rayDistance, PlatformMask); if (!raycastHit) continue; if (!isGoingUp) { var verticalDistanceToHit = _transform.position.y - raycastHit.point.y; if (verticalDistanceToHit < standingOnDistance) { standingOnDistance = verticalDistanceToHit; StandingOn = raycastHit.collider.gameObject; } } deltaMovement.y = raycastHit.point.y - rayVector.y; rayDistance = Mathf.Abs(deltaMovement.y); if (isGoingUp) { deltaMovement.y -= SkinWidth; State.IsCollidingAbove = true; } else { deltaMovement.y += SkinWidth; State.IsCollidingBelow = true; } if (!isGoingUp && deltaMovement.y > 0.0001f) { State.IsMovingUpSlope = true; } if (rayDistance < SkinWidth + 0.0001f) { break; } } } private void CorrectHorizontalPlacement(ref Vector2 deltaMovement, bool isRight) { var halfWidth = (_boxCollider.size.x * _localScale.x) / 2f; var rayOrigin = isRight ? _raycastBottomRight : _raycastBottomLeft; if (isRight) { rayOrigin.x -= (halfWidth - SkinWidth); } else { rayOrigin.x += (halfWidth - SkinWidth); } var rayDirection = isRight ? Vector2.right : Vector2.left; var offset = 0f; for (var i = 1; i < TotalHorizontalRays - 1; i++) { var rayVector = new Vector2(deltaMovement.x + rayOrigin.x, deltaMovement.y + rayOrigin.y + (i * _verticalDistanceBetweenRays)); Debug.DrawRay(rayVector, rayDirection * halfWidth, isRight ? Color.cyan : Color.magenta); var raycastHit = Physics2D.Raycast(rayVector, rayDirection, halfWidth, PlatformMask); if (!raycastHit) continue; offset = isRight ? ((raycastHit.point.x - _transform.position.x) - halfWidth) : (halfWidth - (_transform.position.x - raycastHit.point.x)); } deltaMovement.x += offset; } //경사 처리 private void HandleVerticalSlope(ref Vector2 deltaMovement) { var center = (_raycastBottomLeft.x + _raycastBottomRight.x) / 2; var direction = -Vector2.up; var slopeDistance = SlopeLimitTangant * (_raycastBottomRight.x - center); var slopeRayVector = new Vector2(center, _raycastBottomLeft.y); Debug.DrawRay(slopeRayVector, direction * slopeDistance, Color.yellow); var raycastHit = Physics2D.Raycast(slopeRayVector, direction, slopeDistance, PlatformMask); if (!raycastHit) return; var isMovingDownSlope = Mathf.Sign(raycastHit.normal.x) == Mathf.Sign(deltaMovement.x); if (!isMovingDownSlope) return; var angle = Vector2.Angle(raycastHit.normal, Vector2.up); if (Mathf.Abs(angle) < 0.0001f) return; State.IsMovingUpSlope = true; State.SlopeAngle = angle; deltaMovement.y = raycastHit.point.y - slopeRayVector.y; } //경사 처리 private bool HandleHorizontalSlope(ref Vector2 deltaMovement, float angle, bool isGoingRight) { if (Mathf.RoundToInt(angle) == 90) return false; if (angle > Parameters.SlopeLimit) { deltaMovement.x = 0; return true; } if (deltaMovement.y > 0.07f) return true; deltaMovement.x += isGoingRight ? -SkinWidth : SkinWidth; deltaMovement.y += Mathf.Abs(Mathf.Tan(angle * Mathf.Deg2Rad) * deltaMovement.x); State.IsMovingUpSlope = true; State.IsCollidingBelow = true; return true; } //오른손 공격 private void AttackRight() { InputType = "attack"; Animator.SetTrigger("RightAttack"); } //왼손 공격 private void AttackLeft() { InputType = "attack"; Animator.SetTrigger("LeftAttack"); } //차징 공격 private void ChargeAttack() { InputType = "attack"; if (Animator.GetCurrentAnimatorStateInfo(1).fullPathHash != ANISTS_CHARGE) { Animator.SetTrigger("ChargeAttack"); } else { AttackRight(); } IsFullyCharged = false; HasTriggeredFullyCharged = false; } //차징 준비완료 private void FullyCharged() { Animator.SetTrigger("FullyCharged"); HasTriggeredFullyCharged = true; } //사망모션 처리 public void AnimateDeathMotion() { if (_deadJumpCount == 0) { SetForce(new Vector2(7, 13)); _deadJumpCount++; } else if (_deadJumpCount == 1) { if (Velocity.x > 0) { SlowDownToStopForward(Time.deltaTime * Parameters.SpeedForSlowingDown); } else if (Velocity.x < 0) { SlowDownToStopBackward(Time.deltaTime * Parameters.SpeedForSlowingDown); } } else if (_deadJumpCount == 2) { if (Velocity.x > 0) { SlowDownToStopForward(Time.deltaTime * Parameters.SpeedForSlowingDown); } else if (Velocity.x < 0) { SlowDownToStopBackward(Time.deltaTime * Parameters.SpeedForSlowingDown); } } else { if (Velocity.x > 0) { SlowDownToStopForward(Time.deltaTime * Parameters.SpeedForSlowingDown); } else if (Velocity.x < 0) { SetHorizontalForce(0); return; } } if (Velocity.x > 0) { PlayerBody.transform.Rotate(0, 0, -7); } else if (Velocity.x < 0) { PlayerBody.transform.Rotate(0, 0, 7); } } public void OnTriggerEnter2D(Collider2D other) { var parameters = other.gameObject.GetComponent<ControllerPhysicsVolume2D>(); if (parameters == null) return; _overrideParameters = parameters.Parameters; } public void OnTriggerExit2D(Collider2D other) { var parameters = other.gameObject.GetComponent<ControllerPhysicsVolume2D>(); if (parameters == null) return; _overrideParameters = null; } }
ControllerParameters2D.cs
using System; using UnityEngine; using System.Collections; [Serializable] public class ControllerParameters2D { public enum JumpBehavior { CanJumpOnGround, // 0 CanJumpAnywhere, // 1 CantJump // 2 } //최대 속도 public Vector2 MaxVelocity = new Vector2(float.MaxValue, float.MaxValue); //최대 경사각 [Range(0, 90)] public float SlopeLimit = 30; //중력가속도 public float Gravity = -25f; //점프력 public float JumpMagnitude = 12; //점프 옵션 public JumpBehavior JumpRestrictions; //점프 쿨타임 public float JumpFrequency = 0.01f; //공격 쿨타임 public float AttackFrequency = 0.01f; //감속 속도 public float SpeedForSlowingDown = 3.0f; }
ControllerState2D.cs
using UnityEngine; using System.Collections; public class ControllerState2D { public bool IsCollidingRight { get; set; } public bool IsCollidingLeft { get; set; } public bool IsCollidingAbove { get; set; } public bool IsCollidingBelow { get; set; } public bool IsMovingDownSlope { get; set; } public bool IsMovingUpSlope { get; set; } public bool IsGrounded { get { return IsCollidingBelow; } } public bool IsJumpingUp { get; set; } public bool IsDoubleJumping { get; set; } public float SlopeAngle { get; set; } public bool HasCollisions { get { return IsCollidingRight || IsCollidingLeft || IsCollidingAbove || IsCollidingBelow; } } public void Reset() { IsJumpingUp = IsDoubleJumping = IsCollidingLeft = IsCollidingRight = IsCollidingAbove = IsCollidingBelow = IsMovingUpSlope = IsMovingDownSlope = false; SlopeAngle = 0; } public override string ToString() { return string.Format("(controller: r:{0} l:{1} a:{2} b:{3} down-slope:{4} up-slope:{5} angle:{6})", IsCollidingRight, IsCollidingLeft, IsCollidingAbove, IsCollidingBelow, IsMovingDownSlope, IsMovingUpSlope, SlopeAngle); } }