
[ ์น์ 13. ๋ฏธ๋ RPG ]
๏ผ ํ๊ฒฝ์ธํ
- Terrain์ Unity์์ ๊ฐ๋ฐ์๊ฐ ์ง์ ์งํ์ ์ ์ํ ์ ์๋๋ก ์ ๊ณตํ Tool์ด๋ค.
- Terrain์ [ Hierarchy ] - [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] - [ 3D Object ] - [ Terrain ] ์ ํตํด ์์ฑํ๋ค.

- Light์ Type์๋ Realtime, Mixed, Baked๊ฐ ์๋ค.
> Realtime์ ์กฐ๋ช ์ด ์ฌ์ ์ง์ ์ ์ธ ๋น์ ์ ๊ณตํ๊ณ ๋งค ํ๋ ์๋ง๋ค ์ ๋ฐ์ดํธ๋์ด ๊ฒ์ ์ค๋ธ์ ํธ๊ฐ ์ฌ ๋ด์์ ์ด๋์
์กฐ๋ช ์ ์ฆ์ ์ ๋ฐ์ดํธ๋๋ค. (์๋นํ ๋ถํ๋ฅผ ์ ๋ฐ)
> Baked๋ Lightmap์ Bakingํ ๋ Scene์ Static Object์ ๋ํ ์กฐ๋ช ํจ๊ณผ๊ฐ ๊ณ์ฐ๋๊ณ Texture์ ๊ธฐ๋ก๋๋ค.
(์ฆ, ์์ง์ด์ง ์๋ ๋ฌผ์ฒด์ ๋น์ ๋ฏธ๋ฆฌ ๊ณ์ฐํด๋๋ ๊ฒ)
> ๋ฌผ์ฒด๊ฐ ๋ง์์ง์๋ก Baking ์๊ฐ์ด ์๋นํ ์ค๋ ๊ฑธ๋ฆฐ๋ค. ๋ฐ๋ผ์ ๊ฐ๋ฐ ๋์ค์ ๋ฌผ์ฒด๋ฅผ ์ถ๊ฐํ ๋๋ง๋ค ์๋์ผ๋ก Baking
ํ๋ ๊ฒ์ ๋ง๊ธฐ ์ํด [ Window ] - [ Rendering ] - [ Lighting Settings ] - [ Lightmapping Setting ] ์ Auto Generate์
์ฒดํฌ๋ฅผ ํด์ ํ๋ค. (์ต์ ๋ฒ์ ์์ ์๋ฌ ๋ฐ์์ Compress Lightmaps์ ์ฒดํฌ๋ฅผ ํด์ )
๏ผ ์ด๋
- NavMesh๋ Navigation Mesh์ ์ค์๋ง๋ก, ๊ฒ์ ์๋์์ ๊ฑธ์ ์ ์๋ ํ๋ฉด์ ๋ปํ๋ค.
- NavMesh๋ [ Window ] - [ AI ] - [ Navigation ] ์์ ์ค์ ๊ฐ๋ฅํ๋ค.
> [ Bake ] ์์ ๋ฏธ๋ฆฌ ๊ฐ ์ ์๋ ์์ญ ๋๋ ์ ํ ์ฌํญ๋ค์ Bakeํ๋ค.
(๊ณ์ฐ์ ํฌํจ๋๋ Mesh๋ Navigation Static ์์ฑ์ ๊ฐ์ง๋ค.)
- ์ค์ ๋ NavMesh๋ฅผ ์ด์ฉํ์ฌ ๋ชฉ์ ์ง๊น์ง ๊ฒฝ๋ก๋ฅผ ์ฐพ๊ณ ์ด๋ํ๋ ์บ๋ฆญํฐ์ ๋ถ์ฐฉ๋๋ ์ปดํฌ๋ํธ๊ฐ Nav Mesh Agent์ด๋ค.
NavMesh๋ฅผ ์ด์ฉํ PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
//...
void UpdateMoving()
{
Vector3 dir = _destPos - transform.position; // ๋ชฉ์ ์ง๊น์ง์ ๋ฐฉํฅ๋ฒกํฐ๋ฅผ ์ ์ ์๋ค.
if (dir.magnitude < 0.1f) // ๋ง์ฝ ๋ชฉ์ ์ง๊น์ง ๊ฑฐ์ ๋์ฐฉ์ ์๋ฃํ๋ค๋ฉด
{
_state = PlayerState.Idle;
}
else
{
// NavMesh๋ฅผ ์ฌ์ฉ
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
float moveDist = Mathf.Clamp(Time.deltaTime * _speed, 0, dir.magnitude);
nma.Move(dir.normalized * moveDist);
Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green); // Ray๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"))) // ๊ฑด๋ฌผ, ์ฌ๋ฌผ๊ณผ ์ธ์ ์ Player๊ฐ ๋ฉ์ถ๋๋ก ์ค์
{
_state = PlayerState.Idle;
return;
}
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt์ ์ํ ํ์ ์ ๋ณด๋ค ์์ฐ์ค๋ฝ๊ฒ ํ๋๋ก
}
// ์ ๋๋งค์ด์
Animator anim = GetComponent<Animator>();
anim.SetFloat("speed", _speed);
}
//...
}
๏ผ ์คํฏ
์คํฏ ๊ด๋ฆฌ๋ฅผ ์ํ ๊ณต์ฉ Stat Script ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Stat : MonoBehaviour
{
[SerializeField]
protected int _level;
[SerializeField]
protected int _hp;
[SerializeField]
protected int _maxHp;
[SerializeField]
protected int _attack;
[SerializeField]
protected int _defense;
[SerializeField]
protected float _moveSpeed;
public int Level { get { return _level; } set { _level = value; } }
public int Hp { get { return _hp; } set { _hp = value; } }
public int MaxHp { get { return _maxHp; } set { _maxHp = value; } }
public int Attack { get { return _attack; } set { _attack = value; } }
public int Defense { get { return _defense; } set { _defense = value; } }
public float MoveSpeed { get { return _moveSpeed; } set { _moveSpeed = value; } }
private void Start()
{
_level = 1;
_hp = 100;
_maxHp = 100;
_attack = 10;
_defense = 5;
_moveSpeed= 5.0f;
}
}โ
Player์ ์คํฏ ๊ด๋ฆฌ๋ฅผ ์ํด Stat๋ฅผ ์์๋ฐ๋ PlayerStat Script ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerStat : Stat
{
[SerializeField]
protected int _exp;
[SerializeField]
protected int _gold;
public int Exp { get { return _exp; } set { _exp = value; } }
public int Gold { get { return _gold; } set { _gold = value; } }
private void Start()
{
_level = 1;
_hp = 100;
_maxHp = 100;
_attack = 10;
_defense = 5;
_moveSpeed = 5.0f;
_exp = 0;
_gold = 0;
}
}โ
๏ผ ๋ง์ฐ์ค ์ปค์
- Cursor๋ก ์ฌ์ฉํ๊ธฐ ์ํ Image๋ Inspector ์ฐฝ์์ Texture Type์ Cursor๋ก ์ค์ ํ๋ค.
๋ง์ฐ์ค ์ปค์๋ฅผ ์ํ PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
// ...
Texture2D _attackIcon;
Texture2D _handIcon;
enum CursorType
{
None,
Attack,
Hand,
}
CursorType _cursorType = CursorType.None;
void Start()
{
_attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack"); // Image ํ์ผ์ Texture2D๋ฅผ ํตํด ๋ถ๋ฌ์จ๋ค.
_handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand"); // Image ํ์ผ์ Texture2D๋ฅผ ํตํด ๋ถ๋ฌ์จ๋ค.
}
void Update()
{
UpdateMouseCursor();
}
void UpdateMouseCursor()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100.0f, _mask))
{
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
{
if (_cursorType != CursorType.Attack)
{
Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // ๋ง์ฐ์ค Cursor๋ฅผ ์ค์ ํ๋ค.
_cursorType = CursorType.Attack;
}
}
else
{
if (_cursorType != CursorType.Hand)
{
Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // ๋ง์ฐ์ค Cursor๋ฅผ ์ค์ ํ๋ค.
_cursorType = CursorType.Hand;
}
}
}
}
// ...
}
+ ์ถ๊ฐ ๊ฒ์
- Cursor.SetCursor() ํจ์๋ Cursor์ Image๋ฅผ ์ฌ์ฉ์ ์ง์ Texture๋ก ๋ฐ๊ฟ์ค๋ค.
> ์ฒซ๋ฒ์งธ ์ธ์๋ Cursor์ ์ง์ ํ Texture, ๋๋ฒ์งธ ์ธ์๋ Cursor์ Spot, ์ธ๋ฒ์งธ ์ธ์๋ ์ต์ ํ ๋ฐฉ๋ฒ์ด๋ค.
๏ผ ํ๊ฒํ ๋ฝ์จ
ํ๊ฒํ
๋ฝ์จ์ ์ํด Define์ MouseEvent ์ถ๊ฐ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Define
{
// ...
public enum MouseEvent
{
Press,
PointerDown, // ๋ง์ฐ์ค๋ฅผ Clickํ๋ ์๊ฐ
PointerUp, // ๋ง์ฐ์ค Click์ ๋ผ๋ ์๊ฐ
Click, // ์๊ฐ์ ์ธ Click (๊ต์ฅํ ์งง์ ์๊ฐ)
}
// ...
}โ
InputManager ์์
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class InputManager
{
// ...
float _pressedTime = 0; // Click๊ณผ PointerUp์ ๊ตฌ๋ถํ๊ธฐ ์ํ ๊ฒ
// ์ฒดํฌํ๋ ๋ถ๋ถ์ด ์ ์ผํด์ง
public void OnUpdate()
{
// ...
if (MouseAction != null)
{
if (Input.GetMouseButton(0))
{
if (!_pressed) // ๋ง์ฐ์ค๋ฅผ Clickํ๋ ์๊ฐ
{
MouseAction.Invoke(Define.MouseEvent.PointerDown);
_pressedTime = Time.time;
}
MouseAction.Invoke(Define.MouseEvent.Press); // Press Event๋ฅผ ๋ฐ์์์ผ ์๋ ค์ค๋ค.
_pressed = true;
}
else
{
if (_pressed)
{
if (Time.time < _pressedTime + 0.2f) // 0.2์ด ๋ด์ Mouse Click์ ๋ ๊ฒฝ์ฐ
MouseAction.Invoke(Define.MouseEvent.Click); // Click Event๋ฅผ ๋ฐ์์์ผ ์๋ ค์ค๋ค.
MouseAction.Invoke(Define.MouseEvent.PointerUp);
}
_pressed = false;
_pressedTime = 0; // ์ด๊ธฐํ
}
}
}
}
PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
// ...
void UpdateMouseCursor()
{
if (Input.GetMouseButton(0)) // ๋ง์ฝ ๋ง์ฐ์ค๋ฅผ ๋๋ฅด๊ณ ์๋ค๋ฉด ์ปค์๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์๋๊ธฐ ๋๋ฌธ
return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100.0f, _mask))
{
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
{
if (_cursorType != CursorType.Attack)
{
Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // ๋ง์ฐ์ค Cursor๋ฅผ ์ค์ ํ๋ค.
_cursorType = CursorType.Attack;
}
}
else
{
if (_cursorType != CursorType.Hand)
{
Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // ๋ง์ฐ์ค Cursor๋ฅผ ์ค์ ํ๋ค.
_cursorType = CursorType.Hand;
}
}
}
}
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
GameObject _lockTarget;
void OnMouseEvent(Define.MouseEvent evt)
{
if (_state == PlayerState.Die)
return;
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
//Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
switch (evt)
{
case Define.MouseEvent.PointerDown:
{
if (raycastHit)
{
_destPos = hit.point;
_state = PlayerState.Moving;
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
_lockTarget = hit.collider.gameObject;
else
_lockTarget = null;
}
}
break;
case Define.MouseEvent.Press:
{
if(_lockTarget != null)
_destPos = _lockTarget.transform.position;
else if (raycastHit)
_destPos = hit.point;
}
break;
case Define.MouseEvent.PointerUp:
_lockTarget = null;
break;
}
}
}โ
+ ์ถ๊ฐ ๊ฒ์ (https://unity-beginner.tistory.com/17)
- GetMouseButtonDown( )์ ๋ง์ฐ์ค ๋ฒํผ์ ํด๋ฆญํ๊ณ ์์๋ ๊ณ์ํด์ ๋ฐ์,
GetMouseButton( )์ ๋ง์ฐ์ค ๋ฒํผ์ ํด๋ฆญํ์ ๋ ํ ๋ฒ ๋ฐ์,
GetMouseButtonUp( )์ ๋ง์ฐ์ค ๋ฒํผ์ ๋์์ ๋ ํ ๋ฒ ๋ฐ์
> ๊ดํธ์์ ์ ๋ ฅํ ์ซ์๊ฐ 0 : ๋ง์ฐ์ค ์ผ์ชฝ ๋ฒํผ, 1 : ๋ง์ฐ์ค ์ค๋ฅธ์ชฝ ๋ฒํผ, 2 : ๋ง์ฐ์ค ํ ๋ฒํผ์ ํด๋นํ๋ค.
๏ผ ๊ณต๊ฒฉ #1
PlayerController์์ ๋ง์ฐ์ค Cursor์ ๊ด๋ จ๋ ๋ถ๋ถ์ ๋ฐ๋ก ๋นผ CursorController ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CursorController : MonoBehaviour
{
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
Texture2D _attackIcon;
Texture2D _handIcon;
enum CursorType
{
None,
Attack,
Hand,
}
CursorType _cursorType = CursorType.None;
void Start()
{
_attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack"); // Image ํ์ผ์ Texture2D๋ฅผ ํตํด ๋ถ๋ฌ์จ๋ค.
_handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand"); // Image ํ์ผ์ Texture2D๋ฅผ ํตํด ๋ถ๋ฌ์จ๋ค.
}
void Update()
{
if (Input.GetMouseButton(0)) // ๋ง์ฝ ๋ง์ฐ์ค๋ฅผ ๋๋ฅด๊ณ ์๋ค๋ฉด ์ปค์๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์๋๊ธฐ ๋๋ฌธ
return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100.0f, _mask))
{
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
{
if (_cursorType != CursorType.Attack)
{
Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // ๋ง์ฐ์ค Cursor๋ฅผ ์ค์ ํ๋ค.
_cursorType = CursorType.Attack;
}
}
else
{
if (_cursorType != CursorType.Hand)
{
Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // ๋ง์ฐ์ค Cursor๋ฅผ ์ค์ ํ๋ค.
_cursorType = CursorType.Hand;
}
}
}
}
}โ
๊ณต๊ฒฉ Animation์ ์ํ PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
// ...
public enum PlayerState
{
Die,
Moving,
Idle,
Skill,
}
[SerializeField]
PlayerState _state = PlayerState.Idle;
public PlayerState State // PlayerState๊ฐ ๋ณํจ๊ณผ ๋์์ Animation๋ ๋ณ๊ฒฝํ๊ธฐ ์ํ ํ๋กํผํฐ ์์ฑ
{
get { return _state; }
set
{
_state = value;
Animator anim = GetComponent<Animator>();
switch (_state)
{
case PlayerState.Die:
anim.SetBool("attack", false);
break;
case PlayerState.Idle:
anim.SetFloat("speed", 0);
anim.SetBool("attack", false);
break;
case PlayerState.Moving:
anim.SetFloat("speed", _stat.MoveSpeed);
anim.SetBool("attack", false);
break;
case PlayerState.Skill:
anim.SetBool("attack", true);
break;
}
}
}
// ...
void UpdateMoving()
{
// ๋ชฌ์คํฐ๊ฐ ๋ด ์ฌ์ ๊ฑฐ๋ฆฌ๋ณด๋ค ๊ฐ๊น์ฐ๋ฉด ๊ณต๊ฒฉ
if (_lockTarget != null)
{
_destPos = _lockTarget.transform.position;
float distance = (_destPos - transform.position).magnitude;
if (distance <= 1)
{
State = PlayerState.Skill;
return;
}
}
// ...
}
// ...
void OnHitEvent()
{
State = PlayerState.Moving;
}
void Update()
{
switch (State)
{
case PlayerState.Die:
UpdateDie();
break;
case PlayerState.Moving:
UpdateMoving();
break;
case PlayerState.Idle:
UpdateIdle();
break;
case PlayerState.Skill:
UpdateSkill();
break;
}
}
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
GameObject _lockTarget;
void OnMouseEvent(Define.MouseEvent evt)
{
if (_state == PlayerState.Die)
return;
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
//Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
switch (evt)
{
case Define.MouseEvent.PointerDown:
{
if (raycastHit)
{
_destPos = hit.point;
State = PlayerState.Moving;
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
_lockTarget = hit.collider.gameObject;
else
_lockTarget = null;
}
}
break;
case Define.MouseEvent.Press:
{
if(_lockTarget != null)
_destPos = _lockTarget.transform.position;
else if (raycastHit)
_destPos = hit.point;
}
break;
}
}
}โ
๏ผ ๊ณต๊ฒฉ #2
- Animation ์ ํ์ ๋ฐ๋์ Unity์ Tool์ ์ฌ์ฉํ๋ผ๋ ๋ฒ์ ์๋ค.
> Parameter์ ๋ณ๊ฒฝ์ ํตํ Animation์ ์ ํ๋ณด๋ค Play( ), CrossFade( ) ์ฌ์ฉ์ด ๋ ํธ๋ฆฌํ ๋๋ ์๋ค.
> CrossFade( )๋ ์ฌ์ง์ด Blending๋ ๊ฐ๋ฅํ๋ค. (๋๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ฅผ ํตํด)
PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
// ...
public PlayerState State // PlayerState๊ฐ ๋ณํจ๊ณผ ๋์์ Animation๋ ๋ณ๊ฒฝํ๊ธฐ ์ํ ํ๋กํผํฐ ์์ฑ
{
get { return _state; }
set
{
_state = value;
Animator anim = GetComponent<Animator>();
switch (_state)
{
case PlayerState.Die:
break;
case PlayerState.Idle:
anim.CrossFade("WAIT", 0.1f);
break;
case PlayerState.Moving:
anim.CrossFade("RUN", 0.1f);
break;
case PlayerState.Skill:
anim.CrossFade("ATTACK", 0.1f);
break;
}
}
}
// ...
void UpdateSkill()
{
if (_lockTarget != null)
{
Vector3 dir = _lockTarget.transform.position - transform.position; // ๋ชฉ์ ์ง๊น์ง์ ๋ฐฉํฅ๋ฒกํฐ๋ฅผ ์ ์ ์๋ค.
Quaternion quat = Quaternion.LookRotation(dir); // ๋ชฉ์ ์ง๋ฅผ ๋ฐ๋ผ๋ณด๊ธฐ ์ํด์
transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐ๋๋๋ก
}
}
void OnHitEvent()
{
if (_stopSkill)
{
State = PlayerState.Idle;
}
else
{
State = PlayerState.Skill;
}
}
// ...
bool _stopSkill = false;
void OnMouseEvent(Define.MouseEvent evt)
{
switch (State)
{
case PlayerState.Idle:
OnMouseEvent_IdleRun(evt);
break;
case PlayerState.Moving:
OnMouseEvent_IdleRun(evt);
break;
case PlayerState.Skill:
{
if (evt == Define.MouseEvent.PointerUp)
_stopSkill = true;
}
break;
}
}
void OnMouseEvent_IdleRun(Define.MouseEvent evt)
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
//Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
switch (evt)
{
case Define.MouseEvent.PointerDown:
{
if (raycastHit)
{
_destPos = hit.point;
State = PlayerState.Moving;
_stopSkill = false;
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
_lockTarget = hit.collider.gameObject;
else
_lockTarget = null;
}
}
break;
case Define.MouseEvent.Press:
{
if (_lockTarget == null && raycastHit)
_destPos = hit.point;
}
break;
case Define.MouseEvent.PointerUp:
_stopSkill = true;
break;
}
}
}โ
+ ์ถ๊ฐ ๊ฒ์ (https://ansohxxn.github.io/unitydocs/anim/)
- CrossFade( )๋ Play( ) ์ ๋ํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ค ๋ ๋ถ๋๋ฝ๊ณ ์์ฐ์ค๋ฝ๊ฒ Blending ํ์ฌ Animation์ ์ฌ์ํ๋ค.
> ์ฒซ๋ฒ์งธ Parameter๋ ์ฌ์ํ๊ณ ์ ํ๋ Clip์ ์ด๋ฆ
> ๋๋ฒ์งธ Parameter๋ ์ง์ฐ ์๊ฐ (๋ค์ Animation์ผ๋ก ๋ฐ๋๋๋ฐ ๊ฑธ๋ฆฌ๋ Fade ์๊ฐ)
> ์ธ๋ฒ์งธ Parameter๋ Animation Layer
> ๋ค๋ฒ์งธ Parameter๋ Animation์ ์ฌ์ ์์ ์์ (0 ~ 1 ๋น์จ)
~> 0.0f๋ก ์ค์ ์ ๋ค์ ์ฒ์์ผ๋ก ๋์๊ฐ ์ฌ์ํ๋ค.
~> Clip์ Loop Time์ด ์ฒดํฌ๋์ด ์์ง ์๋๋ผ๋ ๋ฐ๋ณต ์ฌ์์ํฌ ์ ์๋ค.
๏ผ ์ฒด๋ ฅ ๊ฒ์ด์ง #1
- UI๋ 2D UI์ (๊ฒ์ ์ธ์๊ณผ ๋ณ๊ฐ), 3D UI (๊ฒ์ ์ธ์๊ณผ ๊ณต์กด)๋ก ๋๋ ์ ์๋ค.
> 3D UI๋ฅผ ๋ง๋ค๊ธฐ ์ํด์๋ Canvas์ Render Mode๋ฅผ World Space๋ก ๋ณ๊ฒฝํ๋ค.
> 3D UI๋ฅผ ๋ง๋ค๋ Canvas์ Scale์ ์ฝ 1/100 ๋ก ์ค์ด๋ฉด ์ ๋นํ ํฌ๊ธฐ๋ก ๋ณ๊ฒฝ์ด ๊ฐ๋ฅํ๋ค.

- Slider์ Value๊ฐ 0๊ณผ 1์ผ๋ ํ ๋น์ด ์์ง๋, ๊ฐ๋ ์ฐจ์์ง๋ ์๋ค.
> ์ด๋ Fill Area์ [ Inspector ] - [ Rect Transform ] ์์ Left์ Right์ ๊ฐ์ ์ ๋ถ 0์ผ๋ก ์ค์ ํ๋ฉด ํด๊ฒฐ์ด ๊ฐ๋ฅํ๋ค.





- Slider์ Fill์ด Background์ ๊ฒฝ๊ณ์ ์ ๋์ด ํ์ด๋์ ์๋ค.
> ์ด๋ Fill์ [ Inspector ] - [ Rect Transform ] ์์ Left์ Right์ ๊ฐ์ ์ ๋ถ 0์ผ๋ก ์ค์ ํ๋ฉด ํด๊ฒฐ์ด ๊ฐ๋ฅํ๋ค.





HP_Bar์ Canvas๋ฅผ Prefab์ผ๋ก ์ ์ฅํ ๋ค UI_HPBar Script ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_HPBar : UI_Base
{
enum GameObjects
{
HPBar
}
public override void Init()
{
Bind<GameObject>(typeof(GameObjects));
}
void Update()
{
// HP_Bar๋ฅผ ์บ๋ฆญํฐ์ ๋จธ๋ฆฌ ์๋ก ์์น์ํค๊ธฐ ์ํด์
Transform parent = transform.parent;
transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y); // ๋ถ๋ชจ๋ ์์น๋ฅผ ๊ธฐ์ค์ผ๋ก Collider๋งํผ์ ๋์ด๋ฅผ ์ฌ๋ ค ์ ์ฅ
}
}โ
UIManager ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIManager
{
// ...
public T MakeWorldSpaceUI<T>(Transform parent = null, string name = null) where T : UI_Base
{
if (string.IsNullOrEmpty(name))
name = typeof(T).Name;
GameObject go = Managers.Resource.Instantiate($"UI/WorldSpace/{name}");
if (parent != null)
go.transform.SetParent(parent);
Canvas canvas = go.GetOrAddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.worldCamera = Camera.main;
return Util.GetOrAddComponent<T>(go);
}
// ...
}โ
PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
// ...
void Start()
{
_stat = gameObject.GetComponent<PlayerStat>();
Managers.Input.MouseAction -= OnMouseEvent; // ํน์๋ผ๋ ๋ค๋ฅธ ๊ณณ์์ ๊ตฌ๋
์ ์ฒญ์ ํ๊ณ ์๋ ๊ฒฝ์ฐ๋ฅผ ๋๋น
Managers.Input.MouseAction += OnMouseEvent;
Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
}
// ...
}โ
๏ผ ์ฒด๋ ฅ ๊ฒ์ด์ง #2
์์๋ฐ๋ Class๋ค์ด Start ํจ์ ์ ์ธ ์์ด Init ํจ์๋ฅผ ์คํํ๋๋ก UI_Base ์์
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public abstract class UI_Base : MonoBehaviour
{
// ...
public abstract void Init();
void Start()
{
Init();
}
// ...
}โ
Billboard ๊ธฐ๋ฅ ๊ตฌํ ๋ฐ ์ค์๊ฐ HP ๊ฐ์๋ฅผ ์ํ UI_HPBar ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_HPBar : UI_Base
{
enum GameObjects
{
HPBar
}
Stat _stat;
public override void Init()
{
Bind<GameObject>(typeof(GameObjects));
_stat = transform.parent.GetComponent<Stat>();
}
void Update()
{
// HP_Bar๋ฅผ ์บ๋ฆญํฐ์ ๋จธ๋ฆฌ ์๋ก ์์น์ํค๊ธฐ ์ํด์
Transform parent = transform.parent;
transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y); // ๋ถ๋ชจ๋ ์์น๋ฅผ ๊ธฐ์ค์ผ๋ก Collider๋งํผ์ ๋์ด๋ฅผ ์ฌ๋ ค ์ ์ฅ
transform.rotation = Camera.main.transform.rotation; // ์์ ์ด ๋ฐ๋ผ๋ณด๋ ๋ฐฉํฅ์ ์นด๋ฉ๋ผ๊ฐ ๋ฐ๋ผ๋ณด๋ ๋ฐฉํฅ์ผ๋ก ์ค์ (Billboard์ ๊ฐ๋
)
float ratio = _stat.Hp / (float)_stat.MaxHp;
SetHpRatio(ratio);
}
public void SetHpRatio(float ratio)
{
GetObject((int)GameObjects.HPBar).GetComponent<Slider>().value = ratio;
}
}
Hit Event ๋ฐ์์ ์๋๋ฐฉ์ HP๋ฅผ ๊ฐ์์ํค๊ธฐ ์ํ PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
// ...
void OnHitEvent()
{
if (_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
Stat myStat = gameObject.GetComponent<PlayerStat>();
int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
Debug.Log(damage);
targetStat.Hp -= damage;
}
if (_stopSkill)
{
State = PlayerState.Idle;
}
else
{
State = PlayerState.Skill;
}
}
// ...
}โ
+ ์ถ๊ฐ ๊ฒ์
- Unity์์ 3D ์ค๋ธ์ ํธ๊ฐ ์นด๋ฉ๋ผ๋ฅผ ๋ฐ๋ผ๋ณด๋๋ก ๋ง๋๋ ๊ธฐ์ ์ Billboard๋ผ๊ณ ํ๋ค.
> UI์ธ Slider๋ฅผ ์นด๋ฉ๋ผ๋ฅผ ๋ฐ๋ผ๋ณด๋๋ก ์์ ํ ๊ฒฝ์ฐ ์ข์ฐ ๋ฐ์ ์ด ๋ ์ฑ๋ก ํ์๋๋ค.
( transform.LookAt(Camera.main) ์ ํตํด )
~> ๋ฐ๋ผ์ UI์ธ Slider์ Billboard ๊ธฐ์ ์ ์ ์ฉํ๊ธฐ ์ํด์๋ ์๋์ ๊ฐ์ด ์์ ํ๋ค.
( transform.rotation = Camera.main.transform.rotation ์ผ๋ก ์์ )

