코루틴과 Dotween을 이용하여 Material 색을 변하게 하는 코드인데 DamagedAction에 의해 호출이 되어 이벤트 형식으로 작동이 된다.
public virtual void OnDamaged()
{
if (_blinkCoroutine == null)
{
_blinkCoroutine = Blink();
}
else
{
StopCoroutine(_blinkCoroutine);
myMaterial.color = Color.white;
}
StartCoroutine(_blinkCoroutine);
}
private IEnumerator Blink()
{
myMaterial.DOColor(Color.red, blinkDuration);
yield return new WaitForSeconds(blinkDuration);
myMaterial.DOColor(Color.white, blinkDuration);
yield return new WaitForSeconds(blinkDuration);
}
보스 기획
군사기지 보스에 대한 기획을 진행했다.
오늘의 이슈 / 내일 할 것
오늘 이슈
내일 할 것
보스 패턴 구현하기
오늘의 회고
오늘은 피격 효과와 보스 패턴 기획을 진행했다. 피격 효과는 Dotween을 써서 구현했는데 내가 while문을 써서 구현하는 것보다 훨씬 간단하게 사용할 수 있어서 애용해야지라는 생각이 들었다. 보스 패턴은 3단계인 군사 기지에 대한 보스인데 현재 생각은 기존의 EnemyStateMachine을 활용하여 분리하고 패턴만 따로 클래스화 시키도록 구상중이다.
공격 버튼을 눌러도 선 입력이 되고 Combo 파라미터가 이미 올라가 있는 버그가 있었습니다.
원인 :
1. 코드에서 이미 normalizeTime을 불러와서 1이 이상일 때를 체크하는데 ComboAttack 사이에 HasExitTime이 켜져 있었고 이것이 애니메이션 상태가 끝날 때까지 실행하게 되어 문제를 발생시켰다.
2. 가끔 Attack State를 탈출할 때가 있었다.
해결 :
1. ComboAttack 사이에 HasExitTime을 없애서 코드에서 제어가 가능하도록 했다.
2. 애니메이터에서 @Attack SubState에서 접근할 수 있는 트랜지션을 추가하여 탈출해도 다음 콤보를 실행할 수 있도록 했다.
오늘 이슈
- 피격 효과가 필요
- 죽었을 때 적이 사라질 필요성이 있음
오늘의 회고
오늘은 지금까지 한 부분의 중간 발표를 했다. 와중에 지난 주에 못 풀었던 연속 공격의 버그를 해결했는데 코드와 애니메이터 두 곳에서 로직을 제어하려고 해서 문제가 일어났던 것이었다. 별 거 아닌 실수를 해서 허탈하기도 하고 일단 해결해서 다행이라는 생각이 들었다. 발표 때문에 정신이 없었지만 내일도 열심히 해보자 파이팅!
공격 상태가 아님에도 Attack을 호출하여 적 끼리 OnTriggerEnter가 불려서 NullReferenceException이 발생
-> Raycast로 바꾼 뒤 Layer를 체크하여 타겟일 때만 검출하도록 해서 해결!
오늘 이슈
연속 공격 애니메이션과 AttackCombo에 문제 있음
금요일 날 할 것
예비군 갔다와서 ComboMod 적용
오늘의 회고
오늘은 지난 주에 작업한 데미지 계산을 OnTriggerEnter에서 Raycast검출로 바꿨다. 총알 같은 발사체면 Collider 검출을 하고 즉발적인 검출이면 Raycast를 사용하면 된다고 이해하였다. 내일부터 동원 예비군 훈련이 있는데 팀원들한테 조금 미안한 감정이 든다. 그만큼 다녀와서 열심히 하고 뒤쳐지지 않도록 해야겠다. 금요일부터 다시 파이팅!
이번 주에 만든 적 패트롤, 추적, 공격과 플레이어 연속 공격을 한 씬에 넣어서 테스트 했다.
데미지 계산
Weapon이라는 클래스를 만들어 무기에 컴포넌트로 등록시키고 OnTriggerEnter로 체크하여 데미지를 계산하고 입히게끔 했다.
attacker와 target으로 나누어 정보를 전달하였고 무적과 회피 그리고 크리티컬을 체크한 뒤 데미지를 계산하였다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour, IBattle
{
private EntitySO _weaponSO;
private const int PERCENT_EXCLUDE_MAX_VALUE = 101;
private void OnTriggerEnter(Collider other)
{
HealthSO targetSO = null;
IDamageable damageable = null;
if (other.CompareTag("Player"))
{
PlayerStatHandler statHandler = other.GetComponentInParent<PlayerController>().StatHandler;
targetSO = statHandler.Data;
damageable = statHandler;
if (_weaponSO == null)
{
_weaponSO = GetComponentInParent<EnemyController>().StatHandler.Data;
}
Attack(_weaponSO, targetSO, damageable);
}
else if (other.CompareTag("Enemy"))
{
EnemyStatHandler statHandler = other.GetComponentInParent<EnemyController>().StatHandler;
targetSO = statHandler.Data;
damageable = statHandler;
if (_weaponSO == null)
{
_weaponSO = GetComponentInParent<PlayerController>().StatHandler.Data;
}
Attack(_weaponSO, targetSO, damageable);
}
}
public void Attack(EntitySO attacker, HealthSO target, IDamageable targetDamageable)
{
if (target.IsInvincible)
return;
int randomEvasionProbability = UnityEngine.Random.Range(0, PERCENT_EXCLUDE_MAX_VALUE);
if (randomEvasionProbability < target.EvasionProbability)
return;
float attackDamage = attacker.Attack;
int randomCriticalProbability = UnityEngine.Random.Range(0, PERCENT_EXCLUDE_MAX_VALUE);
if (randomCriticalProbability < attacker.CriticalProbability)
attackDamage += attacker.Attack * attacker.CriticalMod;
targetDamageable.Damaged(attackDamage);
}
}
오늘의 이슈 / 내일 할 것
어제 이슈 : 패트롤 비동기
1. 공격 중에 점프, 구르기 이동 막기 (이동은 처음 공격 시 방향 전환만 적용)
StateMachine의 CurrentState가 AttackState면 return하도록 했다.
2. 맵 정보를 받아와서 적이 플레이어 추적하고 공격할 수 있도록 하기
Area에서 정보를 받아서 플레이어가 OnTriggerEnter 됐을 때 추적하고 OnTriggerExit 됐을 때 패트롤 상태가 되도록 했다.
오늘 이슈
1. 공격 상태가 아님에도 Attack을 호출하여 적 끼리 OnTriggerEnter가 불려서 NullReferenceException이 발생
내일 할 것
공격 상태에만 Attack을 호출할 수 있도록 하기
오늘의 회고
오늘은 이번 주에 작업 했던 것들을 합쳐서 점검하고 무기로부터 데미지를 전달할 수 있도록 했다. 이후에 아이템 작업 하시는 분의 StatHandler의 작업이 끝나면 무기의 스텟을 캐릭터 스텟에 추가하고 총 합을 Attack의 매개변수로 넘길 수 있도록 할 것이다. 무기도 장착해주고 보여지는 것들이 하나씩 늘어가니까 뭔가 진행되고 있는 것 같아서 기분이 좋다. 이 기분대로 열심히 더 해보자 파이팅!
플레이어의 AttackState에서의 Update문인데 AnimationController의 AttackType의 이름으로 현재 애니메이션의 Clip 이름과 비교하여 같은 상황에서 Attack 입력을 감지하면 AttackCombo 애니메이션 파라미터가 증가하게 만들어 다음 연속 동작으로 이어갈 수 있게끔 구현하였다.
또한 animator.GetCurrentClipInfo()[0]을 가져왔는데 왜 0번을 가져왔냐면 -> ClipInfo가 배열 타입으로 들어있는데 transition할 때는 2개고 안 할때는 1개라서 기본적으로 0번을 가져와 비교한 것이다.
public override void UpdateState()
{
base.UpdateState();
if (animationController.CheckCurrentClipEnded(animationController.AttackType))
{
stateMachine.ChangeState(stateMachine.MoveState);
}
else
{
if (animationController.IsAttackInputted && animationController.CheckCurrentClipEqual(animationController.AttackType))
{
animationController.SetAttackInputted(false);
animationController.IncreaseAttackCombo();
animationController.SetNextAttackType();
animationController.PlayAnimation(animationsData.AttackComboHash, animationController.AttackCombo);
}
}
}
public bool CheckCurrentClipEnded(AttackType attackType, int layerIndex = 0)
{
var clipInfo = animator.GetCurrentAnimatorClipInfo(layerIndex);
if (clipInfo[0].clip.name.Equals(attackType.ToString()))
{
var stateInfo = animator.GetCurrentAnimatorStateInfo(layerIndex);
if (stateInfo.normalizedTime >= animationNormalizeEndedTime)
return true;
}
return false;
}
public bool CheckCurrentClipEqual(AttackType attackType, int layerIndex = 0)
{
var clipInfo = animator.GetCurrentAnimatorClipInfo(layerIndex);
Debug.Log($"clipInfo[0].clip.name : {clipInfo[0].clip.name}\nattackType : {attackType}");
if (clipInfo[0].clip.name.Equals(attackType.ToString()))
return true;
else
return false;
}
오늘의 이슈 / 내일 할 것
어제 이슈 : 패트롤 비동기
Async에서 동작하는 것이 아닌 Coroutine으로 동작할 수 있게끔 하여 내가 시작과 끝을 제어할 수 있게끔 수정하였다.
목표 지점에 다다랐다면 멈춰서서 순찰 애니메이션을 지정한 시간만큼 실행하고 다시 패트롤 상태로 돌입할 수 있게끔 재귀로 구성하였다.
오늘은 패트롤 상태를 구현했다. 어제 생각했던 적의 추적 관련한 문제들이 골치 아팠는데 기획적으로 쉽게 해결할 수 있어서 좀 신기했다. 비동기로 실행할 필요도 없다고 생각을 해서 내일 동기로 바꿀 생각이다. 내일은 패트롤을 동기로 수정하면서 약간의 리팩토링을 하고 추적 상태를 구현하고 남는 시간에는 공격을 할 수 있도록 해야겠다. 내일도 파이팅!
플레이어 상태를 구현하고 적 상태도 구현하기 위해 공통적인 부분들을 부모 클래스로 하여 상속받아 구현하였다.
플레이어와 적 상태머신의 공통적인 부분을 StateMachine에 합치고
using UnityEngine;
public abstract class StateMachine
{
protected IState currentState;
public abstract void Init();
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState?.Enter();
}
public virtual void Update()
{
currentState.UpdateState();
}
public virtual void PhysicsUpdate()
{
currentState.PhysicsUpdateState();
}
}
캐릭터 제어와 관련된 공통적인 부분은 CharacterController에 작성하였다.
[RequireComponent(typeof(Rigidbody))]
public abstract class CharacterController : MonoBehaviour
{
public AnimationController AnimationController { get; private set; }
public Rigidbody Rigidbody { get; private set; }
[field: SerializeField] public MovementData MovementData { get; private set; }
[field: SerializeField] public GroundData GroundData { get; private set; }
protected virtual void Awake()
{
Rigidbody = GetComponent<Rigidbody>();
AnimationController = GetComponentInChildren<AnimationController>();
AnimationController.Init();
}
}
오늘의 이슈 / 내일 할 것
적의 추적 범위, 추적 빈도
이슈
1. 적의 추적 범위는 어느정도 되며
2. 어떤 조건에서 추적 상태를 취소시킬건지
3. 이동할 땅이 존재하는지에 대한 체크는 어떻게 할 것인지
생각해본 것
1. 생성한 이후 맵 전체에서 고유한 플레이어를 추적
2. 일정 거리가 벗어난 5초 뒤에는 추적 상태 해제
3. 적 앞 쪽에 아래 방향으로 Ray를 쏴서 Ground면 이동 아니면 다시 패트롤 상태로 돌입 (추후 Raycast 성능 확인 필요)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyPatrolState : EnemyStateBase
{
public EnemyPatrolState(EnemyStateMachine stateMachine) : base(stateMachine)
{
}
public override void Enter()
{
}
public override void Exit()
{
}
public override void OnDead()
{
}
}
내일 할 것
패트롤 상태 구현
오늘의 회고
오늘은 적 상태머신을 만들기 위해 플레이어 컨트롤러와 플레이어 상태머신의 공통적인 부분을 분리한 뒤 이 부분을 적 컨트롤러와 적 상태머신이 상속받게 했다. 생각보다 큰 작업이어서 테스트하는데도 시간이 좀 걸렸고 좋았던 부분은 다시 내 코드를 보면서 아 이런 방식으로 작성했었지 하면서 다시 공부하게 되는 시간이 됐던 것이다. 내일은 패트롤 상태를 구현하게 되는데 빨리빨리 구현해서 프로토타입을 완성할 수 있었으면 좋겠다. 내일도 파이팅!
오늘은 빌드 테스트를 하고 구르기 버그를 잡았다. 안드로이드 빌드 테스트 중 시간을 너무 많이 쓴 것 같지만 이번에 확실하게 알게 되어서(알 수 밖에 없었지만 ㅋㅋ..) 오히려 좋았다. 그리고 항상 플레이어 쪽에 버그가 많이 나오는데 코드가 점점 증가해서 그런가 신경써야 될 부분들이 많아져서 그런 것 같다. 그래도 버그를 잡는 과정에서 완성도가 늘어난다고 생각해서 잘 하고있다고 생각한다. 내일도 파이팅!
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을 받아와 이동에 적용하는데 구조를 변경하는 작업이 동반되어서 시간이 좀 걸렸던 것 같다. 리팩토링은 할 땐 귀찮은데 하고 나면 괜히 뿌듯한 작업인 것 같다. 내일도 파이팅!