Demo
Setting Environment
- Asset Store
- Download Medieval Town Exteriors
- change folder name



- Import from own Unity Framework
- create player
- create Scene Manager
- create Main Camera

Move
- Layer
- Change Layer to
Wall
in Terrain - if camera doesn’t move, drag and drop player to player of CameraController Component
- Change Layer to
Block
in Buildings, Components and Fence
- Change Layer to
- Navigation


Bake
means create files before execute- Bake is related with static option of objects
-
from Navigation, we can define whether player can go
- Nav Mesh Agent
- add
Nav Mesh Component
to player
- add

- PlayerController.cs
void UpdateMoving()
{
...
if (dir.magnitude < 0.1f)
{
_state = PlayerState.Idle;
}
else
{
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
nma.Move(dir.normalized * moveDist);
Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green);
if(Physics.Raycast(transform.position + Vector3.up *0.5f, dir, 1.0f, LayerMask.GetMask("Block")))
{
_state = PlayerState.Idle;
return;
}
//transform.position += dir.normalized * moveDist;
...
}
}
Stat
Monster
- Download Monster in Asset Store
- download any knight model with animation
- organize folder

Content
- Scripts\Contents\Stat.cs
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; } }
public void Start()
{
_level = 1;
_hp = 100;
_maxhp = 100;
_attack = 10;
_defense = 5;
_moveSpeed = 5.0f;
}
}
-
change before Stat Class to
Data.Stat
-
Scripts\Contents\PlayerStat.cs
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; } }
public void Start()
{
_level = 1;
_hp = 100;
_maxhp = 100;
_attack = 10;
_defense = 5;
_moveSpeed = 5.0f;
_exp = 0;
_gold = 0;
}
}
- Add Component
- add PlayerStat.cs component in Player
- add Stat.cs component in Monster
Layer
- Change Layer
- Change Layer name
Wall
toGround
- Change Terrain layer to
Ground
- Change Layer name
- Add Layer
- add Layer
Monster
in Monster
- add Layer
- PlayerController.cs
- change
_speed
to_stat.MoveSpeed
- change
public class PlayerController : MonoBehaviour
{
//[SerializeField]
//float _speed = 10.0f;
PlayerStat _stat;
...
void Start()
{
_stat = gameObject.GetComponent<PlayerStat>();
...
}
...
void UpdateMoving()
{
...
else
{
...
float moveDist = Mathf.Clamp(_stat.MoveSpeed * Time.deltaTime, 0, dir.magnitude);
...
anim.SetFloat("speed", _stat.MoveSpeed);
}
}
...
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
void OnMouseClicked(Define.MouseEvent evt)
{
...
if (Physics.Raycast(ray, out hit, 100.0f, _mask))
{
...
if(hit.collider.gameObject.layer == (int)Define.Layer.Monster)
{
Debug.Log("Monster Click!");
}
else
{
Debug.Log("Ground Click!");
}
}
}
}
Test
Cursor
Cursor
- Download Cursor in Asset Store
- Select cursors and Move those to Resources\Textures\Cursor


- Change Texture type to
Cursor
in all Cursor