๏ผ ๋ชฌ์คํฐ AI #1
- ์๋ก ์์ฑํ๊ณ ์ ํ๋ MonsterController๊ฐ PlayerController์ ๊ฒน์น๋ ๋ถ๋ถ์ด ์๋นํ ๋ง์์ ์ด๋ด๋๋ BaseController๋ฅผ
์๋ก ์์ฑํ์ฌ MonsterController์ PlayerController๊ฐ BaseController๋ฅผ ์์๋ฐ๋๋ก ํ๋ ๊ฒ์ด ์ข๋ค.
Define ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Define
{
public enum State
{
Die,
Moving,
Idle,
Skill,
}
// ...
}โ
Base Controller ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class BaseController : MonoBehaviour
{
[SerializeField]
protected Vector3 _destPos; // ๋ชฉ์ ์ง ์ขํ๋ฅผ ์ ์ฅํ๊ธฐ ์ํ ๋ณ์
[SerializeField]
protected Define.State _state = Define.State.Idle;
[SerializeField]
protected GameObject _lockTarget;
public virtual Define.State State // State๊ฐ ๋ณํจ๊ณผ ๋์์ Animation๋ ๋ณ๊ฒฝํ๊ธฐ ์ํ ํ๋กํผํฐ ์์ฑ
{
get { return _state; }
set
{
_state = value;
Animator anim = GetComponent<Animator>();
switch (_state)
{
case Define.State.Die:
break;
case Define.State.Idle:
anim.CrossFade("WAIT", 0.1f);
break;
case Define.State.Moving:
anim.CrossFade("RUN", 0.1f);
break;
case Define.State.Skill:
anim.CrossFade("ATTACK", 0.1f);
break;
}
}
}
private void Start()
{
Init();
}
void Update()
{
switch (State)
{
case Define.State.Die:
UpdateDie();
break;
case Define.State.Moving:
UpdateMoving();
break;
case Define.State.Idle:
UpdateIdle();
break;
case Define.State.Skill:
UpdateSkill();
break;
}
}
public abstract void Init();
protected virtual void UpdateDie() { }
protected virtual void UpdateMoving() { }
protected virtual void UpdateIdle() { }
protected virtual void UpdateSkill() { }
}โ
PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : BaseController
{
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
PlayerStat _stat;
bool _stopSkill = false;
public override void Init()
{
_stat = gameObject.GetComponent<PlayerStat>();
Managers.Input.MouseAction -= OnMouseEvent; // ํน์๋ผ๋ ๋ค๋ฅธ ๊ณณ์์ ๊ตฌ๋
์ ์ฒญ์ ํ๊ณ ์๋ ๊ฒฝ์ฐ๋ฅผ ๋๋น
Managers.Input.MouseAction += OnMouseEvent;
if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
}
protected override void UpdateMoving()
{
// ๋ชฌ์คํฐ๊ฐ ๋ด ์ฌ์ ๊ฑฐ๋ฆฌ๋ณด๋ค ๊ฐ๊น์ฐ๋ฉด ๊ณต๊ฒฉ
if (_lockTarget != null)
{
_destPos = _lockTarget.transform.position;
float distance = (_destPos - transform.position).magnitude;
if (distance <= 1)
{
State = Define.State.Skill;
return;
}
}
Vector3 dir = _destPos - transform.position; // ๋ชฉ์ ์ง๊น์ง์ ๋ฐฉํฅ๋ฒกํฐ๋ฅผ ์ ์ ์๋ค.
if (dir.magnitude < 0.1f) // ๋ง์ฝ ๋ชฉ์ ์ง๊น์ง ๊ฑฐ์ ๋์ฐฉ์ ์๋ฃํ๋ค๋ฉด
{
State = Define.State.Idle;
}
else
{
// NavMesh๋ฅผ ์ฌ์ฉ
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
float moveDist = Mathf.Clamp(Time.deltaTime * _stat.MoveSpeed, 0, dir.magnitude);
nma.Move(dir.normalized * moveDist);
Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green); // Ray๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"))) // ๊ฑด๋ฌผ, ์ฌ๋ฌผ๊ณผ ์ธ์ ์ Player๊ฐ ๋ฉ์ถ๋๋ก ์ค์
{
if (Input.GetMouseButton(0) == false)
State = Define.State.Idle;
return;
}
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt์ ์ํ ํ์ ์ ๋ณด๋ค ์์ฐ์ค๋ฝ๊ฒ ํ๋๋ก
}
}
protected override void UpdateSkill()
{
if (_lockTarget != null)
{
Vector3 dir = _lockTarget.transform.position - transform.position; // ๋ชฉ์ ์ง๊น์ง์ ๋ฐฉํฅ๋ฒกํฐ๋ฅผ ์ ์ ์๋ค.
Quaternion quat = Quaternion.LookRotation(dir); // ๋ชฉ์ ์ง๋ฅผ ๋ฐ๋ผ๋ณด๊ธฐ ์ํด์
transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐ๋๋๋ก
}
}
void OnHitEvent()
{
if (_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
Stat myStat = gameObject.GetComponent<PlayerStat>();
int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
Debug.Log(damage);
targetStat.Hp -= damage;
}
if (_stopSkill)
{
State = Define.State.Idle;
}
else
{
State = Define.State.Skill;
}
}
void OnMouseEvent(Define.MouseEvent evt)
{
switch (State)
{
case Define.State.Idle:
OnMouseEvent_IdleRun(evt);
break;
case Define.State.Moving:
OnMouseEvent_IdleRun(evt);
break;
case Define.State.Skill:
{
if (evt == Define.MouseEvent.PointerUp)
_stopSkill = true;
}
break;
}
}
void OnMouseEvent_IdleRun(Define.MouseEvent evt)
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
//Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
switch (evt)
{
case Define.MouseEvent.PointerDown:
{
if (raycastHit)
{
_destPos = hit.point;
State = Define.State.Moving;
_stopSkill = false;
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
_lockTarget = hit.collider.gameObject;
else
_lockTarget = null;
}
}
break;
case Define.MouseEvent.Press:
{
if (_lockTarget == null && raycastHit)
_destPos = hit.point;
}
break;
case Define.MouseEvent.PointerUp:
_stopSkill = true;
break;
}
}
}โ
๏ผ๋ชฌ์คํฐ AI #2
MonsterController ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MonsterController : BaseController
{
Stat _stat;
[SerializeField]
float _scanRange = 10;
[SerializeField]
float _attackRange = 2;
public override void Init()
{
_stat = gameObject.GetComponent<Stat>();
if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
}
protected override void UpdateIdle()
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player == null)
return;
float distance = (player.transform.position - transform.position).magnitude;
if (distance <= _scanRange) // ์ฌ์ ๊ฑฐ๋ฆฌ ์์ผ๋ก ๋ค์ด์ฌ ๊ฒฝ์ฐ ์์ง์ด๊ธฐ ์์
{
_lockTarget = player;
State = Define.State.Moving;
return;
}
}
protected override void UpdateMoving()
{
// ํ๋ ์ด์ด๊ฐ ๋ด ์ฌ์ ๊ฑฐ๋ฆฌ๋ณด๋ค ๊ฐ๊น์ฐ๋ฉด ๊ณต๊ฒฉ
if (_lockTarget != null)
{
_destPos = _lockTarget.transform.position;
float distance = (_destPos - transform.position).magnitude;
if (distance <= _attackRange)
{
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
nma.SetDestination(transform.position);
State = Define.State.Skill;
return;
}
}
Vector3 dir = _destPos - transform.position; // ๋ชฉ์ ์ง๊น์ง์ ๋ฐฉํฅ๋ฒกํฐ๋ฅผ ์ ์ ์๋ค.
if (dir.magnitude < 0.1f) // ๋ง์ฝ ๋ชฉ์ ์ง๊น์ง ๊ฑฐ์ ๋์ฐฉ์ ์๋ฃํ๋ค๋ฉด
{
State = Define.State.Idle;
}
else
{
// NavMesh๋ฅผ ์ฌ์ฉ
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
nma.SetDestination(_destPos);
nma.speed = _stat.MoveSpeed;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt์ ์ํ ํ์ ์ ๋ณด๋ค ์์ฐ์ค๋ฝ๊ฒ ํ๋๋ก
}
}
protected override void UpdateSkill()
{
if (_lockTarget != null)
{
Vector3 dir = _lockTarget.transform.position - transform.position; // ๋ชฉ์ ์ง๊น์ง์ ๋ฐฉํฅ๋ฒกํฐ๋ฅผ ์ ์ ์๋ค.
Quaternion quat = Quaternion.LookRotation(dir); // ๋ชฉ์ ์ง๋ฅผ ๋ฐ๋ผ๋ณด๊ธฐ ์ํด์
transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐ๋๋๋ก
}
}
void OnHitEvent()
{
if (_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
Stat myStat = gameObject.GetComponent<Stat>();
int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
targetStat.Hp -= damage;
if (targetStat.Hp > 0)
{
float distance = (_lockTarget.transform.position - transform.position).magnitude;
if (distance <= _attackRange)
State = Define.State.Skill;
else
State = Define.State.Moving;
}
else
{
State = Define.State.Idle;
}
}
else
{
State = Define.State.Idle;
}
}
}โ
๏ผ Destroy #1
- Destroy๋ Object๋ ์ฌ์ค ์ค์ ๋ก ์ญ์ ๋๊ฑด C++ Native Object์ด๊ณ , UnityEngine.Object C# Class ๋ถ๋ถ์ ์์ง Memory
์์ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ์ค์ ๋ก null์ด ๋ ๊ฒ์ ์๋์ง๋ง ์์ด์ง ๊ฒ์ฒ๋ผ ํ๋ํด์ผ ํ๊ธฐ ๋๋ฌธ์ "null"๋ก ์ฒ๋ฆฌ๋๋ค.
> ์ด๊ฒ์ด ๋ฐ๋ก fake null ์ด๋ค.
> ์ด์ ๋ํ ์ฒ๋ฆฌ๊ฐ UnityEngine.Object์ == ์ฐ์ฐ์ ์ค๋ฒ๋ก๋ฉ์ ๊ตฌํ๋์ด ์๋ค.
(์ง์ง null์ด ๋๊ฒ ์๋์๋ Destroy๋ Object == null ์ ํ๋ฉด true๋ฅผ ๋ฆฌํดํ๋๋ก ๊ตฌํ๋จ)
- Destroy๋ Object์ Component๋ฅผ ์ฐธ์กฐํ๋ ๊ฒ๋ค์ ์ ํ์ธํด์ผ ํ๋ค.
> Object๊ฐ ํด์ ๋๋ฉด ๋น์ฐํ ํด๋น Object์ Component๋ ์ฐธ์กฐํ ์ ์๋ค.
MosterController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MonsterController : BaseController
{
// ...
void OnHitEvent()
{
// ...
if (targetStat.Hp <=0)
{
GameObject.Destroy(targetStat.gameObject);
}
// ...
}
}โ
CameraController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
// ...
// Player์ ์์ง์์ด ์คํ๋ ๋ค Camera์ ์์น๋ฅผ ์์ง์ฌ์ผ ๋จ๋ฆฌ๋ ํ์์ด ์ค์ด๋ ๋ค.
void LateUpdate() // ๊ฒ์ Logic์์ LateUpdate()๋ Update()๋ณด๋ค ๋ฆ๊ฒ ์คํ๋๋ค.
{
if (_mode == Define.CameraMode.QuaterView)
{
if (_player == null)
{
return;
}
// ...
}
}
// ...
}โ
+ ์ถ๊ฐ ๊ฒ์ (https://ansohxxn.github.io/unitydocs/fakenull/)
- ํด๊ฒฐ ๋ฐฉ๋ฒ 1 : Object๋ฅผ bool๋ก ๋ฌต์์ ํ๋ณํ
- ํด๊ฒฐ ๋ฐฉ๋ฒ 2 : Destroy ํ ์ง์ง null์ ๋์
- ํด๊ฒฐ ๋ฐฉ๋ฒ 3 : System.Object๋ก ํ๋ณํ ํ ๋น๊ต
๏ผ Destroy #2
Define ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Define
{
public enum WorldObject
{
Unknown,
Player,
Monster,
}
// ...
}โ
BaseController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class BaseController : MonoBehaviour
{
// ...
public Define.WorldObject WorldObjectType { get; protected set; } = Define.WorldObject.Unknown;
// ...
}โ
PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : BaseController
{
// ...
public override void Init()
{
WorldObjectType = Define.WorldObject.Player;
// ...
}
// ...
}โ
MonsterController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MonsterController : BaseController
{
// ...
public override void Init()
{
WorldObjectType = Define.WorldObject.Monster;
// ...
}
// ...
void OnHitEvent()
{
if (_lockTarget != null)
{
// ...
if (targetStat.Hp <=0)
{
Managers.Game.Despawn(targetStat.gameObject);
}
// ...
}
// ...
}
}โ
GameManagerEx ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManagerEx : MonoBehaviour
{
// ์คํฐ๋ Object๋ค์ ID๋ก ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ข๋ค => Dictionary๊ฐ ์ ๋น
// ์๋ฒ์์๋ ํด๋น ID๋ฅผ ํตํด ํต์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
//Dictionary<int, GameObject> _players = new Dictionary<int, GameObject>();
//Dictionary<int, GameObject> _monsters = new Dictionary<int, GameObject>();
// ํ์ฌ๋ ๋ฉํฐ๊ฐ ์๋ ์ฑ๊ธ ๊ฒ์์ด๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ๊ด๋ฆฌํ๋ค.
GameObject _player;
HashSet<GameObject> _monsters = new HashSet<GameObject>(); // HashSet์ Dictionary์ ๋น์ทํ์ง๋ง Key๊ฐ X
public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
{
GameObject go = Managers.Resource.Instantiate(path, parent);
switch (type)
{
case Define.WorldObject.Monster:
_monsters.Add(go);
break;
case Define.WorldObject.Player:
_player = go;
break;
}
return go;
}
public Define.WorldObject GetWorldObjectType(GameObject go)
{
BaseController bc = go.GetComponent<BaseController>();
if (bc == null)
return Define.WorldObject.Unknown;
return bc.WorldObjectType ;
}
public void Despawn(GameObject go)
{
Define.WorldObject type = GetWorldObjectType(go);
switch (type)
{
case Define.WorldObject.Monster:
{
if (_monsters.Contains(go))
_monsters.Remove(go);
}
break;
case Define.WorldObject.Player:
{
if (_player == go)
_player = null;
}
break;
}
Managers.Resource.Destroy(go);
}
}โ
Managers ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Managers : MonoBehaviour
{
// ...
GameManagerEx _game = new GameManagerEx();
// ...
public static GameManagerEx Game { get { return Instance._game; } }
// ...
}โ
Extension ์์
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public static class Extension
{
// ...
// ํด๋งํ๋ Object์ผ ๊ฒฝ์ฐ Destroy๊ฐ ์๋ ๋นํ์ฑํ ์ฒ๋ฆฌ๊ฐ ๋๋ค.
// ๋ฐ๋ผ์ null ์ฒดํฌ๋ฟ๋ง์ด ์๋ ํ์ฑํ ์ํ ์ฌ๋ถ๋ ์ฒดํฌํด์ค์ผ ํ๋ค.
public static bool IsValid(this GameObject go)
{
return go != null && go.activeSelf;
}
}โ
CameraController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
// ...
public void SetPlayer(GameObject player) { _player = player; }
// Player์ ์์ง์์ด ์คํ๋ ๋ค Camera์ ์์น๋ฅผ ์์ง์ฌ์ผ ๋จ๋ฆฌ๋ ํ์์ด ์ค์ด๋ ๋ค.
void LateUpdate() // ๊ฒ์ Logic์์ LateUpdate()๋ Update()๋ณด๋ค ๋ฆ๊ฒ ์คํ๋๋ค.
{
if (_mode == Define.CameraMode.QuaterView)
{
if (_player.IsValid() == false)
{
return;
}
// ...
}
}
// ...
}โ
GameScene ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameScene : BaseScene
{
protected override void Init() {
// ...
GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "Player");
Camera.main.gameObject.GetOrAddComponent<CameraController>().SetPlayer(player);
Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
}
// ...
}โ
+ ์ถ๊ฐ ๊ฒ์ (https://wlsdn629.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-Dictionary-HashTable-HastSet-%EA%B0%84%EB%8B%A8-%EC%84%A4%EB%AA%85)
- HashSet์ Key๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉฐ Value(๋ค)์ ๊ตฌ์ฑ์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.
> ์์๊ฐ ์กด์ฌํ์ง ์๋๋ค.
> ๊ฐ ํญ๋ชฉ์ ์ค๋ณต์ ํ์ฉํ์ง ์๋๋ค. (์ฆ, Value ์ค๋ณต์ ํ์ฉ X)
> Add( ), Remove( ), Clear( ), UnionWith( ), IntersectWith( ), ExceptWith( ) ๋ฑ์ ํจ์๊ฐ ์กด์ฌํ๋ค.
๏ผ ๋ ๋ฒจ์
- Damage์ ๊ด๋ จ๋ ๋ถ๋ถ์ ๊ณต๊ฒฉ์ ํ๋ ๊ฐ์ฒด๊ฐ ์๋ ๊ณต๊ฒฉ์ ๋ฐ๋ ๊ฐ์ฒด์์ ์ฒ๋ฆฌํด์ฃผ๋ ๊ฒ์ด ์ข๋ค.
(๊ณต๊ฒฉ์ ๋ฐ๋ ๊ฐ์ฒด์ ๋ฐฉ์ด๋ ฅ, ๋ฒํ ๋ฑ์ด ์กด์ฌํ๊ธฐ ๋๋ฌธ)
Stat ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Stat : MonoBehaviour
{
// ...
public virtual void OnAttacked(Stat attacker)
{
int damage = Mathf.Max(0, attacker.Attack - Defense);
Hp -= damage;
if (Hp <= 0) {
Hp = 0;
OnDead();
}
}
protected virtual void OnDead()
{
Managers.Game.Despawn(gameObject);
}
}โ
PlayerStat ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerStat : Stat
{
// ...
protected override void OnDead()
{
Debug.Log("Player Dead");
}
}โ
MonsterController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : BaseController
{
// ...
void OnHitEvent()
{
if (_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
targetStat.OnAttacked(_stat);
}
// ...
}
// ...
}โ
PlayerController ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : BaseController
{
// ...
void OnHitEvent()
{
if (_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
targetStat.OnAttacked(_stat);
}
// ...
}
// ...
}โ
- Monster๋ฅผ ์ฃฝ์ผ ๊ฒฝ์ฐ Monster์ ๋ฐ๋ฅธ ๊ฒฝํ์น ํ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํตํด ๊ด๋ฆฌํด์ฃผ๋ ๊ฒ์ด ์ข๋ค.
- Level Up ํ์ธ ์ฝ๋๋ ์ ์ผํ ๊ฒ์ด ์ข๋ค. (๊ทธ๋ ์ง ์์ผ๋ฉด ๊ณ ๋ ๋ณต๋ถ ์์ )
StatData json ํ์ผ ์์
{
"stats": [
{
"level": "1",
"maxHp": "200",
"attack": "20",
"totalExp": "0"
},
{
"level": "2",
"maxHp": "250",
"attack": "25",
"totalExp": "10"
},
{
"level": "3",
"maxHp": "300",
"attack": "30",
"totalExp": "20"
}
]
}โ
Data.Contents ์์
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Data
{
#region Stat
[Serializable]
public class Stat
{
public int level;
public int maxHp;
public int attack;
public int totalExp;
}
// ...
#endregion
}โ
Stat ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Stat : MonoBehaviour
{
// ...
public virtual void OnAttacked(Stat attacker)
{
int damage = Mathf.Max(0, attacker.Attack - Defense);
Hp -= damage;
if (Hp <= 0) {
Hp = 0;
OnDead(attacker);
}
}
protected virtual void OnDead(Stat attacker)
{
PlayerStat playerStat = attacker as PlayerStat;
if (playerStat != null)
{
playerStat.Exp += 5;
}
Managers.Game.Despawn(gameObject);
}
}โ
PlayerStat ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerStat : Stat
{
// ...
public int Exp {
get { return _exp; }
set
{
_exp = value;
// Level Up ์ฒดํฌ
int level = Level;
while (true)
{
Data.Stat stat;
if (Managers.Data.StatDict.TryGetValue(level + 1, out stat) == false) // ๋ค์ Level์ด ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ
break;
if (_exp < stat.totalExp)
break;
level++;
}
if (level != Level) // Level์ ๋ณํ๊ฐ ์์ ๊ฒฝ์ฐ
{
Level = level;
SetStat(Level);
}
}
}
// ...
private void Start()
{
_level = 1;
_exp = 0;
_defense = 5;
_moveSpeed = 5.0f;
_gold = 0;
SetStat(_level);
}
public void SetStat(int level)
{
Dictionary<int, Data.Stat> dict = Managers.Data.StatDict;
Data.Stat stat = dict[level];
_hp = stat.maxHp;
_maxHp = stat.maxHp;
_attack = stat.attack;
}
protected override void OnDead(Stat attacker)
{
Debug.Log("Player Dead");
}
}
๏ผ ๋ชฌ์คํฐ ์๋ ์์ฑ
SpawningPool ์์ฑ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class SpawningPool : MonoBehaviour
{
[SerializeField]
int _monsterCount = 0; // ํ์ฌ ๋ชฌ์คํฐ ์
int _reserveCount = 0; // ํ์ฌ Spawn ์์ฝ ์
[SerializeField]
int _keepMonsterCount = 0; // ์ ์ง์์ผ์ผ ํ๋ ๋ชฌ์คํฐ ์
[SerializeField]
Vector3 _spawnPos; // Spawn ์ค์ฌ์
[SerializeField]
float _spawnRadius = 15.0f; // Spawn ์ค์ฌ์ ์ ๊ธฐ์ค์ผ๋ก ๋๋ค ์์ฑ์ ์ํ ์์ฑ ๊ฑฐ๋ฆฌ ๋ฒ์ ์ ํ
[SerializeField]
float _spawnTime = 5.0f; // ๋๋ค ์์ฑ์ ์ํ ์์ฑ ์๊ฐ ๋ฒ์ ์ ํ
public void AddMonsterCount(int value) { _monsterCount += value; }
public void SetkeepMonsterCount(int count) { _keepMonsterCount = count; }
void Start()
{
Managers.Game.OnSpawnEvent -= AddMonsterCount;
Managers.Game.OnSpawnEvent += AddMonsterCount;
}
void Update()
{
while (_reserveCount + _monsterCount < _keepMonsterCount)
{
StartCoroutine("ReserveSpawn");
}
}
IEnumerator ReserveSpawn()
{
_reserveCount++;
yield return new WaitForSeconds(Random.Range(0, _spawnTime));
GameObject obj = Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
NavMeshAgent nma = obj.GetOrAddComponent<NavMeshAgent>();
Vector3 randPos;
while(true) // randPos๊ฐ ๊ฐ ์ ์๋ ๊ธธ์ด ๋์ฌ๋๊น์ง ๋ฐ๋ณต
{
Vector3 randDir = Random.insideUnitSphere * Random.Range(0, _spawnRadius); // ๋๋ค์ผ๋ก ๋ฝํ ๋ฐฉํฅ๋ฒกํฐ๊ฐ randDir์ ๋ด๊ธด๋ค. (insideUnitCircle์ 2D)
randDir.y = 0; // ๋
์ ๋ท๊ณ Spawn ๋์ง ์๋๋ก
randPos = _spawnPos + randDir;
// randPos๊ฐ ๊ฐ ์ ์๋ ๊ธธ์ธ์ง ์ฒดํฌ
NavMeshPath path = new NavMeshPath();
if (nma.CalculatePath(randPos, path)) // ๊ฐ ์ ์๋ ๊ธธ์ด๋ฉด True, ๊ฐ ์ ์๋ ๊ธธ์ด๋ฉด False๋ฅผ ๋ฐํ
break;
}
obj.transform.position = randPos;
_reserveCount--;
}
}โ
GameManagerEx ์์
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManagerEx : MonoBehaviour
{
// ...
public Action<int> OnSpawnEvent; // int๋ ์ฆ๊ฐ/๊ฐ์ ์
// ...
public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
{
// ...
switch (type)
{
case Define.WorldObject.Monster:
// ...
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(1);
break;
// ...
}
// ...
}
// ...
public void Despawn(GameObject go)
{
// ...
switch (type)
{
case Define.WorldObject.Monster:
{
if (_monsters.Contains(go)) {
_monsters.Remove(go);
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(-1);
}
}
break;
// ...
}
// ...
}
}
GameScene ์์
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameScene : BaseScene
{
protected override void Init() {
// ...
//Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
GameObject go = new GameObject { name = "SpawningPool" };
SpawningPool pool = go.GetOrAddComponent<SpawningPool>();
pool.SetkeepMonsterCount(5);
}
// ...
}โ
+ ์ถ๊ฐ ๊ฒ์ ( )
- Unity๋ ๋์ ์์ฑ๊ณผ ๊ด๋ จ๋ ํจ์๋ค์ด ์๋ ์งํฉ์ธ Random ์ด๋ผ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
~> ๊ฐ์ฅ ๋ํ์ ์ธ ๊ธฐ๋ฅ์ด Random.Range(min, max) ์ด๋ฉฐ ์ด๋ min๊ณผ max ๋ฒ์ ๋ด์์ ๋๋คํ ๊ฐ์ ๋ฐํํ๋ค.
(์ด๋ max์ Type์ด int์ผ ๊ฒฝ์ฐ max์ ๊ฐ์ ํฌํจ๋์ง X)
~> Random.insideUnitSphere์ ๋ฐ๊ฒฝ 1์ ๊ฐ๋ ๊ตฌ ์์ ์์์ ์์น(Vector3)๋ฅผ ๋ฐํํ๋ ํ๋กํผํฐ์ด๋ค.
~> Random. insideUnitCircle ์ ๋ฐ๊ฒฝ 1์ ๊ฐ๋ ๊ตฌ ์์ ์์์ ์์น(Vector2)๋ฅผ ๋ฐํํ๋ ํ๋กํผํฐ์ด๋ค.