파일을 저장하는데 이름을 적고 확장자를 xml로 바꾸고 인코딩 방식을 UTF-8 코드로 하고 저장합니다
저장하면 모양이 아래 그림과 같이 됩니다 Windows 7 환경
저장한 파일을 유니티에 Resources폴더를 만들고 드래그하여 옮깁니다
하이 라키 뷰에 XMLManager 오브젝트를 만들고 XMLManager 스크립트를 만들어 붙입니다
XMLManager 스크립트 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;
using System;
public class XMLManager : MonoBehaviour
{
public static XMLManager instance;
//xml 파일
public TextAsset enemyFileXml;
//여러개의 변수들을 넣어서 구조체 하나를 한개의 상자처럼 간주하고 사용할수 있음
struct MonParams
{
//xml 파일로 부터 각각의 몬스터의 대해서 이들 파라미터 값을 읽어 들이고 구조체 내부 변수에 저장하고 구조체를 이용하여 각 몬스터에게 파라미터 값으 전달함
public string name;
public int level;
public int maxHp;
public int attackMin;
public int attackMax;
public int defense;
public int exp;
public int rewardMoney;
}
//딕셔너리의 키값으로 적의이름을 사용할 예정이므로 string타입으로 하고 데이터 값으로는 구조체를 이용함 MonParams로 지정
Dictionary<string, MonParams> dicMonsters = new Dictionary<string, MonParams>();
void Awake()
{
if (instance == null)
{
instance = this;
}
}
private void Start()
{
MakeMonsterXML();
}
//XML로부터 파라미터 값 읽어 들이기
void MakeMonsterXML()
{
XmlDocument monsterXMLDoc = new XmlDocument();
monsterXMLDoc.LoadXml(enemyFileXml.text);
XmlNodeList monsterNodeList = monsterXMLDoc.GetElementsByTagName("row");
//노드 리스트로부터 각각의 노드를 뽑아냄
foreach ( XmlNode monsterNode in monsterNodeList)
{
MonParams monParams = new MonParams();
foreach (XmlNode childNode in monsterNode.ChildNodes)
{
if (childNode.Name == "name")
{
//<name>smallspider</name>
monParams.name = childNode.InnerText;
}
if (childNode.Name == "level")
{
//<level>1</level> Int16.Parse() 은 문자열을 정수로 바꿔줌
monParams.level = Int16.Parse( childNode.InnerText);
}
if (childNode.Name == "maxHp")
{
monParams.maxHp = Int16.Parse(childNode.InnerText);
}
if (childNode.Name == "attackMin")
{
monParams.attackMin = Int16.Parse(childNode.InnerText);
}
if (childNode.Name == "attackMax")
{
monParams.attackMax = Int16.Parse(childNode.InnerText);
}
if (childNode.Name == "defense")
{
monParams.defense = Int16.Parse(childNode.InnerText);
}
if (childNode.Name == "exp")
{
monParams.exp = Int16.Parse(childNode.InnerText);
}
if (childNode.Name == "rewardMoney")
{
monParams.rewardMoney = Int16.Parse(childNode.InnerText);
}
print(childNode.Name + ": " + childNode.InnerText);
}
dicMonsters[monParams.name] = monParams;
}
}
//외부로부터 몬스터의 이름과, EnemyParams 객체를 전달 받아서 해당 이름을 가진 몬스터의
//데이터(XML 에서 읽어 온 데이터)를 전달받은 EnemyParams 객체에 적용하는 역할을 하는 함수
public void LoadMonsterParamsFromXML(string monName, EnemyParams mParams)
{
mParams.level = dicMonsters[monName].level;
mParams.curHp = mParams.maxHp = dicMonsters[monName].maxHp;
mParams.attackMin = dicMonsters[monName].attackMin;
mParams.attackMax = dicMonsters[monName].attackMax;
mParams.defense = dicMonsters[monName].defense;
mParams.exp = dicMonsters[monName].exp;
mParams.rewardMoney = dicMonsters[monName].rewardMoney;
}
void Update()
{
}
}
XMLManager 스크립트에 xml 파일 enemy를 Enemy File Xml에 드래그하여 붙입니다
먼저 ObjectManager 스크립트를 생성하고 하이라키뷰에 Objectmanager 오브젝트를 만든다음 스크립트를 붙힘니다
ObjectManager 스크립트 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectManager : MonoBehaviour
{
public static ObjectManager instance;
public GameObject coinPrefab;
public int initialCoins = 30;
List<GameObject> coins = new List<GameObject>();
private void Awake()
{
if (instance == null)
{
instance = this;
}
MakeCoins();
}
void MakeCoins()
{
for (int i = 0; i < initialCoins; i++)
{
GameObject tempCoin = Instantiate(coinPrefab) as GameObject;
//새로 생성된 코인들이 오브젝트 매니저의 자식 오브젝트로 들어감,하이라키뷰에서 관리가 수월함
tempCoin.transform.parent = transform;
tempCoin.SetActive(false);
coins.Add(tempCoin);
}
}
public void DropCoinToPosition(Vector3 pos, int coinValue)
{
GameObject reusedCoin = null;
for (int i = 0; i < coins.Count; i++)
{
if (coins[i].activeSelf == false)
{
reusedCoin = coins[i];
break;
}
}
if (reusedCoin == null)
{
GameObject newCoin = Instantiate(coinPrefab) as GameObject;
coins.Add(newCoin);
reusedCoin = newCoin;
}
reusedCoin.SetActive(true);
reusedCoin.GetComponent<Coin>().SetCoinValue(coinValue);
reusedCoin.transform.position = new Vector3(pos.x, reusedCoin.transform.position.y, pos.z);
}
// Update is called once per frame
void Update()
{
}
}
작성한 ObjectManager 스크립트에 동전U 프리팹을 ObjectManager coinPrefab에 연결합니다
오늘은 아이템[Coin]을 만들고 플레이어가 Coin 을 획득하는 프로그램을 작성하겠습니다
새 폴더를 만들고 이름을 Coin 이라 하고 동전 파일을 임포트합니다
그림과 같이 임포트 하면 됩니다
동전 맵v 을 선택하여 아래와 같이 속성을 바꿈니다
Materials 폴더에 Material을 생성 하여 그림과 같이 이름을 coin 이라 합니다
새로 만든 Material 에 아래 그림과 같이 Shader를 Unit/Texture를 선택하고 다운 받은 동전맵v 그림을 연결합니다
동전U 를 선택하고 Import Settings 에서 Materials를 선택하여 coin Material을 연결합니다
그리고 동전U를 게임 씬에 드래그하여 올려봄니다
하이라키 에 동전U 오브젝트를 선택하여 아래그림과 같이 tag를 Coin 이라 달고 Spher Collider 와 Rigidbody 를 달고 아래그림과 같이 속성을 변경합니다
새 스크립트를 생성하고 이름을 Coin 이라 합니다
Coin 스크립트를 동전U 오브젝트에 붙힘니다
Coin 스크립트작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Coin : MonoBehaviour
{
public float rotateSpeed = 180f;
[System.NonSerialized]
public int money = 100;
void Start()
{
}
public void SetCoinValue(int money)
{
this.money = money;
}
private void OnTriggerEnter(Collider col)
{
if (col.gameObject.tag == "Player")
{
col.gameObject.GetComponent<PlayerParams>().AddMoney(money);
SoundManager.instance.PlayPickUpSound();
Destroy(gameObject);
}
}
// Update is called once per frame
void Update()
{
transform.Rotate(0f, rotateSpeed * Time.deltaTime, 0f);
}
}
PlayerParams 스크립트를 선택하고 스크립트를 수정합니다
추가
public void AddMoney(int money) { this.money += money; UIManager.instance.UpdatePlayerUI(this); }
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerParams : CharacterParams
{
public string name { get; set; }
public int curExp { get; set; }
public int expToNextLevel { get; set; }
public int money { get; set; }
public override void InitParams()
{
name = "hong";
level = 1;
maxHp = 100;
curHp = maxHp;
attackMin = 10;
attackMax = 40;
defense = 1;
curExp = 0;
expToNextLevel = 100 * level;
money = 0;
isDead = false;
//초기화 할때 헤드업 디스플레이에 플레이어의 이름과 기타 정보들이 제대로 표시되도록함
UIManager.instance.UpdatePlayerUI(this);
}
protected override void UpdateAfterReceiveAttack()
{
base.UpdateAfterReceiveAttack();
UIManager.instance.UpdatePlayerUI(this);
}
public void AddMoney(int money)
{
this.money += money;
UIManager.instance.UpdatePlayerUI(this);
}
}
동전U를 선택하고 Prefabs 폴더에 드래그 드롭하여 프리팹을 만듭니다
게임을 실행하면 동전이 회전 하는지 확인하고 플래이어가 닿으면 소리와 함께 사라지고 Coin 점수가 올라 가는지 확인합니다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyFSM : MonoBehaviour
{
public enum State
{
Idle, //정지
Chase, //추적
Attack, //공격
Dead, //사망
NoState //아무 일도 없는 상태
}
public State currentState = State.Idle;
EnemyParams myParams;
EnemyAni myAni;
Transform player;
PlayerParams playerParams;
CharacterController controller;
float chaseDistance = 5f; // 플레이어를 향해 몬스터가 추적을 시작할 거리
float attackDistance = 2.5f; // 플레이어가 안쪽으로 들오오게 되면 공격을 시작
float reChaseDistance = 3f; // 플레이어가 도망 갈 경우 얼마나 떨어져야 다시 추적
float rotAnglePerSecond = 360f; // 초당 회전 각도
float moveSpeed = 1.3f; // 몬스터의 이동 속도
float attackDelay = 2f;
float attackTimer = 0f;
public ParticleSystem hitEffect;
public GameObject selectMark;
//리스폰 시킬 몬스터를 담을 변수
GameObject myRespawnObj;
//리스폰 오브젝트에서 생성된 몇번째 몬스터에 대한 정보
public int spawnID { get; set; }
//몬스터가 처음 생성될때의 위치를 저장
Vector3 originPos;
void Start()
{
myAni = GetComponent<EnemyAni>();
myParams = GetComponent<EnemyParams>();
myParams.deadEvent.AddListener(CallDeadEvent);
ChangeState(State.Idle, EnemyAni.IDLE);
controller = GetComponent<CharacterController>();
player = GameObject.FindGameObjectWithTag("Player").transform;
playerParams = player.gameObject.GetComponent<PlayerParams>();
hitEffect.Stop();
HideSelection();
}
//몬스터가 리스폰 될때 초기화 상태로 함
public void AddToWorldAgain()
{
// 리스폰 오브젝트에서 처음 생성될때의 위치와 같게 함
transform.position = originPos;
GetComponent<EnemyParams>().InitParams();
GetComponent<BoxCollider>().enabled = true;
}
public void HideSelection()
{
selectMark.SetActive(false);
}
public void ShowSelection()
{
selectMark.SetActive(true);
}
// 몬스터가 어느 리스폰 오브젝트로부터 만들었졋는지에 대한 정보를 전달 받을 함수
public void SetRespawnObj(GameObject respawnObj, int spawnID,Vector3 originPos)
{
myRespawnObj = respawnObj;
this.spawnID = spawnID;
this.originPos = originPos;
}
//몬스터가 죽는 순간 처리 명령어
void CallDeadEvent()
{
ChangeState(State.Dead, EnemyAni.DIE);
player.gameObject.SendMessage("CurrentEnemyDead");
//몬스터가 사망했을때 나는 소리
SoundManager.instance.PlayEnemyDie();
StartCoroutine(RemoveMeFromWorld());
}
IEnumerator RemoveMeFromWorld()
{
yield return new WaitForSeconds(1f);
ChangeState(State.Idle, EnemyAni.IDLE);
//리스폰 오브젝트에 자기 자신을 제거해 달라는 요청
myRespawnObj.GetComponent<RespawnObj>().RemoveMonster(spawnID);
}
public void ShowHitEffect()
{
hitEffect.Play();
}
public void AttackCalculate()
{
playerParams.SetEnemyAttack(myParams.GetRandomAttack());
}
void UpdateState()
{
switch (currentState)
{
case State.Idle:
IdleState();
break;
case State.Chase:
ChaseState();
break;
case State.Attack:
AttackState();
break;
case State.Dead:
DeadState();
break;
case State.NoState:
NoState();
break;
}
}
public void ChangeState(State newState, string aniName)
{
if (currentState == newState)
{
return;
}
currentState = newState;
myAni.ChangeAni(aniName);
}
void IdleState()
{
if (GetDistanceFromPlayer() < chaseDistance)
{
ChangeState(State.Chase, EnemyAni.WALK);
}
}
void ChaseState()
{
//몬스터가 공격 가능 거리 안으로 들어가면 공격 상태
if (GetDistanceFromPlayer() < attackDistance)
{
ChangeState(State.Attack, EnemyAni.ATTACK);
}
else
{
TurnToDestination();
MoveToDestination();
}
}
void AttackState()
{
if (GetDistanceFromPlayer() > reChaseDistance)
{
attackTimer = 0f;
ChangeState(State.Chase, EnemyAni.WALK);
}
else
{
if (attackTimer > attackDelay)
{
transform.LookAt(player.position);
myAni.ChangeAni(EnemyAni.ATTACK);
attackTimer = 0f;
//몬스터가 공격할때 나는 소리
SoundManager.instance.PlayEnemyAttack();
}
attackTimer += Time.deltaTime;
}
}
void DeadState()
{
GetComponent<BoxCollider>().enabled = false;
}
void NoState()
{
}
void TurnToDestination()
{
Quaternion lookRotation = Quaternion.LookRotation(player.position - transform.position);
transform.rotation = Quaternion.RotateTowards(transform.rotation, lookRotation, Time.deltaTime * rotAnglePerSecond);
}
void MoveToDestination()
{
// transform.position = Vector3.MoveTowards(transform.position, player.position, moveSpeed * Time.deltaTime);
//몬스터의 이동을 캐릭터 컨트롤러로 바꿈 몬스터가 전방으로 moveSpeed 만큼의 빠르기로 이동하게 됨
controller.Move(transform.forward * moveSpeed * Time.deltaTime);
}
//플레이어와 거리을 재는 함수
float GetDistanceFromPlayer()
{
float distance = Vector3.Distance(transform.position, player.position);
return distance;
}
// Update is called once per frame
void Update()
{
UpdateState();
}
}
RespawnObj 스크립트를 수정합니다
스크립트의 추가
void SpawnMonster() { for (int i = 0; i <monsters.Length; i++) { //몬스터가 리스폰 될때 초기화 될 함수를 찾아 실행 monsters[i].GetComponent().AddToWorldAgain(); monsters[i].SetActive(true); } }
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RespawnObj : MonoBehaviour
{
List<Transform> spawnPos = new List<Transform>();
GameObject[] monsters;
public GameObject monPrefab;
public int spawnNumber = 1;
public float respawnDelay = 3f;
int deadMonsters = 0;
void Start()
{
MakeSpawnPos();
}
void MakeSpawnPos()
{
//자식의 트랜스폼을 확인하여 태그 붙인(Respawn)을 찾아 리스트(spawnPos)에 넣는다
foreach ( Transform pos in transform)
{
if (pos.tag == "Respawn")
{
spawnPos.Add(pos);
}
}
if (spawnNumber > spawnPos.Count)
{
spawnNumber = spawnPos.Count;
}
monsters = new GameObject[spawnNumber];
MakeMonsters();
}
//프리팹으로 부터 몬스터를 만들어 관리하는 함수
void MakeMonsters()
{
for (int i = 0; i < spawnNumber; i++)
{
GameObject mon = Instantiate(monPrefab, spawnPos[i].position, Quaternion.identity) as GameObject;
mon.GetComponent<EnemyFSM>().SetRespawnObj(gameObject, i, spawnPos[i].position);
mon.SetActive(false);
monsters[i] = mon;
GameManager.instance.AddNewMonsters(mon);
}
}
public void RemoveMonster(int spawnID)
{
//Destroy(monsters[spawnID]);
deadMonsters++;
monsters[spawnID].SetActive(false);
print(spawnID + "monster was killed");
//죽은 몬스터의 숫자가 몬스터 배열의 크기와 같다면 일정시간후에 몬스터를 다시 생성
if (deadMonsters == monsters.Length)
{
StartCoroutine(InitMonsters());
deadMonsters = 0;
}
}
IEnumerator InitMonsters()
{
yield return new WaitForSeconds(respawnDelay);
GetComponent<SphereCollider>().enabled = true;
}
void SpawnMonster()
{
for (int i = 0; i <monsters.Length; i++)
{
//몬스터가 리스폰 될때 초기화 될 함수를 찾아 실행
monsters[i].GetComponent<EnemyFSM>().AddToWorldAgain();
monsters[i].SetActive(true);
}
}
private void OnTriggerEnter(Collider col)
{
if (col.gameObject.tag == "Player")
{
SpawnMonster();
GetComponent<SphereCollider>().enabled = false;
}
}
void Update()
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyFSM : MonoBehaviour
{
public enum State
{
Idle, //정지
Chase, //추적
Attack, //공격
Dead, //사망
NoState //아무 일도 없는 상태
}
public State currentState = State.Idle;
EnemyParams myParams;
EnemyAni myAni;
Transform player;
PlayerParams playerParams;
float chaseDistance = 5f; // 플레이어를 향해 몬스터가 추적을 시작할 거리
float attackDistance = 2.5f; // 플레이어가 안쪽으로 들오오게 되면 공격을 시작
float reChaseDistance = 3f; // 플레이어가 도망 갈 경우 얼마나 떨어져야 다시 추적
float rotAnglePerSecond = 360f; // 초당 회전 각도
float moveSpeed = 1.3f; // 몬스터의 이동 속도
float attackDelay = 2f;
float attackTimer = 0f;
public ParticleSystem hitEffect;
public GameObject selectMark;
//리스폰 시킬 몬스터를 담을 변수
GameObject myRespawnObj;
//리스폰 오브젝트에서 생성된 몇번째 몬스터에 대한 정보
public int spawnID { get; set; }
//몬스터가 처음 생성될때의 위치를 저장
Vector3 originPos;
void Start()
{
myAni = GetComponent<EnemyAni>();
myParams = GetComponent<EnemyParams>();
myParams.deadEvent.AddListener(CallDeadEvent);
ChangeState(State.Idle, EnemyAni.IDLE);
player = GameObject.FindGameObjectWithTag("Player").transform;
playerParams = player.gameObject.GetComponent<PlayerParams>();
hitEffect.Stop();
HideSelection();
}
public void HideSelection()
{
selectMark.SetActive(false);
}
public void ShowSelection()
{
selectMark.SetActive(true);
}
// 몬스터가 어느 리스폰 오브젝트로부터 만들었졋는지에 대한 정보를 전달 받을 함수
public void SetRespawnObj(GameObject respawnObj, int spawnID,Vector3 originPos)
{
myRespawnObj = respawnObj;
this.spawnID = spawnID;
this.originPos = originPos;
}
//몬스터가 죽는 순간 처리 명령어
void CallDeadEvent()
{
ChangeState(State.Dead, EnemyAni.DIE);
player.gameObject.SendMessage("CurrentEnemyDead");
//몬스터가 사망했을때 나는 소리
SoundManager.instance.PlayEnemyDie();
StartCoroutine(RemoveMeFromWorld());
}
IEnumerator RemoveMeFromWorld()
{
yield return new WaitForSeconds(1f);
ChangeState(State.Idle, EnemyAni.IDLE);
//리스폰 오브젝트에 자기 자신을 제거해 달라는 요청
myRespawnObj.GetComponent<RespawnObj>().RemoveMonster(spawnID);
}
public void ShowHitEffect()
{
hitEffect.Play();
}
public void AttackCalculate()
{
playerParams.SetEnemyAttack(myParams.GetRandomAttack());
}
void UpdateState()
{
switch (currentState)
{
case State.Idle:
IdleState();
break;
case State.Chase:
ChaseState();
break;
case State.Attack:
AttackState();
break;
case State.Dead:
DeadState();
break;
case State.NoState:
NoState();
break;
}
}
public void ChangeState(State newState, string aniName)
{
if (currentState == newState)
{
return;
}
currentState = newState;
myAni.ChangeAni(aniName);
}
void IdleState()
{
if (GetDistanceFromPlayer() < chaseDistance)
{
ChangeState(State.Chase, EnemyAni.WALK);
}
}
void ChaseState()
{
//몬스터가 공격 가능 거리 안으로 들어가면 공격 상태
if (GetDistanceFromPlayer() < attackDistance)
{
ChangeState(State.Attack, EnemyAni.ATTACK);
}
else
{
TurnToDestination();
MoveToDestination();
}
}
void AttackState()
{
if (GetDistanceFromPlayer() > reChaseDistance)
{
attackTimer = 0f;
ChangeState(State.Chase, EnemyAni.WALK);
}
else
{
if (attackTimer > attackDelay)
{
transform.LookAt(player.position);
myAni.ChangeAni(EnemyAni.ATTACK);
attackTimer = 0f;
//몬스터가 공격할때 나는 소리
SoundManager.instance.PlayEnemyAttack();
}
attackTimer += Time.deltaTime;
}
}
void DeadState()
{
GetComponent<BoxCollider>().enabled = false;
}
void NoState()
{
}
void TurnToDestination()
{
Quaternion lookRotation = Quaternion.LookRotation(player.position - transform.position);
transform.rotation = Quaternion.RotateTowards(transform.rotation, lookRotation, Time.deltaTime * rotAnglePerSecond);
}
void MoveToDestination()
{
transform.position = Vector3.MoveTowards(transform.position, player.position, moveSpeed * Time.deltaTime);
}
//플레이어와 거리을 재는 함수
float GetDistanceFromPlayer()
{
float distance = Vector3.Distance(transform.position, player.position);
return distance;
}
// Update is called once per frame
void Update()
{
UpdateState();
}
}
RespawnObj 스크립트수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RespawnObj : MonoBehaviour
{
List<Transform> spawnPos = new List<Transform>();
GameObject[] monsters;
public GameObject monPrefab;
public int spawnNumber = 1;
public float respawnDelay = 3f;
int deadMonsters = 0;
void Start()
{
MakeSpawnPos();
}
void MakeSpawnPos()
{
foreach ( Transform pos in transform)
{
if (pos.tag == "Respawn")
{
spawnPos.Add(pos);
}
}
if (spawnNumber > spawnPos.Count)
{
spawnNumber = spawnPos.Count;
}
monsters = new GameObject[spawnNumber];
MakeMonsters();
}
//프리팹으로 부터 몬스터를 만들어 관리하는 함수
void MakeMonsters()
{
for (int i = 0; i < spawnNumber; i++)
{
GameObject mon = Instantiate(monPrefab, spawnPos[i].position, Quaternion.identity) as GameObject;
mon.GetComponent<EnemyFSM>().SetRespawnObj(gameObject, i, spawnPos[i].position);
mon.SetActive(false);
monsters[i] = mon;
GameManager.instance.AddNewMonsters(mon);
}
}
public void RemoveMonster(int spawnID)
{
//Destroy(monsters[spawnID]);
deadMonsters++;
monsters[spawnID].SetActive(false);
print(spawnID + "monster was killed");
//죽은 몬스터의 숫자가 몬스터 배열의 크기와 같다면 일정시간후에 몬스터를 다시 생성
if (deadMonsters == monsters.Length)
{
StartCoroutine(InitMonsters());
deadMonsters = 0;
}
}
IEnumerator InitMonsters()
{
yield return new WaitForSeconds(respawnDelay);
GetComponent<SphereCollider>().enabled = true;
}
void SpawnMonster()
{
for (int i = 0; i <monsters.Length; i++)
{
monsters[i].SetActive(true);
}
}
private void OnTriggerEnter(Collider col)
{
if (col.gameObject.tag == "Player")
{
SpawnMonster();
GetComponent<SphereCollider>().enabled = false;
}
}
void Update()
{
}
}
빈 폴더를 생성하고 이름을 Texture 라 합니다
Texture 폴더를 선택하고 Import new Asset 를 눌러서 텍스트를 선택하고 임포트합니다
임포트한 텍스트를 선택하고 속성을 Texture type 을 Sprite and UI 로 바꾸고 아래 그림과 같이 바꾼다
비활성이였던 Enemy 오브젝트를 활성한 다음 Enemy 오브젝트를 선택하고 자식으로 빈 오브젝트를 만듬니다 그리고 이름을 SelectMarkPos 라 하고 자식으로 Quad를 생성합니다
빈 폴더를 생성하여 이름을 Materials 라 하고 폴더 안에 material을 생성하고 이름을 Selection이라 합니다
Selection 메터리얼을 선택하고 속성에서 Shader 를 Unlit/Transparent 를 선택하고 임포트한 텍스쳐를 드래그하여 그림과같이 올려 놓습니다
아래 그림과 같이 Enemy를 선택하고 Selection 메트리얼 을 드래그하여 자식으로 있는 Quad 에 드래그하여 올려 놓습니다.그리고 그림과 같이 Quad의 위치와 각도를 몬스터의 아래 방향을 향하도록 변경합니다
그리고 Enemy 오브젝트를 선택하고 수정한 EnemyFSM 스크립트의 Select Mark 부분에 Quad 오브젝트를 드래그 드롭 합니다 그리고 프리팹을 다시 저장 하기 위해서 Override 를 눌러서 Apply All 을 합니다
Enemy 오브젝트를 다시 비활성 상태로 돌아 감니다
하이라키에 빈 오브젝트를 만들과 이름을 GameManager 라 합니다
GameManager 스크립트를 생성하고 GameManager 오브젝트에 붙힘니다
GameManager 스크립트작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager instance;
List<GameObject> monsters = new List<GameObject>();
void Awake()
{
if (instance == null)
{
instance = this;
}
}
//외부에서 전달된 몬스터가 기존에 리스트에 보관하고 있는 몬스터와 일치하는지 여부를 체크
public void AddNewMonsters(GameObject mon)
{
//인자로 넘어온 몬스터가 기존의 리스트에 존재하면 sameExist = true 아니면 false
bool sameExist = false;
for (int i = 0; i < monsters.Count; i++)
{
if (monsters[i] == mon)
{
sameExist = true;
break;
}
}
if (sameExist == false)
{
monsters.Add(mon);
}
}
public void RemoveMonster(GameObject mon)
{
foreach (GameObject monster in monsters)
{
if (monster == mon)
{
monsters.Remove(monster);
break;
}
}
}
//현재 플레이어가 클릭한 몬스터만 선택마크가 표시
public void ChangeCurrentTarget(GameObject mon)
{
DeselectAllMonsters();
mon.GetComponent<EnemyFSM>().ShowSelection();
}
public void DeselectAllMonsters()
{
for (int i = 0; i < monsters.Count; i++)
{
monsters[i].GetComponent<EnemyFSM>().HideSelection();
}
}
// Update is called once per frame
void Update()
{
}
}
그리고 PlayerFSM 스크립트를 선택하고 AttackEnemy(GameObject enemy) 함수 부분을 수정합니다.
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class RespawnObj : MonoBehaviour { List<Transform> spawnPos = new List<Transform>(); GameObject[] monsters;
public GameObject monPrefab; public int spawnNumber = 1; public float respawnDelay = 3f;
int deadMonsters = 0; void Start() { MakeSpawnPos(); } void MakeSpawnPos() { foreach ( Transform pos in transform) { if (pos.tag == "Respawn") { spawnPos.Add(pos); } } if (spawnNumber > spawnPos.Count) { spawnNumber = spawnPos.Count; }
monsters = new GameObject[spawnNumber];
MakeMonsters(); }
//프리팹으로 부터 몬스터를 만들어 관리하는 함수 void MakeMonsters() { for (int i = 0; i < spawnNumber; i++) { GameObject mon = Instantiate(monPrefab, spawnPos[i].position, Quaternion.identity) as GameObject; mon.SetActive(false);
monsters[i] = mon; } }
void SpawnMonster() { for (int i = 0; i <monsters.Length; i++) { monsters[i].SetActive(true); } }
Sound 폴더를 선택하고 오른쪽 마우스를 클릭하여 Import New Asset 을 클릭합니다
그리고 아까 만들었던 4개의 음향 파일을 선택하여 임포트합니다
Hierarchy 에 빈오브젝트를 만들고 이름을 SoundManager 라고 합니다
새로운 스크립트를 생성하여 이름을 SoundManager 라 합니다
SoundManager 스크립트 작성
using System.Collections; using System.Collections.Generic; using UnityEngine;
//자동으로 AudioSource GetComponent 부착 [RequireComponent(typeof(AudioSource))] public class SoundManager : MonoBehaviour {
//어디서나 접근할수 있는 정적 변수를 만듬니다 public static SoundManager instance;
AudioSource myAudio;
public AudioClip sndHitEnefmy; public AudioClip sndEnemyAttack; public AudioClip sndPickUp; public AudioClip sndEnemyDie;
private void Awake() { if (instance == null) { instance = this; } } // Start is called before the first frame update void Start() { myAudio = GetComponent(); }
public void PlayHitSound() { myAudio.PlayOneShot(sndHitEnefmy); }
public void PlayEnemyAttack() { myAudio.PlayOneShot(sndEnemyAttack); }
public void PlayEnemyDie() { myAudio.PlayOneShot(sndEnemyDie); }
public void PlayPickUpSound() { myAudio.PlayOneShot(sndPickUp); } // Update is called once per frame void Update() {
} }
작성한 SoundManager 스크립트를 Hierarchy 의 SoundManager 오브젝트에 드래그하여 붙힘니다
아래 사진과 같이 Audio Source 콤퍼너트 가 자동으로 붙는걸 확인합니다
그리고 임포트한 4개의 음향 소스를 SoundManager 오브젝트의 붙힌 SoundManager 스크립트 빈 공간에 드래그 드롭하여 그림과 같이 순서 대로 넣습니다
PlayerFSM 스크립트를 선택하고 수정합니다
// 수정한 부분은 붉은색으로 표시됨니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerFSM : MonoBehaviour { public enum State { Idle, Move, Attack, AttackWait, Dead } //idle 상태를 기본 상태로 지정 public State currentState = State.Idle;
//마우수 클릭 지점,플레이어가 이동할 목적지의 좌표를 저장할 예정 Vector3 curTargetPos;
GameObject curEnemy;
public float rotAnglePerSecond = 360f; //1초에 플레이어의 방향을 360도 회전한다
public float moveSpeed = 2f; //초당 2미터의 속도로 이동
float attackDelay = 2f; // 공격을 한번하고 다시 공격할때 까지의 지연시간
float attackTimer = 0f; //공격을 하고 난 뒤에 경과되는 시간을 계산하기 위한 변수
float attackDistance = 1.5f; // 공격 거리 (적과의 거리)
float chaseDistance = 2.5f; // 전투 중 적이 도망가면 다시 추적을 시작 하기 위한 거리
//transform.LookAt(목표지점 위치) 목표지점을 향해 오브젝트를 회전 시키는 함수 transform.LookAt(curTargetPos); ChangeState(State.AttackWait, PlayerAni.ANI_ATKIDLE); } void AttackWaitState() { if (attackTimer > attackDelay) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK);
if (curEnemy == null) { //플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } else if (Vector3.Distance(transform.position,curTargetPos) < attackDistance)//Vector3.Distance(A,B):A와B사이의 거리 { ChangeState(State.Attack, PlayerAni.ANI_ATTACK); } } void Update() { UpdateState(); } }
EnemyFSM을 선택하고 수정합니다
EnemyFSM을 선택하고 수정
//수정한 부분은 붉은색으로 표시
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class EnemyFSM : MonoBehaviour { public enum State { Idle, //정지 Chase, //추적 Attack, //공격 Dead, //사망 NoState //아무 일도 없는 상태 }
public State currentState = State.Idle;
EnemyParams myParams;
EnemyAni myAni;
Transform player;
PlayerParams playerParams;
float chaseDistance = 5f; // 플레이어를 향해 몬스터가 추적을 시작할 거리 float attackDistance = 2.5f; // 플레이어가 안쪽으로 들오오게 되면 공격을 시작 float reChaseDistance = 3f; // 플레이어가 도망 갈 경우 얼마나 떨어져야 다시 추적
float rotAnglePerSecond = 360f; // 초당 회전 각도 float moveSpeed = 1.3f; // 몬스터의 이동 속도
void ChaseState() { //몬스터가 공격 가능 거리 안으로 들어가면 공격 상태 if (GetDistanceFromPlayer() < attackDistance) { ChangeState(State.Attack, EnemyAni.ATTACK); } else { TurnToDestination(); MoveToDestination(); } }
Canvas 의 UIManager 에 각각 의 빈곳에 PlayerName, PalyerMoney, PlayerHPBar 에 드래그 드롭합니다
PlayerParams스크립트를 선택하고 수정합니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerParams : CharacterParams { public string name { get; set; } public int curExp { get; set; } public int expToNextLevel { get; set; } public int money { get; set; }
먼저 AniEventControl 스크립트를 만들고 Enemy 오브젝트에 자식으로 되어있는 Spider 오브젝트에 스크립트를 붙힘니다
AniEventControl 스크립트 작성
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class AniEventControl : MonoBehaviour { // Start is called before the first frame update void Start() {
}
public void SendAttackEnemy() { transform.parent.gameObject.SendMessage("AttackCalculate"); } // Update is called once per frame void Update() {
} }
그리고 Spider 오브젝트를 선택하고 Animation 을 들어갑니다
읽기전용으로 되어 있는 attack1을 바꿔줘야 합니다
attack1 애니메이션을 Ctrl + D를 눌러서 복사하고 이름을 attack1_new 로 바꾸어서 그것을 드래그하여 animaton에 등록 합니다
그리고 Spider 오브젝트를 선택하고 Window ->Animation ->Animation 을 열고 attack1_new 애니메이션에 공격 타이밍에 Add event 를 누른 다음 Function 을 SendAttackEnemy() 함수를 등록합니다
그리고 EnemyAni 스크립트를 선택하여 수정합니다
attack1 이라 되어 있던 이름을 attack1_new 로 바꿈니다
플레이어 HP가 공격을 받고 0이되었을때 사망처리 하기 위해서
그리고 PlayerFSM 스크립트를 열고 수정합니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerFSM : MonoBehaviour { public enum State { Idle, Move, Attack, AttackWait, Dead } //idle 상태를 기본 상태로 지정 public State currentState = State.Idle;
//마우수 클릭 지점,플레이어가 이동할 목적지의 좌표를 저장할 예정 Vector3 curTargetPos;
GameObject curEnemy;
public float rotAnglePerSecond = 360f; //1초에 플레이어의 방향을 360도 회전한다
public float moveSpeed = 2f; //초당 2미터의 속도로 이동
float attackDelay = 2f; // 공격을 한번하고 다시 공격할때 까지의 지연시간
float attackTimer = 0f; //공격을 하고 난 뒤에 경과되는 시간을 계산하기 위한 변수
float attackDistance = 1.5f; // 공격 거리 (적과의 거리)
float chaseDistance = 2.5f; // 전투 중 적이 도망가면 다시 추적을 시작 하기 위한 거리
//transform.LookAt(목표지점 위치) 목표지점을 향해 오브젝트를 회전 시키는 함수 transform.LookAt(curTargetPos); ChangeState(State.AttackWait, PlayerAni.ANI_ATKIDLE); } void AttackWaitState() { if (attackTimer > attackDelay) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK);
if (curEnemy == null) { //플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } else if (Vector3.Distance(transform.position,curTargetPos) < attackDistance)//Vector3.Distance(A,B):A와B사이의 거리 { ChangeState(State.Attack, PlayerAni.ANI_ATTACK); } } void Update() { UpdateState(); } }
게임을 실행시키고 몬스터가 플레이어를 공격하면 플레이어의 HP 가 줄고 0이되면 사망 처리 되는 것을 확인 한다
유니티가 제공하는 유니티 이벤트 기능인 UnityEvent 를 만들고 AddListenr() 함수를 등록해 보겠습니다
플레이어가 몬스터를 공격하였을때 몬스터가 죽으면 몬스터의 죽는 순간의 이벤트를 처리하고
몬스터가 죽음과 동시에 플레이어의 행동을 다시 Idle로 변환하는 SendMessage() 를 처리하여 보겠습니다
CharackterParams 스크립트를 선택하고 스크립트를 수정합니다
//CharackterParams 스크립트수정
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events;//유니티 이벤트를 사용하기 위해서는 네임스페이스를 추가해야함
//CharacterParams는 플레이어의 파라미터 클래스와 몬스터 파라미터 클래스의 부모 클래스 역할을 하게됨 public class CharacterParams : MonoBehaviour { //퍼블릭 변수가 아니라 약식프로퍼티,속성으로 지정 //퍼블릭 변수와 똑같이 사용할수 있지만 유니티 인스펙터에 노출되는 것을 막고 보안을 위해정식 프로퍼티로 전환이 쉬워짐 public int level { get; set; } public int maxHp { get; set; } public int curHp { get; set; } public int attackMin { get; set; } public int attackMax { get; set; } public int defense { get; set; } public bool isDead { get; set; }
[System.NonSerialized] public UnityEvent deadEvent = new UnityEvent();
void Start() { InitParams(); }
//나중에 CharacterParams 클래스를 상속한 자식클래스 에서 //InitParams 함수 에 자신만의 명령어를 추가하기만 하면 자동으로 필요한 명령어들이 실행 public virtual void InitParams() {
}
public int GetRandomAttack() { int randAttack = Random.Range(attackMin, attackMax + 1); return randAttack; }
public void SetEnemyAttack(int enemyAttackPower) { curHp -= enemyAttackPower; UpdateAfterReceiveAttack(); }
//캐릭터가 적으로 부터 공격을 받은 뒤에 자동으로 실행될 함수를 가상함수로 만듬 protected virtual void UpdateAfterReceiveAttack() { print(name + "'s HP: " + curHp);
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class EnemyFSM : MonoBehaviour { public enum State { Idle, //정지 Chase, //추적 Attack, //공격 Dead, //사망 NoState //아무 일도 없는 상태 }
public State currentState = State.Idle;
EnemyParams myParams;
EnemyAni myAni;
Transform player;
PlayerParams playerParams;
float chaseDistance = 5f; // 플레이어를 향해 몬스터가 추적을 시작할 거리 float attackDistance = 2.5f; // 플레이어가 안쪽으로 들오오게 되면 공격을 시작 float reChaseDistance = 3f; // 플레이어가 도망 갈 경우 얼마나 떨어져야 다시 추적
float rotAnglePerSecond = 360f; // 초당 회전 각도 float moveSpeed = 1.3f; // 몬스터의 이동 속도
void ChaseState() { //몬스터가 공격 가능 거리 안으로 들어가면 공격 상태 if (GetDistanceFromPlayer() < attackDistance) { ChangeState(State.Attack, EnemyAni.ATTACK); } else { TurnToDestination(); MoveToDestination(); } }
// Update is called once per frame void Update() { UpdateState(); } }
PlayerFSM 스크립트를 선택하고 수정합니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerFSM : MonoBehaviour { public enum State { Idle, Move, Attack, AttackWait, Dead } //idle 상태를 기본 상태로 지정 public State currentState = State.Idle;
//마우수 클릭 지점,플레이어가 이동할 목적지의 좌표를 저장할 예정 Vector3 curTargetPos;
GameObject curEnemy;
public float rotAnglePerSecond = 360f; //1초에 플레이어의 방향을 360도 회전한다
public float moveSpeed = 2f; //초당 2미터의 속도로 이동
float attackDelay = 2f; // 공격을 한번하고 다시 공격할때 까지의 지연시간
float attackTimer = 0f; //공격을 하고 난 뒤에 경과되는 시간을 계산하기 위한 변수
float attackDistance = 1.5f; // 공격 거리 (적과의 거리)
float chaseDistance = 2.5f; // 전투 중 적이 도망가면 다시 추적을 시작 하기 위한 거리
//transform.LookAt(목표지점 위치) 목표지점을 향해 오브젝트를 회전 시키는 함수 transform.LookAt(curTargetPos); ChangeState(State.AttackWait, PlayerAni.ANI_ATKIDLE); } void AttackWaitState() { if (attackTimer > attackDelay) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK);
if (curEnemy == null) { //플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } else if (Vector3.Distance(transform.position,curTargetPos) < attackDistance)//Vector3.Distance(A,B):A와B사이의 거리 { ChangeState(State.Attack, PlayerAni.ANI_ATTACK); } } void Update() { UpdateState(); } }
게임을 실행하여 몬스터가 HP 를 소모 하면 죽는 행동과 플레이어가 IDLE 상태로 변하는 것을 확인 합니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
//CharacterParams는 플레이어의 파라미터 클래스와 몬스터 파라미터 클래스의 부모 클래스 역할을 하게됨 public class CharacterParams : MonoBehaviour { //퍼블릭 변수가 아니라 약식프로퍼티,속성으로 지정 //퍼블릭 변수와 똑같이 사용할수 있지만 유니티 인스펙터에 노출되는 것을 막고 보안을 위해정식 프로퍼티로 전환이 쉬워짐 public int level { get; set; } public int maxHp { get; set; } public int curHp { get; set; } public int attackMin { get; set; } public int attackMax { get; set; } public int defense { get; set; } public bool isDead { get; set; }
void Start() { InitParams(); }
//나중에 CharacterParams 클래스를 상속한 자식클래스 에서 //InitParams 함수 에 자신만의 명령어를 추가하기만 하면 자동으로 필요한 명령어드이 실행 public virtual void InitParams() {
}
public int GetRandomAttack() { int randAttack = Random.Range(attackMin, attackMax + 1); return randAttack; }
public void SetEnemyAttack(int enemyAttackPower) { curHp -= enemyAttackPower; UpdateAfterReceiveAttack(); }
//캐릭터가 적으로 부터 공격을 받은 뒤에 자동으로 실행될 함수를 가상함수로 만듬 protected virtual void UpdateAfterReceiveAttack() { print(name + "'s HP: " + curHp); }
}
그리고 새로운 스크립트를 생성하고 이름을 EnemyParams라 합니다
EnemyParams 스크립트 작성
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class EnemyParams : CharacterParams { public string name; public int exp { get; set; } public int rewardMoney { get; set; } public override void InitParams() { name = "Monster"; level = 1; maxHp = 50; curHp = maxHp; attackMin = 2; attackMax = 5; defense = 1;
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerParams : CharacterParams { public string name { get; set; } public int curExp { get; set; } public int expToNextLevel { get; set; } public int money { get; set; }
Player 오브젝트를 선택하고 PlayerParams 스크립트를 Player 에 붙힘니다
Enemy 오브젝트를 선택하고 EnemyParams 스크립트를 Enemy에 붙힘니다
그리고 PlayerFSM 스크립트를 선택하고 수정하겠습니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerFSM : MonoBehaviour { public enum State { Idle, Move, Attack, AttackWait, Dead } //idle 상태를 기본 상태로 지정 public State currentState = State.Idle;
//마우수 클릭 지점,플레이어가 이동할 목적지의 좌표를 저장할 예정 Vector3 curTargetPos;
GameObject curEnemy;
public float rotAnglePerSecond = 360f; //1초에 플레이어의 방향을 360도 회전한다
public float moveSpeed = 2f; //초당 2미터의 속도로 이동
float attackDelay = 2f; // 공격을 한번하고 다시 공격할때 까지의 지연시간
float attackTimer = 0f; //공격을 하고 난 뒤에 경과되는 시간을 계산하기 위한 변수
float attackDistance = 1.5f; // 공격 거리 (적과의 거리)
float chaseDistance = 2.5f; // 전투 중 적이 도망가면 다시 추적을 시작 하기 위한 거리
//transform.LookAt(목표지점 위치) 목표지점을 향해 오브젝트를 회전 시키는 함수 transform.LookAt(curTargetPos); ChangeState(State.AttackWait, PlayerAni.ANI_ATKIDLE); } void AttackWaitState() { if (attackTimer > attackDelay) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK);
//MoveTo(캐릭터가 이동할 목표 지점의 좌표) public void MoveTo(Vector3 tPos) { curEnemy = null; curTargetPos = tPos; ChangeState(State.Move, PlayerAni.ANI_WALK); }
void TurnToDestination() { // Quaternion lookRotation(회전할 목표 방향) : 목표 방향은 목적지 위치에서 자신의 위치를 빼면 구함 Quaternion lookRotation = Quaternion.LookRotation(curTargetPos - transform.position);
if (curEnemy == null) { //플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } else if (Vector3.Distance(transform.position,curTargetPos) < attackDistance)//Vector3.Distance(A,B):A와B사이의 거리 { ChangeState(State.Attack, PlayerAni.ANI_ATTACK); } } void Update() { UpdateState(); } }
게임을 실행하고 플레이어가 적을 공격하였을때 콘솔창에 적의 HP 가 깍이는 것을 확인합니다
복사본 이름을 Hikick_new로 바꾸고 Animator에 들어가서 attack에 복사본을 motion에 드래그해서 바꾸어 놓습니다
PlayerFSM을 선택하고 수정합니다
PlayerFSM 함수 추가
public class PlayerFSM : MonoBehaviour { public enum State { Idle, Move, Attack, AttackWait, Dead } //idle 상태를 기본 상태로 지정 public State currentState = State.Idle;
//마우수 클릭 지점, 플레이어가 이동할 목적지의 좌표를 저장할 예정 Vector3 curTargetPos;
GameObject curEnemy;
public float rotAnglePerSecond = 360f; //1초에 플레이어의 방향을 360도 회전한다
public float moveSpeed = 2f; //초당 2미터의 속도로 이동
float attackDelay = 2f; // 공격을 한번 하고 다시 공격할 때까지의 지연시간
float attackTimer = 0f; //공격을 하고 난 뒤에 경과되는 시간을 계산하기 위한 변수
float attackDistance = 1.5f; // 공격 거리 (적과의 거리)
float chaseDistance = 2.5f; // 전투 중 적이 도망가면 다시 추적을 시작하기 위한 거리
public void AttackCalculate() { if (curEnemy == null) { return; } curEnemy.GetComponent<EnemyFSM>().ShowHitEffect(); }
// 적을 공격하기 위한 함수 public void AttackEnemy(GameObject enemy) { if (curEnemy != null && curEnemy == enemy) { return; } curEnemy = enemy; curTargetPos = curEnemy.transform.position;
//캐릭터의 상태가 바뀌면 어떤 일이 일어날지 를 미리 정의 void UpdateState() { switch (currentState) { case State.Idle: IdleState(); break; case State.Move: MoveState(); break; case State.Attack: AttackState(); break; case State.AttackWait: AttackWaitState(); break; case State.Dead: DeadState(); break; default: break; } } void IdleState() { } void MoveState() { TurnToDestination(); MoveToDestination(); } void AttackState() { attackTimer = 0f;
//transform.LookAt(목표지점 위치) 목표지점을 향해 오브젝트를 회전시키는 함수 transform.LookAt(curTargetPos); ChangeState(State.AttackWait, PlayerAni.ANI_ATKIDLE); } void AttackWaitState() { if (attackTimer > attackDelay) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK);
//MoveTo(캐릭터가 이동할 목표 지점의 좌표) public void MoveTo(Vector3 tPos) { curEnemy = null; curTargetPos = tPos; ChangeState(State.Move, PlayerAni.ANI_WALK); }
void TurnToDestination() { // Quaternion lookRotation(회전할 목표 방향) : 목표 방향은 목적지 위치에서 자신의 위치를 빼면 구함 Quaternion lookRotation = Quaternion.LookRotation(curTargetPos - transform.position);
//Quaternion.RotateTowards(현재의 rotation값, 최종 목표 rotation 값, 최대 회전각) transform.rotation = Quaternion.RotateTowards(transform.rotation, lookRotation, Time.deltaTime * rotAnglePerSecond); }
if (curEnemy == null) { //플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } else if (Vector3.Distance(transform.position,curTargetPos) < attackDistance)//Vector3.Distance(A,B):A와B사이의 거리 { ChangeState(State.Attack, PlayerAni.ANI_ATTACK); } } void Update() { UpdateState(); } }
그리고 Animation을 활성화시켜서 Player 공격하는 시점 프레임에 Add Event를 추가시켜 AttackCalculate() 함수를 등록시킵니다.
Asset Store에 들어가서 Simple Partice Pack을 다운로드합니다
Simpe Particle Pack에서 Explosions에 Burst를 임포트 합니다
Enemy 오브젝트를 선택하고 자식으로 빈 오브젝트를 만들고 이름을 EffectPos 라 하고 임포트 한 Burst 프리 팹을 찾아 드래그해서 자식으로 놓습니다
그리고 Enemy 오브젝트에 머리 위로 위치시켜 이팩트가 터지면 잘 보이도록 위치시킵니다
EnemyFSM 스크립트를 선택하고 수정합니다
public class EnemyFSM : MonoBehaviour { public enum State { Idle, //정지 Chase, //추적 Attack, //공격 Dead, //사망 NoState //아무 일도 없는 상태 }
public State currentState = State.Idle;
EnemyAni myAni;
Transform player;
float chaseDistance = 5f; // 플레이어를 향해 몬스터가 추적을 시작할 거리 float attackDistance = 2.5f; // 플레이어가 안쪽으로 들오오 게 되면 공격을 시작 float reChaseDistance = 3f; // 플레이어가 도망갈 경우 얼마나 떨어져야 다시 추적
float rotAnglePerSecond = 360f; // 초당 회전 각도 float moveSpeed = 1.3f; // 몬스터의 이동 속도
void ChaseState() { //몬스터가 공격 가능 거리 안으로 들어가면 공격 상태 if (GetDistanceFromPlayer() < attackDistance) { ChangeState(State.Attack, EnemyAni.ATTACK); } else { TurnToDestination(); MoveToDestination(); } }
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class EnemyFSM : MonoBehaviour { public enum State { Idle, //정지 Chase, //추적 Attack, //공격 Dead, //사망 NoState //아무 일도 없는 상태 }
public State currentState = State.Idle;
EnemyAni myAni;
Transform player;
float chaseDistance = 5f; // 플레이어를 향해 몬스터가 추적을 시작할 거리 float attackDistance = 2.5f; // 플레이어가 안쪽으로 들오오게 되면 공격을 시작 float reChaseDistance = 3f; // 플레이어가 도망 갈 경우 얼마나 떨어져야 다시 추적
float rotAnglePerSecond = 360f; // 초당 회전 각도 float moveSpeed = 1.3f; // 몬스터의 이동 속도
void ChaseState() { //몬스터가 공격 가능 거리 안으로 들어가면 공격 상태 if (GetDistanceFromPlayer() < attackDistance) { ChangeState(State.Attack, EnemyAni.ATTACK); } else { TurnToDestination(); MoveToDestination(); } }
// Update is called once per frame void Update() { UpdateState(); } }
그리고 EnemyAni 스크립트를 만들고 Enemy 오브젝트에 붙힘니다
EnemyAni 스크립트 작성
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class EnemyAni : MonoBehaviour { public const string IDLE = "idle"; public const string WALK = "walk"; public const string ATTACK = "attack1"; public const string DIE = "death1";
Animation anim; // Start is called before the first frame update void Start() { anim = GetComponentInChildren<Animation>(); }
public void ChangeAni(string aniName) { anim.CrossFade(aniName); } // Update is called once per frame void Update() {
} }
게임을 실행시켜서 player가 적에게 다가갔을때 적이 공격하고 Player가 달아 났을 때 적이 추적하는지 확인 합니다
//transform.LookAt(목표지점 위치) 목표지점을 향해 오브젝트를 회전 시키는 함수 transform.LookAt(curTargetPos); ChangeState(State.AttackWait, PlayerAni.ANI_ATKIDLE); }
void AttackWaitState() { if (attackTimer > attackDelay) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK);
}
attackTimer += Time.deltaTime; }
void DeadState() {
}
//MoveTo(캐릭터가 이동할 목표 지점의 좌표) public void MoveTo(Vector3 tPos) { curEnemy = null; curTargetPos = tPos; ChangeState(State.Move, PlayerAni.ANI_WALK); }
void TurnToDestination() { // Quaternion lookRotation(회전할 목표 방향) : 목표 방향은 목적지 위치에서 자신의 위치를 빼면 구함 Quaternion lookRotation = Quaternion.LookRotation(curTargetPos - transform.position);
if (curEnemy == null) { //플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } else if (Vector3.Distance(transform.position,curTargetPos) < attackDistance) { ChangeState(State.Attack, PlayerAni.ANI_ATTACK); }
} void Update() { UpdateState(); } }
InputManager 오브젝트를 선택하고 InputManager 스크립트를 수정합니다
//InputManager 스크립트 수정
public class InputManager : MonoBehaviour {
GameObject player; // Start is called before the first frame update void Start() { player = GameObject.FindGameObjectWithTag("Player"); }
void CheckClick() { if (Input.GetMouseButtonDown(0)) { //카메라로부터 화면사의 좌표를 관통하는 가상의 선(레이)을 생성해서 리턴해 주는 함수 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
//Physics.Raycast(래이 타입 변수, out 레이캐스트 히트 타입변수) : //가상의 레이저선(래이)이 충돌체와 충돌하면, true(참) 값을 리턴하면서 동시에 레이캐스트 히트 변수에 충돌 대상의 정보를 담아 주는 함수 if (Physics.Raycast(ray, out hit)) { if (hit.collider.gameObject.name == "Terrain") { // player.transform.position = hit.point;
//마우스 클릭 지점의 좌표를 플레이어가 전달받은뒤,상태를 이동상태로 바뀜 player.GetComponent<PlayerFSM>().MoveTo(hit.point); } else if (hit.collider.gameObject.tag == "Enemy")//마우스 클릭한 대상이 적 캐릭터인 경우 { player.GetComponent<PlayerFSM>().AttackEnemy(hit.collider.gameObject); } } }
} // Update is called once per frame void Update() { CheckClick(); } }
//MoveTo(캐릭터가 이동할 목표 지점의 좌표) public void MoveTo(Vector3 tPos) { curTargetPos = tPos; ChangeState(State.Move, PlayerAni.ANI_WALK); }
void TurnToDestination() { // Quaternion lookRotation(회전할 목표 방향) : 목표 방향은 목적지 위치에서 자신의 위치를 빼면 구함 Quaternion lookRotation = Quaternion.LookRotation(curTargetPos - transform.position);
//Quaternion.RotateTowards(현재의 rotation값, 최종 목표 rotation 값, 최대 회전각) transform.rotation = Quaternion.RotateTowards(transform.rotation, lookRotation, Time.deltaTime * rotAnglePerSecond); }
//플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } void Update() { UpdateState(); } }
CameraControl 스크립트를 새로 만들고 CameraControl 스크립트를 Main Camera에 붙입니다
Main Camera에 붙입니다
//CameraControl 스크립트 작성
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class CameraControl : MonoBehaviour { public Transform player;
Vector3 offset; // Start is called before the first frame update void Start() { offset = player.position - transform.position; }
//카메라가 플레이어 움직임에 한 템포 늦게 움직임을 준다 void LateUpdate() { //플레이어의 위치와 카메라의 위치를 최초 저장한 위치 차이만큼 자동으로 유지시켜주게 됨 transform.position = player.position - offset; } }
CameraControl 스크립트 작성하고 Player 오브젝트를 드래그하여 inspector 변수에 넣습니다
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class InputManager : MonoBehaviour {
GameObject player; // Start is called before the first frame update void Start() { player = GameObject.FindGameObjectWithTag("Player"); }
void CheckClick() { if (Input.GetMouseButtonDown(0)) { //카메라로부터 화면사의 좌표를 관통하는 가상의 선(레이)을 생성해서 리턴해 주는 함수 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
//Physics.Raycast(래이 타입 변수, out 레이캐스트 히트 타입변수) : //가상의 레이저선(래이)이 충돌체와 충돌하면, true(참) 값을 리턴하면서 동시에 레이캐스트 히트 변수에 충돌 대상의 정보를 담아 주는 함수 if (Physics.Raycast(ray, out hit)) { if (hit.collider.gameObject.name == "Terrain") { // player.transform.position = hit.point;
//마우스 클릭 지점의 좌표를 플레이어가 전달받은뒤,상태를 이동상태로 바뀜 player.GetComponent<PlayerFSM>().MoveTo(hit.point);
} } }
} // Update is called once per frame void Update() { CheckClick(); } }
PlayerFSM 스크립트를 선택하고 스크립트를 엽니다
그리고 수정합니다
//PlayerFSM 스크립트 수정
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerFSM : MonoBehaviour { public enum State { Idle, Move, Attack, AttackWait, Dead } //idle 상태를 기본 상태로 지정 public State currentState = State.Idle;
//마우수 클릭 지점,플레이어가 이동할 목적지의 좌표를 저장할 예정 Vector3 curTargetPos;
public float rotAnglePerSecond = 360f; //1초에 플레이어의 방향을 360도 회전한다
//캐릭터의 상태가 바뀌면 어떤 일이 일어날지 를 미리 정의 void UpdateState() { switch (currentState) { case State.Idle: break; case State.Move: TurnToDestination(); MoveToDestination(); break; case State.Attack: break; case State.AttackWait: break; case State.Dead: break; default: break; }
}
//MoveTo(캐릭터가 이동할 목표 지점의 좌표) public void MoveTo(Vector3 tPos) { curTargetPos = tPos; ChangeState(State.Move, PlayerAni.ANI_WALK); }
void TurnToDestination() { // Quaternion lookRotation(회전할 목표 방향) : 목표 방향은 목적지 위치에서 자신의 위치를 빼면 구함 Quaternion lookRotation = Quaternion.LookRotation(curTargetPos - transform.position);
//플레이어의 위치와 목표지점의 위치가 같으면, 상태를 Idle 상태로 바꾸라는 명령 if (transform.position == curTargetPos) { ChangeState(State.Idle, PlayerAni.ANI_IDLE); } } void Update() { UpdateState(); } }
스크립트를 수정하고 플레이 버튼을 눌러서 플레이어가 마우스 클릭지점으로 이동하는지 확인 합니다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerFSM : MonoBehaviour
{
public enum State
{
Idle,
Move,
Attack,
AttackWait,
Dead
}
//idle 상태를 기본 상태로 지정
public State currentState = State.Idle;
PlayerAni myAni;
void Start()
{
myAni = GetComponent<PlayerAni>();
// myAni.ChangeAni(PlayerAni.ANI_WALK);
ChangeState(State.Idle, PlayerAni.ANI_IDLE);
}
void ChangeState(State newState, int aniNumber)
{
if (currentState == newState)
{
return;
}
myAni.ChangeAni(aniNumber);
currentState = newState;
}
//캐릭터의 상태가 바뀌면 어떤 일이 일어날지 를 미리 정의
void UpdateState()
{
switch (currentState)
{
case State.Idle:
break;
case State.Move:
break;
case State.Attack:
break;
case State.AttackWait:
break;
case State.Dead:
break;
default:
break;
}
}
// Update is called once per frame
void Update()
{
UpdateState();
}
}
//PlayerAni 스크립트 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAni : MonoBehaviour
{
// 애니메이터 컨트롤러의 전이 관계에서 설정한 번호에 맞춤니다
public const int ANI_IDLE = 0;
public const int ANI_WALK = 1;
public const int ANI_ATTACK = 2;
public const int ANI_ATKIDLE = 3;
public const int ANI_DIE = 4;
Animator anim;
void Start()
{
anim = GetComponent<Animator>();
}
//애니메이션 번호를 입력 받아서 플레이어의 애니메이션을 해당되는 애니메이션으로 바꿔주는 함수
public void ChangeAni(int aniNumber)
{
anim.SetInteger("aniName", aniNumber);
}
// Update is called once per frame
void Update()
{
}
}
Scripts폴더 안에 InputManager 새 스크립트를 생성하고 스크립트를 드래그하여 InputManager 오브젝트에 붙입니다
InputManager Scripts 작성
아래와 같이 스크립트를 작성합니다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InputManager : MonoBehaviour
{
GameObject player;
// Start is called before the first frame update
void Start()
{
player = GameObject.FindGameObjectWithTag("Player");
}
void CheckClick()
{
if (Input.GetMouseButtonDown(0))
{
//카메라로부터 화면사의 좌표를 관통하는 가상의 선(레이)을 생성해서 리턴해 주는 함수
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
//Physics.Raycast(래이 타입 변수, out 레이 캐스트 히트 타입변수) :
//가상의 레이저선(래이)이 충돌체와 충돌하면, true(참) 값을 리턴하면서 동시에 레이캐스트 히트 변수에 충 돌 대상의 정보를 담아 주는 함수
if (Physics.Raycast(ray, out hit))
{
if (hit.collider.gameObject.name == "Terrain")
{
player.transform.position = hit.point;
}
}
}
}
// Update is called once per frame
void Update()
{
CheckClick();
}
}