이전에 커스텀 애니메이터를 만들었으므로, 캐릭터 상태에 따른 애니메이션 업데이트를 테스트 하며, 게임에서 플레이어가 행동을 할때 사용하는 FSM을 제작합니다. FSM은 Finite-State Machine(유한 상태 기계)의 약자이며, 오브젝트가 특정 상태에 따라 다른 행동을 취할 수 있게 하는 모델입니다. FSM으로 구현하게 되면 상태가 추가되어도 구현하기가 쉬우며, 상태별로 코루틴을 만들기 때문에 코드를 작성할 때 헷갈리지 않고 독립적으로 만들 수 있습니다. 플레이어/몬스터 등 state를 가지게 되는 오브젝트들에게 부여할 예정이며, FSMbase 스크립트를 만들고 필요에 따라 상속시켜 구체적으로 구현하도록 합니다.
1. FSM base
public class FSMbase : MonoBehaviour
{
public enum State
{
idle = 0,
move,
attack,
dead,
skill,
hited,
};
public State objectState;
public myAnimator _anim;
public bool newState;
public string myPath;
public void Awake()
{
objectState = State.idle;
_anim = GetComponent<myAnimator>();
setAnim();
}
public void OnEnable()
{
StartCoroutine("FSMmain");
}
public virtual void setState(State s)
{
newState = true;
objectState = s;
_anim.setState(objectState.ToString());
}
IEnumerator FSMmain()
{
while (true)
{
newState = false;
yield return StartCoroutine(objectState.ToString());
}
}
}
enum형태로 State 타입을 만들어 필요한 상태들을 추가합니다. 또한 상태에 맞게 애니메이션을 바꿔주기 위해 애니메이터를 캐싱합니다. FSMmain 코루틴은 현재 상태에 대한 코루틴이 계속 실행 될 수 있게 합니다. 상태가 바뀌면 setState 함수를 호출해 현재 상태를 바꾸고, 애니메이터에게 현재 상태를 넘겨줍니다. 이렇게 구현하면, 한번에 한가지 상태만 유지되게 하며 예기치 않게 코루틴이 종료되어도 현재 상태의 코루틴을 다시 실행시킵니다.
이제 애니메이터를 초기화합니다.
public virtual void setAnim() {//Awake에서 실행해줍니다.
_anim.setPath(myPath);
_anim.setDir(6);//아래방향으로 초기화
_anim.initAnims(Enum.GetNames(typeof(State)));
}
public virtual void setState(State s,int atkNum)//공격같은 경우 3번째 공격까지 있기 때문에 하나 더 만들어 줍니다.
{
newState = true;
objectState = s;
_anim.setState(objectState.ToString()+"/"+atkNum);
}
이후엔 base를 상속 받아서 다음과 같은 형태로 State별로 구현해주면 됩니다.
IEnumerator idle()
{
do
{
yield return null;
} while (!newState);
}
2. playerFSM 구현
public class playerFSM : FSMbase
{
public float attackRange = 1.1f;
public float moveSpeed = 5;
float speedRate;
int atkNum;
int degree;
Rigidbody2D RBD;
bool animEnd;
public float checkDashTime = 0.75f;//대쉬 가능 시간
float checkDashTimeTemp;
int canDash = 0;//dash페이즈 0=기본, 1=처음 눌림, 2=뗌, 3= 다시 눌림(대쉬시작)
bool dashState = false;
// Use this for initialization
void Awake()
{
base.Awake();
speedRate = 100;
atkNum = 0;
checkDashTimeTemp = checkDashTime;
setState(State.idle);
RBD = GetComponent<Rigidbody2D>();
for (int i = 1; i < 4; i++) {
_anim.initAnims("attack/" + i);
}
}
bool movePlayer()
{
Vector2 moveDir = new Vector2(0, 0);
if (Input.GetKey(KeyCode.LeftArrow))
{
moveDir.x += -1;
}
if (Input.GetKey(KeyCode.RightArrow))
{
moveDir.x += 1;
}
if (Input.GetKey(KeyCode.UpArrow))
{
moveDir.y += 1;
}
if (Input.GetKey(KeyCode.DownArrow))
{
moveDir.y += -1;
}
if (dashState)
speedRate = 1000;
else if (objectState == State.attack)
speedRate = 20;
else
speedRate = 100;
if (moveDir != Vector2.zero)
{
RBD.velocity = moveDir * moveSpeed * speedRate / 100;
degree = Mathf.RoundToInt((Mathf.Atan2(moveDir.y,moveDir.x)/Mathf.PI*180f -180 )* -1)/45;
_anim.setDir(degree);
return true;
}
else
{
RBD.velocity = moveDir;
}
return false;
}
}
playerFSM 초기화와 이동키 입력 감지 부분입니다. Rigidbody2D 컴포넌트를 미리 받아두고 이동 시 그만큼 속도를 주는 방법으로 이동을 합니다. 이동하는 방법은 여러가지가 있지만 이번 프로젝트에선 Colider를 최대한 활용하고 싶어 Rigidbody를 이용합니다.
degree = Mathf.RoundToInt((Mathf.Atan2(moveDir.y,moveDir.x)/Mathf.PI*180f -180 )* -1)/45;
_anim.setDir(degree);
움직일때 움직이는 방향을 360도에서 8방향으로 변환 해 애니메이터에게 넘어주는 부분입니다.
Atan2로 먼저 라디안을 구하고 180/파이로 우리가 아는 degree단위로 바꾸면 180~0~-180 범위가 되는데
180을 빼서 0~-180~-360을 만들고, -1을 곱해 0~180~360으로 만듭니다. 이걸 8방향으로 쓰기 위해 45로 나눠 줍니다.
bool attackInput()
{
if (Input.GetKeyDown(KeyCode.Space))
{
atkNum++;
if (atkNum > 3)
atkNum = 1;
return true;
}
return false;
}
IEnumerator idle()
{
do
{
yield return null;
if (attackInput())
{
setState(State.attack,atkNum);
}
else
{
if (movePlayer())
setState(State.move);
}
} while (!newState);
}
여기까지 보면 아시겠지만, attackInput()과 movePlayer 모두 bool 반환값을 가지고, 입력이 되었는지 확인 한 뒤 상태를 바꿀 수 있도록 합니다. "손맛"을 위해 move는 키를 누르고 있어도 되게 하고, attack은 키가 눌렸을때 인식되도록 합니다.
다음은 대쉬기능입니다. 본문과 동 떨어져 있지만, 대쉬만 따로 글을 올리기도 뭐해서 지금 올립니다.
핵심은 대쉬를 인식하는것을 단계별로 나눠서 생각하고, 각 단계가 제대로 진행되었을 때 대쉬로 인식하는것입니다.
void Update() {
dashCount();
}
void dashCount()
{
if (canDash<3)
{
checkDashTimeTemp -= Time.deltaTime;
if (checkDashTimeTemp < 0) {
checkDashTimeTemp = checkDashTime;
canDash = 0;
}
}
}
bool dashPlayer() {//dash페이즈 체크
if (dashState)
return false;
if (canDash == 0)
{
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
if (Input.GetKeyDown(KeyCode.RightArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
if (Input.GetKeyDown(KeyCode.DownArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
}
else if (canDash == 1) {
if (Input.GetKeyUp(KeyCode.LeftArrow))
{
canDash = 2;
}
if (Input.GetKeyUp(KeyCode.RightArrow))
{
canDash = 2;
}
if (Input.GetKeyUp(KeyCode.UpArrow))
{
canDash = 2;
}
if (Input.GetKeyUp(KeyCode.DownArrow))
{
canDash = 2;
}
}
else if (canDash == 2)
{
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
canDash = 3;
return true;
}
if (Input.GetKeyDown(KeyCode.RightArrow))
{
canDash = 3;
return true;
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
canDash = 3;
return true;
}
if (Input.GetKeyDown(KeyCode.DownArrow))
{
canDash = 3;
return true;
}
}
return false;
}
IEnumerator idle()
{
do
{
yield return null;
if (dashPlayer())
{
StartCoroutine(dashTimer());
continue;
}
if (attackInput())
{
setState(State.attack,atkNum);
}
else
{
if (movePlayer())
setState(State.move);
}
} while (!newState);
}
IEnumerator dashTimer() {
dashState = true;
yield return new WaitForSeconds(0.1f);
dashState = false;
canDash = 0;
}
3. 테스트
이때, Rigidbody에서 FreezeRotation Z축 회전 정지를 체크해줍니다. 나중에 원치않는 일이 일어날 수 있습니다. 또, 중력이 적용되는게임이 아님으로 Edit - Project Setting - Physics의 Gravity를 0으로 맞춰줍니다.
애니메이션 이미지보기▼

다음 글에서 다룰 애니메이터 수정을 적용해야 애니메이션이 제대로 나옵니다.



4. 애니메이터 수정
다음 글에서는 이전에 만들었던 애니메이터를 미리 이미지들을 불러와놓고 쓰는 방식으로 수정한 이유와 방법을 적도록 하겠습니다.
풀소스▼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class playerFSM : FSMbase
{
public float attackRange = 1.1f;
public float moveSpeed = 5;
float speedRate;
int atkNum;
int degree;
Rigidbody2D RBD;
bool animEnd;
public float checkDashTime = 0.75f;//대쉬 가능 시간
float checkDashTimeTemp;
int canDash = 0;//dash페이즈 0=기본, 1=처음 눌림, 2=뗌, 3= 다시 눌림(대쉬시작)
bool dashState = false;
// Use this for initialization
void Awake()
{
base.Awake();
speedRate = 100;
atkNum = 0;
checkDashTimeTemp = checkDashTime;
setState(State.idle);
RBD = GetComponent<Rigidbody2D>();
for (int i = 1; i < 4; i++) {
_anim.initAnims("attack/" + i);
}
}
void Update() {
dashCount();
}
void dashCount()
{
if (canDash<3)
{
checkDashTimeTemp -= Time.deltaTime;
if (checkDashTimeTemp < 0) {
checkDashTimeTemp = checkDashTime;
canDash = 0;
}
}
}
bool dashPlayer() {//dash페이즈 체크
if (dashState)
return false;
if (canDash == 0)
{
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
if (Input.GetKeyDown(KeyCode.RightArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
if (Input.GetKeyDown(KeyCode.DownArrow))
{
canDash = 1;
checkDashTimeTemp = checkDashTime;
}
}
else if (canDash == 1) {
if (Input.GetKeyUp(KeyCode.LeftArrow))
{
canDash = 2;
}
if (Input.GetKeyUp(KeyCode.RightArrow))
{
canDash = 2;
}
if (Input.GetKeyUp(KeyCode.UpArrow))
{
canDash = 2;
}
if (Input.GetKeyUp(KeyCode.DownArrow))
{
canDash = 2;
}
}
else if (canDash == 2)
{
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
canDash = 3;
return true;
}
if (Input.GetKeyDown(KeyCode.RightArrow))
{
canDash = 3;
return true;
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
canDash = 3;
return true;
}
if (Input.GetKeyDown(KeyCode.DownArrow))
{
canDash = 3;
return true;
}
}
return false;
}
bool movePlayer()
{
Vector2 moveDir = new Vector2(0, 0);
if (Input.GetKey(KeyCode.LeftArrow))
{
moveDir.x += -1;
}
if (Input.GetKey(KeyCode.RightArrow))
{
moveDir.x += 1;
}
if (Input.GetKey(KeyCode.UpArrow))
{
moveDir.y += 1;
}
if (Input.GetKey(KeyCode.DownArrow))
{
moveDir.y += -1;
}
if (dashState)
speedRate = 1000;
else if (objectState == State.attack)
speedRate = 20;
else
speedRate = 100;
if (moveDir != Vector2.zero)
{
RBD.velocity = moveDir * moveSpeed * speedRate / 100;
degree = Mathf.RoundToInt((Mathf.Atan2(moveDir.y,moveDir.x)/Mathf.PI*180f -180 )* -1)/45;
_anim.setDir(degree);
return true;
}
else
{
RBD.velocity = moveDir;
}
return false;
}
bool attackInput()
{
if (Input.GetKeyDown(KeyCode.Space))
{
atkNum++;
if (atkNum > 3)
atkNum = 1;
return true;
}
return false;
}
IEnumerator idle()
{
do
{
yield return null;
if (dashPlayer())
{
StartCoroutine(dashTimer());
continue;
}
if (attackInput())
{
setState(State.attack,atkNum);
}
else
{
if (movePlayer())
setState(State.move);
}
} while (!newState);
}
IEnumerator dashTimer() {
dashState = true;
yield return new WaitForSeconds(0.1f);
dashState = false;
canDash = 0;
}
IEnumerator move()
{
do
{
yield return null;
if (dashPlayer())
{
StartCoroutine(dashTimer());
continue;
}
if (attackInput())
{
setState(State.attack,atkNum);
}
else
{
if (!movePlayer())
setState(State.idle);
}
} while (!newState);
}
IEnumerator attack()
{
do
{
yield return null;
animEnd =_anim.isEnd();
if (dashPlayer())
{
StartCoroutine(dashTimer());
setState(State.move);
continue;
}
if (movePlayer())
{
if (animEnd) {
setState(State.move);
}
}
else if (animEnd)
{
setState(State.idle);
}
} while (!newState);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class FSMbase : MonoBehaviour
{
public enum State
{
idle = 0,
move,
attack,
dead,
skill,
hited,
};
public State objectState;
public myAnimator _anim;
public bool newState;
public string myPath;
public void Awake()
{
objectState = State.idle;
_anim = GetComponent<myAnimator>();
setAnim();
}
public void OnEnable()
{
StartCoroutine("FSMmain");
}
public virtual void setAnim() {
_anim.setPath(myPath);
_anim.setDir(6);//아래방향으로 초기화
_anim.initAnims(Enum.GetNames(typeof(State)));
}
public virtual void setState(State s)
{
newState = true;
objectState = s;
_anim.setState(objectState.ToString());
}
public virtual void setState(State s,int atkNum)
{
newState = true;
objectState = s;
_anim.setState(objectState.ToString()+"/"+atkNum);
}
IEnumerator FSMmain()
{
while (true)
{
newState = false;
yield return StartCoroutine(objectState.ToString());
}
}
IEnumerator idle()
{
do
{
yield return null;
} while (!newState);
}
IEnumerator move()
{
do
{
yield return null;
} while (!newState);
}
}
'게임제작일지 > 던전앤인챈트(완)' 카테고리의 다른 글
던전앤인챈트 개발 #1 커스텀 애니메이터 제작 (0) | 2020.03.12 |
---|