Change Cursor
- PlayerController.cs
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");
_handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand");
...
}
...
void UpdateMouseCousor()
{
...
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);
_cursorType = CursorType.Attack;
}
}
else
{
if(_cursorType != CursorType.Hand)
{
Cursor.SetCursor(_handIcon, new Vector2(_handIcon.width / 3, 0), CursorMode.Auto);
_cursorType = CursorType.Hand;
}
}
}
}
...
}
Test
Targeting
- Click vs Press
- Click: press mouse button shortly
- Press: press mouse button for several seconds
- you can define Click and Press
Mouse Event
- Define.cs
public enum MouseEvent
{
Press,
PointerDown,
PointerUp,
Click,
}
- PlayerController.cs
- when you press mouse button, player still moves
public class PlayerController : MonoBehaviour
{
void UpdateMouseCousor()
{
if (Input.GetMouseButton(0))
return;
...
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);
_cursorType = CursorType.Attack;
}
}
else
{
if(_cursorType != CursorType.Hand)
{
Cursor.SetCursor(_handIcon, new Vector2(_handIcon.width / 3, 0), CursorMode.Auto);
_cursorType = CursorType.Hand;
}
}
}
}
...
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);
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;
}
}
}
Test
Attack
Mouse Event
- InputManager.cs
- Define Click and Press
public class InputManager
{
...
public Action<Define.MouseEvent> MouseAction = null;
bool _pressed = false;
float _pressedTime = 0;
public void OnUpdate()
{
if (EventSystem.current.IsPointerOverGameObject())
return;
...
if (MouseAction != null)
{
if (Input.GetMouseButton(0))
{
if (!_pressed)
{
MouseAction.Invoke(Define.MouseEvent.PointerDown);
_pressedTime = Time.time;
}
MouseAction.Invoke(Define.MouseEvent.Press);
_pressed = true;
}
else
{
if (_pressed)
{
if (Time.time < _pressedTime + 0.2f)
MouseAction.Invoke(Define.MouseEvent.Click);
MouseAction.Invoke(Define.MouseEvent.PointerUp);
}
_pressed = false;
_pressedTime = 0;
}
}
}
...
}
- PlayerController.cs
- included code organization
public class PlayerController : MonoBehaviour
{
public enum PlayerState
{
Die,
Moving,
Idle,
Skill,
}
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
PlayerStat _stat;
Vector3 _destPos;
[SerializeField]
PlayerState _state = PlayerState.Idle;
GameObject _lockTarget;
public PlayerState State
{
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, -1, 0);
break;
}
}
}
...
void UpdateMoving()
{
if (_lockTarget != null)
{
_destPos = _lockTarget.transform.position;
float distance = (_destPos - transform.position).magnitude;
if (distance <= 1)
{
State = PlayerState.Skill;
return;
}
}
Vector3 dir = _destPos - transform.position;
if (dir.magnitude < 0.1f)
{
State = PlayerState.Idle;
}
else
{
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
float moveDist = Mathf.Clamp(_stat.MoveSpeed * Time.deltaTime, 0, dir.magnitude);
nma.Move(dir.normalized * moveDist);
Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green);
if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block")))
{
if (Input.GetMouseButton(0) == false)
State = PlayerState.Idle;
return;
}
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
}
}
...
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;
}
void Update()
{
switch (State)
{
case PlayerState.Die:
UpdateDie();
break;
case PlayerState.Moving:
UpdateMoving();
break;
case PlayerState.Idle:
UpdateIdle();
break;
case PlayerState.Skill:
UpdateSkill();
break;
}
}
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);
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;
}
}
}
Animation
- use CrossFade method
- delete all parameters in PlayerAnimController
- add
ATTACK
Animation from Knight animation resources in PlayerAnimController

UI
- Create sword prefab from knight and paste to player

Test
Hp
- reduce monster’s Hp gage when player attaks monster
UI
- Create Hp bar by
Slider
- Create Prefab

- Change transform



Componenets
- UIManager.cs
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.cs
- if myStat.Attack - targetStat.Defense is negative, then damage is 0
void Start()
{
...
Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
}
...
void OnHitEvent()
{
if(_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
Stat myStat = gameObject.GetComponent<PlayerStat>();
int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
targetStat.Hp -= damage;
}
if (_stopSkill)
State = PlayerState.Idle;
else
State = PlayerState.Skill;
}
- Scripts\UI\WordSpace\UI_HPBar.cs
- ratio controlls hp
- int / int = int, so we need float casting
- add UI_HPBar.cs component to monster
public class UI_HPBar : UI_Base
{
enum GameObjects
{
HPBar,
}
Stat _stat;
public override void Init()
{
Bind<GameObject>(typeof(GameObjects));
_stat = transform.parent.GetComponent<Stat>();
}
private void Update()
{
Transform parent = transform.parent;
transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y);
transform.rotation = Camera.main.transform.rotation;
float ratio = _stat.Hp / (float)_stat.MaxHp;
SetHpRatio(ratio);
}
public void SetHpRatio(float ratio)
{
GetObject((int)GameObjects.HPBar).GetComponent<Slider>().value = ratio;
}
}
- UI_Base.cs
- you should remove all start function in this project
private void Start()
{
Init();
}
Test
Monster AI
- monster can detect player
- monster can follow player
- monster can attack player
State Controll
-
Change state by enum and switch
-
PlayerController.cs
- include code organization
- change PlayerState to Define.State
public class PlayerController : BaseController
{
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
bool _stopSkill = false;
PlayerStat _stat;
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
{
Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green);
if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block")))
{
if (Input.GetMouseButton(0) == false)
State = Define.State.Idle;
return;
}
float moveDist = Mathf.Clamp(_stat.MoveSpeed * Time.deltaTime, 0, dir.magnitude);
transform.position += dir.normalized * moveDist;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
}
}
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>();
PlayerStat myStat = gameObject.GetComponent<PlayerStat>();
int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
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);
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;
}
}
}
- Define.cs
public class Define
{
public enum State
{
Die,
Moving,
Idle,
Skill,
}
...
}
- Scripts\Controllers\BaseController.cs
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
{
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, -1, 0);
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() { }
}
- Scripts\Controllers\MonsterController.cs
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
{
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
nma.SetDestination(_destPos);
nma.speed = _stat.MoveSpeed;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
}
}
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;
}
}
}
Animation
- MonsterAnimController

