목차
게임에 필요한 상식
25.04.08
지난 시간에 이어서 Character Animation 제작
: Apply Root Motion을 Script에서 조절하여 구현
>> 이어서 Jump 구현
: Sub-State Machine을 활용하여 구현
1. 'On Jump' 설정
: 점프 모션들 추가 및 Parameter를 Float 타입으로 만들고 이름을 'GroundDistance'로 변경
--> Automate Thresholds의 체크를 해제하여 motion 간의 간격을 조정 가능하다.
※ 점프 모션이 조금 어색하다..?
: 강사님의 모션 순서는 다음과 같다
- GoesDown2
- GoesDown
- Peak
- GoesUp2
- GoesUp
- TakeOff
2. 'Idle' -> 'Jump' 설정
: Parameter를 Bool 타입으로 만들어서 적용
--> Has Exit Time도 체크 해제
3. 'Jump' -> 'Idle' 설정
: Parameter를 Bool 타입으로 만들어서 적용
4. 'Move' -> 'Jump' 설정
: Make Transition할 때, StateMachine의 Jump와 연결
※ 'Jump' -> 'Move'는 Animation 상 어색한 부분이 있어서 따로 만들지 않음 즉, Idle로 갔다가 다시 Move로 가도록 조치
5. PlayerController.cs에 코드 작성
// 사용자 입력 처리 함수
private void HandleMovement()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
if (vertical > 0)
{
_animator.SetBool(Move, true);
}
else
{
_animator.SetBool(Move, false);
}
_animator.SetFloat(Speed, _speed);
#region Root Motion Off
// // 달리기
// float speed = 0;
// if (Input.GetKey(KeyCode.LeftShift))
// {
// speed = 1;
// }
//
// _animator.SetFloat("Speed", speed);
#endregion
Vector3 movement = transform.forward * vertical;
transform.Rotate(0, horizontal * rotateSpeed * Time.deltaTime, 0);
#region Root Motion Off
// _characterController.Move(movement * Time.deltaTime);
// _groundDistance = GetDistanceToGround();
// if (Input.GetButtonDown("Jump"))
// {
// _velocity.y = Mathf.Sqrt(jumpForce * -2f * _gravity);
// }
#endregion
// 점프
if (Input.GetButtonDown("Jump") && IsGrounded)
{
_velocity.y = Mathf.Sqrt(jumpForce * -2f * _gravity);
_animator.SetBool("Jump", true);
}
// 점프 높이 설정
_animator.SetFloat("GroundDistance", GetDistanceToGround());
}
6. Parameter를 Trigger 타입으로 만들어서 이름을 'Jump2'로 하고 추가 설정
: 'Idle' -> 'Jump'와 'Move' -> 'Jump'의 Conditions를 Jump2로 수정
--> 코드도 수정
// 점프
if (Input.GetButtonDown("Jump") && IsGrounded)
{
_velocity.y = Mathf.Sqrt(jumpForce * -2f * _gravity);
_animator.SetTrigger("Jump2");
}
※ 점프가 너무 세서 Jump Force를 2로 수정
7. 'Plane'의 Layer를 Ground로 수정
: Ground Layer를 추가한 다음 설정
8. 코드 수정 후, Layer 바인딩
9. Skin Width 조정
: Ray에 살짝 문제가 있어서 캐릭터가 지면에서 살짝 떠있도록 수정
10. 'On Jump' -> 'EllenIdleLand' 설정
Camera의 전방을 기준으로 방향 잡기
: 자신과 상대방의 좌표를 알면 상대방과의 거리와 θ각을 구할 수 있다.
https://spice-theory-152.notion.site/1c4d3cfdca3480f0b2cef91667339361
게임에 필요한 수학 상식 | Notion
좌표계
spice-theory-152.notion.site
1. 코드 작성
: CameraController.cs
2. Camera 바인딩
3. Ellen Prefab화
다른 Scene에 만든 'Ellen'을 추가하여 테스트
- 'PlayerController'에 Main Camera를 다시 바인딩
- Main Camera에 'CameraController' 추가 및 'Ellen' 바인딩
- 공간의 Layer를 'Ground'로 설정
※ 지금은 수작업으로 할당하지만 나중에는 Ellen이 Intantiate로 생성되어야 하기 때문에 이를 코드로 처리해야한다.
>> Ellen이 어둡게 나오는 현상 해결
: Map은 전체적으로 Bake되어 있어서 밝은 것이기에, Light Probe를 적용하여 해결
1. Light Probe Group 생성
2. 옆면에서 작업
: Jump도 할테니 Y축으로도 범위를 넓혀줌
--> Gizmo를 Iso로 바꿔서 작업하면 편하다
3. 이후 Top View에서도 작업하여 완성
- 결과물
--> 임시로 일단 방 하나만 작업해둠
4. 다시 Bake 하기
>> 최종 결과
Attack Animation 구현
1. Animator에서 기존에 만들어둔 'Attack' State를 삭제하고 새로 Sub-State Machine 생성
: 이름을 'Attack'으로 변경
2. Attack 내부로 들어가서 Animation 추가
: EllenCombo1~4를 추가한 뒤 Make Transition으로 연결
3. 공격 모션 중 추가적으로 입력이 없을 때 빠져나가기 위해 Exit에 Make Transition
4. Trigger로 Parameter 생성
: 이름은 'Attack'
5. Transition의 조건 설정
: Has Exit Time은 체크 유지 --> 모션이 끝나야 다음 공격으로 이어지도록
>> 2 → 3, 3 → 4도 마찬가지로 설정
6. Attack에 Make Transition 및 설정
: Attack → Move, Move → Attack, Attack → Idle
--> Make Transition 할 때, StateMachine-Attack에 연결
--> Has Exit Time 체크 해제 및 Conditions 추가 (Attack → Move, Move → Attack)
--> Attack → Idle은 연결만 해두기
└ Animation의 Events
: Animation을 더블 클릭하고 Inspector의 Events를 보면 Ellen에 기본적으로 Events를 추가해놨다.
--> 함수나 Float, Int, String, Object 등을 할당해줄 수 있다.
--> 이를 이용하여 공격하려고 무기를 꺼낼 때나 공격 후 무기를 다시 넣을 때 Enemy에게 공격판정이 들어가지 않도록 즉, 공격판정을 더 정확하게 만들 수 있다.
--> Combo1~4까지 전부 세팅되어 있고 Function 이름도 동일하게 되어있다.
>> 코드 작성
: PlayerController.cs
--> MeleeAttackStart()와 MeleeAttackEnd()를 활용
Player의 상태 구현 (상태 패턴 활용)
: 기존의 PlayerController는 PlayerControllerOld로 이름을 변경하고 새롭게 PlayerController.cs 생성 및 작성
※ Unity에서 만드는 State Script
>> Interface로 'IPlayerState' 생성
>> 여러 상태들을 Script로 생성
- PlayerStateIdle.cs
- PlayerStateMove.cs
- PlayerStateJump.cs
- PlayerStateAttack.cs
- PlayerStateHit.cs
- PlayerStateDead.cs
>> 코드 작성
- IPlayerState.cs
- PlayerController.cs
- 여러 상태들
>> Jump 구현
: Animator에서 Idle → Jump, Move → Jump 조건을 Jump로 변경
└ Jump가 어색하게 구현되는 문제
: Jump → Idle 로 넘어가는 조건이 어색해서 Jump 도중에 Idle로 돌아가는 문제
- Class의 State 패턴으로 해결 --> 현재 짜고있는 방식
- Behaviour State를 활용하여 해결
--> 원래라면 최대한 일관되게 하나의 방식으로만 짜서 해결하겠지만, 지금은 Behaviour State를 활용하면 간단하게 해결 가능하기도 하고 실전이 아닌 수업이므로 Behaviour State를 활용하여 해결하는 방식을 채용
1. Class의 State 패턴으로 해결
: PlayerStateJump.cs
--> 결국 이렇게 작성해도 어색하게 구현됐음 다른 방식으로 해결하는 방법이 있을 것
public void Update()
{
var distanceToGround = _playerController.GetDistanceToGround();
if (distanceToGround < 0.1f)
{
_playerController.SetState(PlayerState.Idle);
}
else
{
_playerController.Animator.SetFloat("GroundDistance", _playerController.GetDistanceToGround());
}
}
2. Behaviour State를 활용하여 해결
: PlayerAnimatorStateJump.cs를 생성하여 Animation을 재생한 후, Exit할 때 Idle로 변경
>> Ellen에 기존에 있던 PlayerControllerOld.cs는 비활성화하고 PlayerController.cs 추가
: 그리고 바인딩 (강사님은 PlayerControllerOld를 Remove하심)
--> 이후 Prefab에 Override하기
└ CustomEditor로 Player의 상태 확인하기
: 디버깅을 원활하게 해준다. --> Inspector에서 상태 확인 가능
- PlayerControllerEditor.cs 생성 후 코드 작성
└ Play시, 마우스 커서 사라지게 하기
: GameManager.cs를 생성하여 코드 작성 --> 이전에 작성했던 Singleton.cs 를 재활용
>> 빈 게임 오브젝트로 'GameManager'를 만들어서 GameManager.cs 추가
최종 코드
>> PlayerControllerOld.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(Animator))]
public class PlayerControllerOld : MonoBehaviour
{
private static readonly int Move = Animator.StringToHash("Move");
private static readonly int Speed = Animator.StringToHash("Speed");
[SerializeField] private float rotateSpeed = 100f;
[SerializeField] private float jumpForce = 2f;
[SerializeField] private LayerMask groundLayer; // 땅으로 인식할 레이어
[SerializeField] private Transform cameraTransform;
private CharacterController _characterController;
private Animator _animator;
private float _gravity = -9.81f;
private Vector3 _velocity;
private float _groundDistance;
// Root Motion On
private float _groundedMinDistance = 0.1f;
private float _speed = 0f;
private bool _isAttacking = false;
private bool IsGrounded
{
get
{
var distance = GetDistanceToGround();
return distance < _groundedMinDistance;
}
}
private void Awake()
{
_characterController = GetComponent<CharacterController>();
_animator = GetComponent<Animator>();
}
private void Start()
{
// 커서 설정
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
// 커서 Rock 해제
if (Input.GetKeyDown(KeyCode.Escape))
{
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
HandleMovement();
// ApplyGravity(); // Root Motion Off
CheckRun();
// 점프 높이 설정
_animator.SetFloat("GroundDistance", GetDistanceToGround());
}
// 사용자 입력 처리 함수
private void HandleMovement()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
if (vertical > 0)
{
RotatePlayerToCameraForward();
_animator.SetBool(Move, true);
}
else
{
_animator.SetBool(Move, false);
}
_animator.SetFloat(Speed, _speed);
#region Root Motion Off
// // 달리기
// float speed = 0;
// if (Input.GetKey(KeyCode.LeftShift))
// {
// speed = 1;
// }
//
// _animator.SetFloat("Speed", speed);
#endregion
Vector3 movement = transform.forward * vertical;
transform.Rotate(0, horizontal * rotateSpeed * Time.deltaTime, 0);
#region Root Motion Off
// _characterController.Move(movement * Time.deltaTime);
// _groundDistance = GetDistanceToGround();
// if (Input.GetButtonDown("Jump"))
// {
// _velocity.y = Mathf.Sqrt(jumpForce * -2f * _gravity);
// }
#endregion
// 점프
if (Input.GetButtonDown("Jump") && IsGrounded)
{
_velocity.y = Mathf.Sqrt(jumpForce * -2f * _gravity);
_animator.SetTrigger("Jump2");
}
// 공격
if (Input.GetButtonDown("Fire1") && !_isAttacking)
{
_animator.SetTrigger("Attack");
}
}
#region Root Motion Off
// 중력 적용 함수
private void ApplyGravity()
{
_velocity.y += _gravity * Time.deltaTime;
_characterController.Move(_velocity * Time.deltaTime);
}
#endregion
// 달리기 처리
private void CheckRun()
{
if (Input.GetKey(KeyCode.LeftShift))
{
_speed += Time.deltaTime;
_speed = Mathf.Clamp01(_speed); //Mathf.Clamp(_speed, 0f, 1f);와 동일
}
else
{
_speed -= Time.deltaTime;
_speed = Mathf.Clamp01(_speed);
}
}
// 바닥과 거리를 계산하는 함수
private float GetDistanceToGround()
{
float maxDistance = 10f;
if (Physics.Raycast(
transform.position, Vector3.down, out RaycastHit hit, maxDistance, groundLayer))
{
return hit.distance;
}
else
{
return maxDistance;
}
}
// 카메라의 방향으로 캐릭터의 이동 방향 설정
private void RotatePlayerToCameraForward()
{
Vector3 cameraForward = cameraTransform.forward;
cameraForward.y = 0; // 각도를 구할 것이기에 높이는 필요하지 않기 때문
cameraForward.Normalize(); // 정규화
// // #1 삼각함수
// float targetAngle = Mathf.Atan2(cameraForward.x, cameraForward.z) * Mathf.Rad2Deg;
// float currentAngle = Mathf.Atan2(transform.forward.x, transform.forward.z) * Mathf.Rad2Deg;
// float angle = Mathf.DeltaAngle(currentAngle, targetAngle);
//
// transform.Rotate(0, angle, 0);
// // #2 벡터의 내적
// float dotProduct = Vector3.Dot(transform.forward, cameraTransform.forward);
// float angle = Mathf.Acos(dotProduct) * Mathf.Rad2Deg;
// // #3 벡터의 외적
// Vector3 crossProduct = Vector3.Cross(transform.forward, cameraTransform.forward);
// float angle = Mathf.Asin(crossProduct.y) * Mathf.Rad2Deg;
// // 부드럽게 회전 --> 위의 3가지 방법 중 하나를 사용한 다음 사용
// Quaternion targetRotation = Quaternion.Euler(0, angle, 0);
// transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
// #4 Quaternion
Quaternion targetRotation = Quaternion.LookRotation(cameraForward);
// 부드럽게 회전
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
}
#region Animator Method
private void OnAnimatorMove()
{
Vector3 movePosition;
if (IsGrounded)
{
movePosition = _animator.deltaPosition;
}
else
{
movePosition = _characterController.velocity * Time.deltaTime;
}
// 중력 적용
_velocity.y += _gravity * Time.deltaTime;
movePosition.y = _velocity.y * Time.deltaTime;
_characterController.Move(movePosition);
}
public void MeleeAttackStart()
{
_isAttacking = true;
}
public void MeleeAttackEnd()
{
_isAttacking = false;
}
#endregion
#region Debug
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + Vector3.down * _groundDistance);
}
#endregion
}
>> PlayerController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum PlayerState { None, Idle, Move, Jump, Attack, Hit, Dead }
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(Animator))]
public class PlayerController : MonoBehaviour
{
[Header("Player")] // Lv Up시, 변경될 Player의 스탯
[SerializeField] private int maxHealth = 100;
[SerializeField] private int attackPower = 10;
[Header("Movement")] // 이동 관련
[SerializeField] private float jumpSpeed = 2f;
[SerializeField] private float rotationSpeed = 100f;
[SerializeField] private LayerMask groundLayer;
[SerializeField] private float maxGroundCheckDistance = 10f;
[Header("Attach Points")] // 지팡이를 손에 쥘 때 필요한 손의 Position, 카메라를 위한 머리의 Position 등
[SerializeField] private Transform leftHandTransform;
[SerializeField] private Transform headTransform;
// ㅡㅡㅡ 상태 관련 ㅡㅡㅡ
private PlayerStateIdle _playerStateIdle;
private PlayerStateMove _playerStateMove;
private PlayerStateJump _playerStateJump;
private PlayerStateAttack _playerStateAttack;
private PlayerStateHit _playerStateHit;
private PlayerStateDead _playerStateDead;
public PlayerState CurrentState { get; private set; }
private Dictionary<PlayerState, IPlayerState> _playerStates;
// ㅡㅡㅡ 외부 접근 가능 변수 ㅡㅡㅡ
public Animator Animator { get; private set; }
public bool IsGrounded
{
get
{
return GetDistanceToGround() < 0.2f;
}
}
// ㅡㅡㅡ 내부에서만 사용되는 변수 ㅡㅡㅡ
private CharacterController _characterController;
private const float _gravity = -9.81f;
private Vector3 _velocity = Vector3.zero;
private int _currentHealth = 0;
private void Awake()
{
Animator = GetComponent<Animator>();
_characterController = GetComponent<CharacterController>();
}
private void Start()
{
// 상태 초기화
_playerStateIdle = new PlayerStateIdle();
_playerStateMove = new PlayerStateMove();
_playerStateJump = new PlayerStateJump();
_playerStateAttack = new PlayerStateAttack();
_playerStateHit = new PlayerStateHit();
_playerStateDead = new PlayerStateDead();
_playerStates = new Dictionary<PlayerState, IPlayerState>
{
{ PlayerState.Idle, _playerStateIdle },
{ PlayerState.Move, _playerStateMove },
{ PlayerState.Jump, _playerStateJump },
{ PlayerState.Attack, _playerStateAttack },
{ PlayerState.Hit, _playerStateHit },
{ PlayerState.Dead, _playerStateDead }
};
SetState(PlayerState.Idle);
// 체력 초기화
_currentHealth = maxHealth;
}
private void Update()
{
if (CurrentState != PlayerState.None)
{
_playerStates[CurrentState].Update();
}
}
public void SetState(PlayerState state)
{
if (CurrentState != PlayerState.None) // 맨 처음 SetState를 할 때는 PlayerState.None 상태기 때문
{
_playerStates[CurrentState].Exit();
}
CurrentState = state;
_playerStates[CurrentState].Enter(this);
}
#region 동작 관련
public void Rotate(float x, float z) // Idle과 Move 둘다 같은 함수가 필요하므로 PlayerController에서 구현
{
// 카메라 설정
// Tag로 MainCamera가 할당되어 있으면 따로 할당 없이 Camera.main으로 접근 가능하다.
var cameraTransform = Camera.main.transform;
var cameraForward = cameraTransform.forward;
var cameraRight = cameraTransform.right;
// Y값을 0으로 설정해서 수평 방향만 고려
cameraForward.y = 0;
cameraRight.y = 0;
// 입력 방향에 따라 카메라 기준으로 이동 방향 계산
var moveDirection = cameraForward * z + cameraRight * x;
// 이동 방향이 있을 경우에만 회전
if (moveDirection != Vector3.zero)
{
moveDirection.Normalize();
transform.rotation = Quaternion.LookRotation(moveDirection);
}
}
public void Jump() // _velocity.y값이 여기 있기 때문에 편하게 PlayerController에서 구현
{
_velocity.y = Mathf.Sqrt(jumpSpeed * -2f * _gravity);
}
private void OnAnimatorMove()
{
Vector3 movePosition;
if (IsGrounded)
{
movePosition = Animator.deltaPosition;
}
else
{
movePosition = _characterController.velocity * Time.deltaTime;
}
// 중력 적용
_velocity.y += _gravity * Time.deltaTime;
movePosition.y = _velocity.y * Time.deltaTime;
_characterController.Move(movePosition);
}
public void MeleeAttackStart()
{
}
public void MeleeAttackEnd()
{
}
#endregion
#region 계산 관련
// 바닥과 거리를 계산하는 함수
public float GetDistanceToGround()
{
float maxDistance = 10f;
if (Physics.Raycast(
transform.position, Vector3.down, out RaycastHit hit, maxDistance, groundLayer))
{
return hit.distance;
}
else
{
return maxDistance;
}
}
#endregion
}
>> IPlayerState.cs
public interface IPlayerState
{
// 해당 상태로 진입했을 때 호출되는 Method
void Enter(PlayerController playerController);
// 해당 상태에 머물러 있을 때 Update 주기로 호출되는 Method
void Update();
// 해당 상태에서 빠져나갈 때 호출되는 Method
void Exit();
}
>> PlayerStateIdle.cs
using UnityEngine;
public class PlayerStateIdle : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;
public void Enter(PlayerController playerController)
{
_playerController = playerController;
_playerController.Animator.SetBool("Idle", true);
}
public void Update()
{
var inputVertical = Input.GetAxis("Vertical");
var inputHorizontal = Input.GetAxis("Horizontal");
// 이동
if (inputVertical != 0 || inputHorizontal != 0)
{
_playerController.Rotate(inputHorizontal, inputVertical);
_playerController.SetState(PlayerState.Move);
return;
}
// 점프
if (Input.GetButtonDown("Jump"))
{
_playerController.SetState(PlayerState.Jump);
return;
}
// 공격
if (Input.GetButtonDown("Fire1"))
{
_playerController.SetState(PlayerState.Attack);
return;
}
}
public void Exit()
{
_playerController.Animator.SetBool("Idle", false);
_playerController = null;
}
}
>> PlayerStateMove.cs
using UnityEngine;
public class PlayerStateMove : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;
private float _speed = 0f;
public void Enter(PlayerController playerController)
{
_playerController = playerController;
_playerController.Animator.SetBool("Move", true);
}
public void Update()
{
var inputVertical = Input.GetAxis("Vertical");
var inputHorizontal = Input.GetAxis("Horizontal");
// 이동
if (inputVertical != 0 || inputHorizontal != 0)
{
_playerController.Rotate(inputHorizontal, inputVertical);
}
else
{
_playerController.SetState(PlayerState.Idle);
return;
}
// 점프
if (Input.GetButtonDown("Jump") && _playerController.IsGrounded)
{
_playerController.SetState(PlayerState.Jump);
return;
}
// 공격
if (Input.GetButtonDown("Fire1") && _playerController.IsGrounded)
{
_playerController.SetState(PlayerState.Attack);
return;
}
// Left Shift 버튼을 누르면 속도 증가
if (Input.GetKey(KeyCode.LeftShift))
{
if (_speed < 1f)
{
_speed += Time.deltaTime;
_speed = Mathf.Clamp01(_speed);
}
}
else
{
if (_speed > 0f)
{
_speed -= Time.deltaTime;
_speed = Mathf.Clamp01(_speed);
}
}
_playerController.Animator.SetFloat("Speed", _speed);
}
public void Exit()
{
_playerController.Animator.SetBool("Move", false);
_playerController = null;
}
}
>> PlayerStateJump.cs
using UnityEngine;
public class PlayerStateJump : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;
public void Enter(PlayerController playerController)
{
_playerController = playerController;
_playerController.Animator.SetBool("Jump", true);
_playerController.Jump();
}
public void Update()
{
_playerController.Animator.SetFloat("GroundDistance", _playerController.GetDistanceToGround());
}
public void Exit()
{
_playerController.Animator.SetBool("Jump", false);
_playerController = null;
}
}
>> PlayerStateAttack.cs
using UnityEngine;
public class PlayerStateAttack : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;
public void Enter(PlayerController playerController)
{
_playerController = playerController;
_playerController.Animator.SetTrigger("Attack");
}
public void Update()
{
}
public void Exit()
{
_playerController = null;
}
}
>> PlayerStateHit.cs
using UnityEngine;
public class PlayerStateHit : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;
public void Enter(PlayerController playerController)
{
_playerController = playerController;
}
public void Update()
{
}
public void Exit()
{
_playerController = null;
}
}
>> PlayerStateDead.cs
using UnityEngine;
public class PlayerStateDead : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;
public void Enter(PlayerController playerController)
{
_playerController = playerController;
}
public void Update()
{
}
public void Exit()
{
_playerController = null;
}
}
>> PlayerControllerEditor.cs
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(PlayerController))]
public class PlayerControllerEditor : Editor
{
public override void OnInspectorGUI()
{
// 기본 인스펙터를 그리기
base.OnInspectorGUI();
// 타겟 컴포넌트 참조 가져오기
PlayerController playerController = (PlayerController)target;
// 여백 추가
EditorGUILayout.Space();
EditorGUILayout.LabelField("상태 디버그 정보", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 상태별 색상 지정
switch (playerController.CurrentState)
{
case PlayerState.None:
GUI.backgroundColor = new Color(1, 1, 1, 1f);
break;
case PlayerState.Idle:
GUI.backgroundColor = new Color(0, 0, 1, 1f);
break;
case PlayerState.Move:
GUI.backgroundColor = new Color(0, 1, 0, 1f);
break;
case PlayerState.Jump:
GUI.backgroundColor = new Color(1, 0, 1, 1f);
break;
case PlayerState.Attack:
GUI.backgroundColor = new Color(1, 1, 0, 1f);
break;
case PlayerState.Hit:
GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1f);
break;
case PlayerState.Dead:
GUI.backgroundColor = new Color(1, 0, 0, 1f);
break;
}
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("현재 상태", playerController.CurrentState.ToString(),
EditorStyles.boldLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.EndVertical();
// Color 초기화
GUI.backgroundColor = Color.white;
// 여백 추가
EditorGUILayout.Space();
EditorGUILayout.LabelField("캐릭터 위치 디버그 정보", EditorStyles.boldLabel);
// 지면 접촉 상태
GUI.enabled = false; // toggle은 직접 클릭이 가능하기 때문에 읽기 전용으로 만들어줌
EditorGUILayout.Toggle("지면 접촉", playerController.IsGrounded);
GUI.enabled = true;
// 여백 추가
EditorGUILayout.Space();
EditorGUILayout.LabelField("캐릭터 상태 강제 변경", EditorStyles.boldLabel);
// 강제로 상태 변경 버튼
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Idle"))
playerController.SetState(PlayerState.Idle);
if (GUILayout.Button("Move"))
playerController.SetState(PlayerState.Move);
if (GUILayout.Button("Jump"))
playerController.SetState(PlayerState.Jump);
if (GUILayout.Button("Attack"))
playerController.SetState(PlayerState.Attack);
if (GUILayout.Button("Hit"))
playerController.SetState(PlayerState.Hit);
if (GUILayout.Button("Dead"))
playerController.SetState(PlayerState.Dead);
EditorGUILayout.EndHorizontal();
}
}
>> Singleton.cs
using UnityEngine;
using UnityEngine.SceneManagement;
public abstract class Singleton<T> : MonoBehaviour where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
private void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
// 경우에 따라 첫 Scene의 OnSceneLoaded가 호출이 안 되는 경우를 해결
OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
// Scene 전환 시, 호출되는 Action Method 할당
SceneManager.sceneLoaded += OnSceneLoaded;
}
else
{
Destroy(gameObject);
}
}
// Destroy 후에는 OnSceneLoaded가 할당하지 않도록
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
protected abstract void OnSceneLoaded(Scene scene, LoadSceneMode mode);
}
>> GameManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : Singleton<GameManager>
{
private void Start()
{
// 커서 설정
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
// 커서 설정
if (Input.GetKeyDown(KeyCode.Escape))
{
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
}
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
}
>> PlayerAnimatorStateJump.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAnimatorStateJump : StateMachineBehaviour
{
// OnStateExit is called before OnStateExit is called on any state inside this state machine
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.gameObject.GetComponent<PlayerController>().SetState(PlayerState.Idle);
}
}
'Development > Unity BootCamp' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 최종 팀 프로젝트 (0) | 2025.04.28 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 91일차 (0) | 2025.04.22 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 89일차 (0) | 2025.04.04 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 88일차 (1) | 2025.04.03 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 87일차 (1) | 2025.04.02 |