public class JumpCountHandler
{
public JumpCountHandler(int maxJumpCount)
{
SetJumpCount(maxJumpCount);
}
public int JumpCount { get; private set; }
public void DecreaseJumpCount()
{
JumpCount--;
}
public void SetJumpCount(int count)
{
JumpCount = count;
}
}
PlayerJumpState에서 Jump 상태 변경을 관리한다.
using UnityEngine;
public class PlayerJumpState : PlayerAirState
{
private void Jump()
{
if (jumpCountSetter.JumpCount > 0)
{
animationController.ReStartIfAnimationIsPlaying(animationsData.JumpParameterHash);
jumpCountSetter.DecreaseJumpCount();
Vector3 velocity = rigid.velocity;
velocity.y = playerController.StatHandler.Data.JumpForce;
rigid.velocity = velocity;
}
}
}
마지막으로 자연스러운 더블점프 동작을 위해 현재 점프 상태이고 또 점프가 들어오면 처음부터 애니메이션을 다시 재생하도록 했다.
using UnityEngine;
public class AnimationController : MonoBehaviour
{
[field: SerializeField] public PlayerAnimationsData AnimationData { get; private set; }
private Animator _animator;
public void ReStartIfAnimationIsPlaying(int animationParameterHash, int layerIndex = 0)
{
if (_animator.GetCurrentAnimatorStateInfo(layerIndex).shortNameHash.Equals(animationParameterHash))
_animator.Play(animationParameterHash);
}
}
플레이어 구르기
구르기에서는 무적, 쿨타임, 구르기 시 움직임 제한 기능이 필요했다.
먼저 PlayerRollState에서 구르기 애니메이션과 구르기 끝났는지를 체크하고
public class PlayerRollState : PlayerStateBase
{
private void CheckRollingEnded()
{
if (animationController.CheckAnimationEnded(animationsData.RollParameterHash))
{
stateMachine.RollDataHandler.SetIsRolling(false);
if (stateMachine.IsGrounded)
stateMachine.ChangeState(stateMachine.MovementState);
else
stateMachine.ChangeState(stateMachine.FallState);
}
}
}
RollDataHandler에서 구르기 수행 시 무적과 쿨타임을 계산한다.
using UnityEngine;
public class RollDataHandler
{
public bool CanRoll { get; private set; }
public bool IsInvincible { get; private set; }
public bool IsRolling { get; private set; }
private float _rollingCoolTime;
private float _currentRollingElapsedTime;
private float _invincibleTime;
public RollDataHandler(float rollingCoolTime, float invincibleTime)
{
SetRollingCoolTime(rollingCoolTime);
_currentRollingElapsedTime = rollingCoolTime;
_invincibleTime = invincibleTime;
CanRoll = true;
}
public void SetRollingCoolTime(float rollingCoolTime)
{
_rollingCoolTime = rollingCoolTime;
}
public void ResetCurrentRollingElapsedTime()
{
CanRoll = false;
_currentRollingElapsedTime = 0f;
IsInvincible = true;
}
public void CalculateCoolTime()
{
if (_currentRollingElapsedTime >= _rollingCoolTime)
{
CanRoll = true;
return;
}
_currentRollingElapsedTime += Time.deltaTime;
_currentRollingElapsedTime =
_currentRollingElapsedTime > _rollingCoolTime ?
_rollingCoolTime : _currentRollingElapsedTime;
CalculateInvincible();
}
public void SetIsRolling(bool isRolling)
{
IsRolling = isRolling;
}
private void CalculateInvincible()
{
if (_currentRollingElapsedTime < _invincibleTime)
return;
IsInvincible = false;
}
}
오늘의 이슈 / 내일 할 것
더블 점프 구현을 위한 JumpCount 이슈
문제점 : JumpCount를 IsGronded일 때 JumpCountMax로 초기화를 해줬으나 Debug.Log를 찍어보면 초기화가 안 되는 이슈, PlayerJumpState에서의 문제인 줄 알고 PlayerAirState에 옮겼으나 결과는 같았음 원인 : 호출 스택을 살펴보니 PlayerFallState 부모의 PlayerAirState에서만 초기화되는 경우였음 (PlayerJumpState JumpCount != PlayerFallState JumpCount) 해결 : 공통적으로 참조하는 StateMachine에서 JumpCount를 관리하기로 함 ** 또한 IsGrounded도 같은 이슈여서 StateMachine에서 관리하는 것으로 처리
플레이어가 날라가는 이슈
문제점 : 플레이어 날아가는 이슈
원인 : 점프 값에도 Speed가 곱해져 날라가던 것이었다.
해결 : 이동에만 Speed를 곱해주니 해결
내일 할 것 : 구르기 시 이동 입력 받기, 빌드
오늘의 회고
오늘은 캐릭터 더블 점프와 구르기를 구현했다. 더블 점프에 상태머신이 겹쳐서 참조 변수를 각각 따로 참조하게 되는 실수를 저질렀는데 이후에 호출 스택을 보고 처리하길 잘했던 것 같다. 그리고 구르기 시 Input을 받지 않는 작업은 InputSystem의 Interaction Hold 부분을 좀 더 공부해보고 적용해봐야겠다. 내일도 파이팅!
StateMachine에서 현재 IState인 _currentState를 ChangeState(IState newState)로 바꿔준다.
public class StateMachine
{
private IState _currentState;
public PlayerController PlayerController { get; private set; }
public PlayerMoveState MovementState { get; private set; }
public PlayerJumpState JumpState { get; private set; }
public StateMachine(PlayerController playerController)
{
PlayerController = playerController;
MovementState = new PlayerMoveState(this);
}
public void Init()
{
ChangeState(MovementState);
}
public void ChangeState(IState newState)
{
_currentState?.Exit();
_currentState = newState;
_currentState?.Enter();
}
public void Update()
{
_currentState.UpdateState();
}
public void PhysicsUpdate()
{
_currentState.PhysicsUpdateState();
}
}
각 State는 애니메이션을 동작시키는데 예를 들어 PlayerMoveState가 있으면 Enter에서 플레이어의 애니메이션 bool 파라미터를 true로 만들고 Exit에서 false로 만들게끔 했다.
public class PlayerMoveState : PlayerStateBase
{
public PlayerMoveState(StateMachine stateMachine) : base(stateMachine)
{
}
public override void Enter()
{
playerController.Animator.SetBool(animationsData.MoveParameterHash, true);
}
public override void UpdateState()
{
base.UpdateState();
playerController.Animator.SetFloat(animationsData.SpeedRatioParameterHash, speedRatio);
}
public override void Exit()
{
playerController.Animator.SetBool(animationsData.MoveParameterHash, false);
}
public override void OnDead()
{
base.OnDead();
}
}
애니메이션은 블렌드 트리를 사용하였다.
오늘의 이슈 / 내일 할 것
이슈 : Mixamo에서 가져온 Animation에서 humanoid로 변경 후 Import Message Warning이 뜸
시도한 것 : 'Take001'이라는 애니메이션이 문제여서 Animation 탭의 Clips에서 제거함, 그러나 왜인지 Source Take 부분에는 남아 있고 Import Message Warning도 존재함
내일 할 것 : 캐릭터 점프, 슬라이딩
오늘의 회고
오늘은 캐릭터 점프와 슬라이딩 쪽을 구현할 예정이었으나 캐릭터 상태머신과 애니메이션이 더 필요하다고 하여 상태머신 쪽을 먼저 구현하게 되었다. Input을 받아와 이동에 적용하는데 구조를 변경하는 작업이 동반되어서 시간이 좀 걸렸던 것 같다. 리팩토링은 할 땐 귀찮은데 하고 나면 괜히 뿌듯한 작업인 것 같다. 내일도 파이팅!
해결 방안 : 이해하기 쉬운 Euler각을 이용하여 각도에 제한을 둠, 여기서 Vector3.SignedAngle()을 이용하여 좌/우측 둘 다 고려, Quaternion.RotateTowards는 더 작은 각도인 쪽으로 도는데 이를 이용한 트릭으로 Circle_Angle에(-90도, 90도) 방향으로 1을 더해주거나 빼줌으로써 정면만 바라보게 했음
오늘은 프로토 타입을 만들기 위해 UI 스테이지 컨셉을 기획하고 플레이어 데이터를 구상해보았다. 기획한 것들을 UI에 집어넣다보니 조금 UI가 복잡해지는 감이 있지만 일단은 넣고 싶은 것들을 다 넣어놓고 프로토타입에서 테스트를 한 뒤 추가할 것은 추가하고 뺄 건 빼는 방향으로 해볼 생각이다.
내일은 플레이어 이동을 구현해서 팀원이 만든 맵에 테스트를 해보고 시간이 남으면 조이스틱에 연동까지 할 수 있도록 해야겠다. 내일도 파이팅!
오늘은 최종 프로젝트 기획을 본격적으로 시작했다. 우리 조는 기획이 개발 전반적으로 영향을 끼치는 것을 알고 있기 때문에 기획에 시간을 충분히 쏟고 시작하기로 했다. 먼저 우리는 출시가 목표이기 때문에 레퍼런스 게임을 잡고 우리만의 컨셉(시스템)을 넣어서 새로운 로그라이크 게임을 만들기로 했다. 그리고 필수적인 기능들이 뭔지 미리 생각해보고 클래스를 만들어보기도 했다.
내일은 오늘 한 것에 이어서 기획을 더 해볼건데 에셋과 더불어 스테이지 컨셉을 정할 생각이다. 내일도 파이팅!
각자의 미니게임을 합치는 부분에서 좀 오래 걸렸고 특히나 합칠 때 버그를 잡는 부분에서 시간이 많이 걸렸던 것 같다.
특히나 마지막에는 프로젝트 기한을 맞추다보니 확장성이 부족한 코드들을 작성해서 불편한 기분을 느꼈다.
오늘의 회고
오늘은 최종 프로젝트 전 팀 프로젝트 발표가 있었다. 오전 중에는 발표 영상 만드느랴 버그 잡느랴 정신없이 지나갔고 오후에는 팀 발표물들을 보면서 다른 팀에게서도 배울 점이 많다고 생각이 들었다. 최종 프로젝트에는 기존에 프로젝트를 하면서 아쉬운 점들을 모두 보완하도록 노력해야겠다. 다음 주 최종 프로젝트 화이팅!
4. 이를 기다리는데 다른 로직이 방해받지 않고 기다렸으면 좋겠다고 생각하고 정보들을 찾아보기로 했다.
찾아보니 Unite Copenhagen에서 진행한 자료가 있어서 참고해봤다.
대충 이런 차이점이 있는데 내 경우에서는 MonoBehaviour를 상속받지 않으므로 Async를 사용하기로 했다.
다음은 Async를 적용하여 MonoBehaviour를 상속받는 Obstacle 클래스에서 OnCollisionEnter 이벤트가 호출되면 조건을 체크하여 ObstacleCollision의 ApplyCollision을 호출하고 물리적 힘을 준 뒤 캐릭터가 이미 충돌 상태인지 확인하고 처리해주는 것을 구현한 로직이다.
public class Obstacle : MonoBehaviour, IPoolingObject<Obstacle>
{
private void OnCollisionEnter(Collision collision)
{
if (collision.collider.TryGetComponent(out CharacterController controller))
{
_obstacleCollision.ApplyCollision(controller, collision.rigidbody, -collision.GetContact(0).normal, mass, waitMilliseconds);
}
}
}
public class ObstacleCollision
{
public async void ApplyCollision(CharacterController controller, Rigidbody oppositeRigid, Vector3 towardTargetVec, float mass, int waitMilliseconds)
{
oppositeRigid.AddForce(towardTargetVec * mass, ForceMode.Impulse);
controller.SetImmovable();
await Task.Delay(waitMilliseconds);
controller.SetMovable();
}
}
public class CharacterController : MonoBehaviour
{
public void SetMovable()
{
if (_collisionStack > 0)
{
_collisionStack--;
if (_collisionStack == 0)
_canMove = true;
}
}
public void SetImmovable()
{
_collisionStack++;
if (_collisionStack == 1)
_canMove = false;
}
}
오늘의 회고
오늘은 코루틴과 비동기의 차이점에 대해서 배웠다. 코루틴은 유니티에서 MonoBehaviour를 상속 받을 때 싱글 쓰레드로 동작하는 것이고 Async는 멀티 쓰레드로 웹에 요청을 보내거나 파일 입출력을 할 때 많이 쓰인다는 것을 배우게 되었다.
내일은 게임의 결과를 보여주는 창과 SFX 관리를 할 생각이다. 내일도 열심히 하자 파이팅!
MonoBehviour을 상속 받고 충돌을 체크하며 float의 mass 값을 가진 Obstacle이 있고
충돌체에 물리적인 힘을 가하는 ObstacleCollision이 있다.
ObstacleCollision은 Obstacle 클래스의 OnCollisionEnter로부터 필요한 정보(Rigidbody, normal, mass) 값만 받아와서 충돌을 처리한다. -> 역할 구분이 확실해진다.
public class Obstacle : MonoBehaviour
{
[Header("Collision")]
[SerializeField] private float mass;
private ObstacleCollision _obstacleCollision;
private void Awake()
{
_obstacleCollision = new ObstacleCollision();
}
private void OnCollisionEnter(Collision collision)
{
if (collision.rigidbody != null)
{
_obstacleCollision.ApplyCollision(collision.rigidbody, -collision.GetContact(0).normal, mass);
}
}
}
public class ObstacleCollision
{
public void ApplyCollision(Rigidbody oppositeRigid, Vector3 towardTargetVec, float mass)
{
oppositeRigid.AddForce(towardTargetVec * mass, ForceMode.Impulse);
}
}
오늘의 회고
오늘은 Solid 원칙 중 단일 책임 원칙을 보고 클래스가 하는 역할에 따라 구분해보았다. Obstacle의 클래스의 다른 부분이 얼마나 수정되든 ObstacleCollision의 클래스는 수정할 필요가 없고 그 반대도 동일하다. 이래서 단일 책임 원칙을 사용하나 싶다.
내일은 오브젝트 풀링을 이용해 오브젝트들을 세 곳에서 생성하고 플레이어가 경사진 다리를 올라가게 할 것이다. 내일도 열심히 해보자 파이팅!