Components
- Remove NavMeshAgent Component from player
- Add
player
tag in player


Test
Destroy
- you cannot make player to null directly
- because other managers or componenets still use player
Prefab
- Player
- remove original player prefab and create new player prefab(Drag and Drop hierarchy object to Prefabs folder)
- Monster
- remove original monster prefab and create new monster prefab(Drag and Drop hierarchy object to Prefabs folder)
Componenets
- Define.cs
public class Define
{
public enum WorldObject
{
Unkown,
Player,
Monster,
}
...
}
- BaseController.cs
public abstract class BaseController : MonoBehaviour
{
...
public Define.WorldObject WorldObjectType { get; protected set; } = Define.WorldObject.Unkown;
...
}
- Manager.cs
public class Managers : MonoBehaviour
{
static Managers s_instance;
static Managers Instance{ get { Init(); return s_instance; } }
#region Contents
GameManagerEx _game = new GameManagerEx();
public static GameManagerEx Game { get { return Instance._game; } }
#endregion
#region Core
DataManager _data = new DataManager();
InputManager _input = new InputManager();
PoolManager _pool = new PoolManager();
ResourceManager _resource = new ResourceManager();
SceneManagerEx _scene = new SceneManagerEx();
SoundManager _sound = new SoundManager();
UIManager _ui = new UIManager();
#endregion
...
}
- MonsterController.cs
public class MonsterController : BaseController
{
...
public override void Init()
{
WorldObjectType = Define.WorldObject.Monster;
...
}
...
void OnHitEvent()
{
if(_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
int damage = Mathf.Max(0, _stat.Attack - targetStat.Defense);
targetStat.Hp -= damage;
if (targetStat.Hp <= 0)
{
Managers.Game.Despawn(targetStat.gameObject);
}
...
}
}
- PlayerController.cs
public class PlayerController : BaseController
{
public override void Init()
{
WorldObjectType = Define.WorldObject.Player;
...
}
...
}
- Extension.cs
- only null checking is not enough
- because player has poolable component
- so you need activate cheking
public static class Extension
{
...
public static bool IsValid(this GameObject go)
{
return go != null && go.activeSelf;
}
}
- CameraController.cs
public class CameraController : MonoBehaviour
{
...
[SerializeField]
GameObject _player = null;
public void SetPlayer(GameObject player) { _player = player; }
...
void LateUpdate()
{
RaycastHit hit;
if (_mode == Define.CameraMode.QuaterView)
{
if(_player.IsValid() == false)
{
return;
}
...
}
}
...
}
- Scripts\Managers\Contents\GameManagerEx.cs
- if you make GameManager.cs, then the icon is wheel
public class GameManagerEx
{
GameObject _player;
HashSet<GameObject> _monsters = new HashSet<GameObject>();
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.Unkown;
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);
}
}
- Folder Organization



