처음 이동 스크립트는 대~충 Update에 Input.~~ 써넣어서 만들었다.
이동은 몇개없으니 괜찮을거라는 안일함
하지만 과거 스펠렁키만들때와 똑같은 실수였다.ㅜㅜ
그러다가 동작하나를 추가하고, 에러가 나는순간 걷잡을 수 없다는거...
그래서 코드를 갈아엎었다.
BT(행동트리)로 구현하다가 FSM보다 더 복잡해질 것 같고, 플레이어를 구현하면 더 힘들것같았기에
FSM으로 만들었다.
모든 캐릭터는 여러 동작을 가지고 있다.
몬스터나 플레이어나 동작들이 변화하며 움직인다.
그리고 모든 캐릭터는 정보를 가지고 있다.
그래서 CharacterFSM와 CharacterInfo를 만들었다.
Player는 PlayerFSM와 PlayerInfo가 있고,
Monster는 MonsterFSM와 MonsterInfo가 있다.
그리고 FSM을 정석으로 만드는 방법은 예전에 쓴 스펠렁키처럼 만들면 되지만...
하지만 답이 이거다하는건 아니다. 더 간략화하는 방법이 있다.
코루틴으로 실행해서 각 상태의 코루틴을 변경하는 방식.
Ienumerator Move() {~~}
Ienumerator Jump() {~~}
이렇게 만들어 준다.
그리고 void chageState(enum state)라는 함수를 만들고,
현재 상태의 코루틴을 종료하고 새로운 상태 코루틴을 실행해주는 방식
코루틴을 실행하고 종료하는 방법이 메소드이름을 가져와서 쓴다는것을 이용한 방법이라 편하다.
최적화 면에선 좋은 방법은 아니지만, 만들기가 편해진다.
CharacterFSM
public enum State
{
Idle,
Move,
Jump,
...,
}
public class A
{
public void ChangeState(State newState)
{
StopCoroutine(curState.ToString());
curState = newState;
StartCoroutine(curState.ToString());
animator.SetInteger("curState", (int)curState);
}
public IEnumerator Idle() { while(true){yield return null; } }
public IEnumerator Move() { while(true){yield return null; } }
public IEnumerator Jump() { while(true){yield return null; } }
}
다시한번 말하지만 update대신 코루틴을 쓰는거라 최적화에 좋지 않다.
startCoroutine()을 쓸 때 마다 객체를 리턴해서 GC(Garbage Collector)가 하나씩 수거해간다.
이 GC의 번거로움을 덜어내괒 오브젝트풀같은 최적화 방법을 사용하는데, update를 놔두고 위처럼 구현하는건 좋은 방법은 아니다.
하지만 만들기가 쉬워지고, 편해서 쓰는 것일뿐...
만들기 전에 게임의 규모를 생각하고, 이정도는 게임에 전혀 지장이 없을꺼라는 확실을 했기때문에 사용하는거다.
위 방법은 각 상태를 코루틴으로 정의하고 상태 코루틴을 바꿔가는 방법.
피규어 파츠바꾸듯이 코루틴을 바꿔끼우는거다.
각 상태에서 {while(true){~~~}}부분을 입맛대로 바꾸면 된다.
이동
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum State
{
Idle = 0,
Move = 1,
Jump = 2,
SitDown = 3,
Attack = 4,
Interactive = 5,
GetHit = 6,
Die = 7,
}
public abstract class CharacterFSM : MonoBehaviour
{
[SerializeField] protected State curState;
protected CharacterController characterController;
protected Animator animator;
protected CharacterInfo myInfo;
protected virtual void Awake()
{
curState = State.Idle; // 기본상태로 시작
StartCoroutine(curState.ToString());
}
// 현재 상태 코루틴을 변경
public void ChangeState(State newState)
{
StopCoroutine(curState.ToString());
curState = newState;
StartCoroutine(curState.ToString());
animator.SetInteger("curState", (int)curState);
}
// 자신의 앞에 있는 적을 공격
protected void Hit(Transform me)
{
if (Physics.SphereCast(transform.position, 0.5f, me.forward, out RaycastHit hit, myInfo.attackRange, myInfo.enemyLayer))
{
hit.transform.GetComponent<CharacterInfo>().GetHit(myInfo.StrikingPower);
}
}
}
우선 각 상태를 enum으로 만들었다.
그 상태의 이름에 맞는 함수를 만들어준다.
그리고 그 상태를 문자열로 받아와서 코루틴을 실행하고 종료하도록 만든다.
이 클래스를 상속받아서 Move나 Jump등을 코루틴으로 구현하면 된다.
이동 & 점프
public class PlayerFSM : CharacterFSM
{
//플레이어 물리구현 값
[Header("물리구현 값 (확인용)")]
[SerializeField] private Vector3 moveDir; // 이동 방향
//애니메이션 값
[Space(10f)]
[Header("애니메이션 (확인용)")]
[SerializeField] private bool isLeftPunch = false;
[SerializeField] private bool isRightPunch = false;
[SerializeField] private float temp_punch = 1;
[Space(10f)]
[Header("player 설정값")]
[SerializeField] LayerMask interactiveLayer;
[SerializeField] private Transform unityChan;
[SerializeField] private Transform camArm;
[Space(10f)]
[Header("기타")]
[SerializeField] Interactable targetObj;
protected override void Awake()
{
characterController = GetComponent<CharacterController>();
animator = unityChan.GetComponent<Animator>();
myInfo = GetComponent<PlayerInfo>();
base.Awake();
}
// Idle와 Move가 blend tree로 함께 구현되어 있음.
public IEnumerator Idle() // (+Move)
{
while (true)
{
Vector2 moveInput = MoveTo();
animator.SetFloat("MoveSpeed", moveInput.magnitude);
if (Input.GetMouseButtonDown(0))
{
StartCoroutine("Punch");
}
if (Input.GetKeyDown(KeyCode.F))
{
Interact();
if (GameManager.Instance.GetGameState(GAMESTATE.TALK))
{
ChangeState(State.Interactive);
}
}
if (Input.GetButton("Jump"))
{
JumpTo();
}
if (!characterController.isGrounded)
{
ChangeState(State.Jump);
}
else if (Input.GetButton("Sit"))
{
ChangeState(State.SitDown);
}
yield return null;
}
}
// 점프
public IEnumerator Jump()
{
while (true)
{
if (Input.GetMouseButtonDown(0))
{
StartCoroutine("Punch");
}
// 중력 적용
moveDir.y += myInfo.gravity * Time.deltaTime;
Vector2 moveInput = MoveTo();
animator.SetFloat("MoveSpeed", moveInput.magnitude);
animator.SetFloat("JumpSpeed", moveDir.y);
if (characterController.isGrounded)
{
if (Input.GetButton("Sit"))
{
ChangeState(State.SitDown);
}
else
{
ChangeState(State.Idle);
}
}
yield return null;
}
}
// 앉기
public IEnumerator SitDown()
{
while (true)
{
if (Input.GetMouseButtonDown(0))
{
StartCoroutine("Punch");
}
float sitDown = Input.GetAxis("Sit");
animator.SetFloat("SitDown", sitDown);
if (Input.GetButton("Jump"))
{
JumpTo();
MoveTo();
}
if (!characterController.isGrounded)
{
ChangeState(State.Jump);
}
else if (Input.GetButtonUp("Sit"))
{
ChangeState(State.Idle);
}
yield return null;
}
}
위에 만든 CharatorFSM을 상속받아서 스크립트를 만들었다.
그리고 원하는 상태에서 전이할 때 ChageState(State.name)를 쓰면 된다.
Idle는 만들다보니 blend Tree로 구현했다.
걷는 상태에서 자연스럽게 움직이도록 바꾸고싶었고, Move를 따로 만드는것보다 Idle에 같이 넣는게 더 자연스러웠다.
이동키를 누르면 getAsix로 값을 받아와서 0에서 1까지 서서히 늘어나기때문에 자연스러운 움직임이 가능하다.
Idle과 Move를 합쳐서 만들었다.
이렇게 만들면 가중치에따라 애니메이션의 상태가 정해지니 자연스러운 움직임이 가능하다.
input이 0이면 Idle이고 0.5라면 걷기, 1이라면 달리기로 바뀐다.
getAxis로 받아오니 값이 서서히 증가하고, 애니메이션이 자연스럽게 이어질 수 있다.
(그리고 위 스크립트에 상호작용이나 공격도 있는데, 다음에 설명할 예정)
private Vector2 MoveTo()
{
Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
moveInput = RunTo(moveInput);
//카메라의 방향을 계산해서 입력한 방향으로 움직임
Vector3 lookForward = new Vector3(camArm.forward.x, 0f, camArm.forward.z).normalized;
Vector3 lookRight = new Vector3(camArm.right.x, 0f, camArm.right.z).normalized;
Vector3 _moveDir = lookForward * moveInput.y + lookRight * moveInput.x;
if (moveInput.magnitude > Mathf.Epsilon)
{
unityChan.forward = _moveDir;
}
// 이동
characterController.Move(Time.deltaTime * myInfo.moveSpeed * new Vector3(_moveDir.x, moveDir.y, _moveDir.z));
return moveInput;
}
// 달리기
public Vector3 RunTo(Vector2 _moveInput)
{
float runInput = Input.GetAxis("Sprint");
_moveInput *= 0.5f + runInput * 0.5f; //x축과 z축에 달리기속도 더하기
return _moveInput;
}
public void JumpTo()
{
moveDir.y = myInfo.jumpPower;
}
각 움직임을 계산하는 메소드다.
MoveTo는 카메라의 방향을 계산해서 움직이도록 만들었다.
달리기는 Shift를 누르면 달리도록 만들었는데, sprint를 추가했다.
Edit - Project Settings - Input Manager에서 Size가 기본18로 되어있다.
19로 늘려주고 Sprint를 추가했다.
앉기
// 앉기
public IEnumerator SitDown()
{
while (true)
{
if (Input.GetMouseButtonDown(0))
{
StartCoroutine("Punch");
}
float sitDown = Input.GetAxis("Sit");
animator.SetFloat("SitDown", sitDown);
// 점프 후 이동연산
if (Input.GetButton("Jump"))
{
JumpTo();
MoveTo();
}
if (!characterController.isGrounded)
{
ChangeState(State.Jump);
}
else if (Input.GetButtonUp("Sit"))
{
ChangeState(State.Idle);
}
yield return null;
}
}
앉기도 다른 코드랑 비슷하게 만들었다.
앉기도 getAxis로 받아오기때문에 자연스럽게 움직이는데, 앉기버튼을 때면 벌떡 일어나버린다.
그래서 앉기버튼에서 다른상태로 전이할 때 Transition Duration을 0.1초로 설정했다.
transition Duration의 값을 약간씩 넣어주면 움직임에 자연스러움이 더해진다.
2D게임을 만들 땐 움직임이 과하면서 부자연스러운 느낌을 2D만의 개성으로 살릴 수 있었다.
하지만 3D의 경우는 움직임이 사실처럼 묘사되는게 더 몰입감에 좋은 것 같다.
'유니티_일기 > 3D_RPG!' 카테고리의 다른 글
3D RPG 만들기! (6) 몬스터+아이템 만들기 (0) | 2023.08.17 |
---|---|
3D RPG 만들기! (5) 공격 만들기 - 레이어마스크, 애니메이션 레이어 (0) | 2023.08.09 |
3D RPG 만들기! (4) 대화 구현하기 (0) | 2023.08.07 |
3D RPG 만들기! (2) TPS로 만들기 (0) | 2023.08.03 |
3D RPG 만들기! (1) 유니티짱 적용하기 (0) | 2023.08.02 |
댓글