Test
- GameScene.cs
protected override void Init()
{
...
GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "UnityChan");
Camera.main.gameObject.GetOrAddComponent<CameraController>().SetPlayer(player);
Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
}
Level
Camera Zoom
- CameraController.cs
- when player is behind buildings, camera will zoom player
void LateUpdate()
{
...
if (_mode == Define.CameraMode.QuaterView)
{
...
if(Physics.Raycast(_player.transform.position, _delta, out hit, _delta.magnitude, 1<<(int)Define.Layer.Block))
{
float dist = (hit.point - _player.transform.position).magnitude * 0.8f;
transform.position = _player.transform.position + _delta.normalized * dist + Vector3.up * 1.5f;
}
...
}
}
Leveling
- GameManagerEx.cs
public class GameManagerEx
{
...
public GameObject GetPlayer() { return _player; }
...
}
- StatData.json
- for player
{
"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"
}
]
}
- Stat.cs
public class Stat : MonoBehaviour
{
...
public void Start()
{
_level = 1;
_hp = 100;
_maxhp = 100;
_attack = 10;
_defense = 5;
_moveSpeed = 5.0f;
}
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 += 15;
}
Managers.Game.Despawn(gameObject);
}
}
- PlayerStat.cs
public class PlayerStat : Stat
{
...
public int Exp
{
get { return _exp; }
set
{
_exp = value;
int level = Level;
while (true)
{
Data.Stat stat;
if (Managers.Data.StatDict.TryGetValue(level + 1, out stat) == false)
break;
if (_exp < stat.totalExp)
break;
level++;
}
if(level != Level)
{
Debug.Log("Level UP!");
Level = level;
SetStat(Level);
}
}
}
...
public 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");
}
}
- MonsterController.cs
public class MonsterController : BaseController
{
Stat _stat;
...
protected override void UpdateIdle()
{
GameObject player = Managers.Game.GetPlayer();
if (player == null)
return;
float distance = (player.transform.position - transform.position).magnitude;
if(distance <= _scanRange)
{
_lockTarget = player;
State = Define.State.Moving;
return;
}
}
...
void OnHitEvent()
{
if(_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
targetStat.OnAttacked(_stat);
...
}
...
}
}
- PlayerController.cs
public class PlayerController : BaseController
{
...
void OnHitEvent()
{
if(_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
targetStat.OnAttacked(_stat);
}
...
}
...
}
Test
Monster Generator
Spawning
-
Monster is generated automatically for same number
-
Scripts\Contents\SpawningPool.cs
public class SpawningPool : MonoBehaviour
{
[SerializeField]
int _monsterCount = 0;
int _reserveCount = 0;
[SerializeField]
int _keepMonsterCount = 0;
[SerializeField]
Vector3 _spawnPos;
[SerializeField]
float _spawnRadius = 15.0f;
[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)
{
Vector3 randDir = Random.insideUnitSphere * Random.Range(0, _spawnRadius);
randDir.y = 0;
randPos = _spawnPos + randDir;
NavMeshPath path = new NavMeshPath();
if (nma.CalculatePath(randPos, path))
break;
}
obj.transform.position = randPos;
_reserveCount--;
}
}
- GameManagerEx.cs
public class GameManagerEx
{
...
public Action<int> OnSpawnEvent;
...
public void Despawn(GameObject go)
{
Define.WorldObject type = GetWorldObjectType(go);
switch (type)
{
case Define.WorldObject.Monster:
{
if (_monsters.Contains(go))
{
_monsters.Remove(go);
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(-1);
}
}
break;
...
}
}
Test
- PlayerController.cs
- to avoid player going up
protected override void UpdateMoving()
{
...
Vector3 dir = _destPos - transform.position;
dir.y = 0;
...
}
- GameScene.cs
protected override void Init()
{
base.Init();
SceneType = Define.Scene.Game;
//Managers.UI.ShowSceneUI<UI_Inven>();
Dictionary<int, Data.Stat> dict = Managers.Data.StatDict;
gameObject.GetOrAddComponent<CursorController>();
GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "UnityChan");
Camera.main.gameObject.GetOrAddComponent<CameraController>().SetPlayer(player);
//Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
GameObject go = new GameObject { name = "SpawningPool" };
SpawningPool pool = go.GetOrAddComponent<SpawningPool>();
pool.SetKeepMonsterCount(5);
}