์ด ๋ ์ปดํฌ๋ํธ๋ ์๋ก ๋ค๋ฅธ ๋ชฉ์ ๊ณผ ์ฌ์ฉ ์ผ์ด์ค์ ๋ฐ๋ผ ์ ๋๋ฉ์ด์ ์ ์ ์ดํ๋ ๋ฐ ์ฌ์ฉ๋๋ค. Animation์ ๋ ๊ฐ๋จํ ์ ๋๋ฉ์ด์ ์ ์ฌ์ฉ๋๋ฉฐ, Animator๋ ๋ ๋ณต์กํ ์ ๋๋ฉ์ด์ ์ํ์ค์ ์ํ ๊ด๋ฆฌ์ ์ฌ์ฉ๋๋ค.
Animation :
Animation ์ปดํฌ๋ํธ๋ ๊ฒ์ ์ค๋ธ์ ํธ์ ์ ๋๋ฉ์ด์ ์ ์ถ๊ฐํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
์ด ์ปดํฌ๋ํธ๋ ์ ๋๋ฉ์ด์ ํด๋ฆฝ์ ์ฌ์ํ ์ ์๋ค.
Animation ์ปดํฌ๋ํธ๋ ๊ฐ๋จํ ์ ๋๋ฉ์ด์ ์ ์ ํฉํ๋ฉฐ, ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ์ง์ ์ ์ดํ ์ ์๋ค.
์ ๋๋ฉ์ด์ ํด๋ฆฝ์ Unity์ Animation window๋ฅผ ํตํด ์์ฑํ๊ฑฐ๋ ํธ์งํ ์ ์๋ค.
์๋ฅผ ๋ค์ด, ์ด ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ค๋ธ์ ํธ์ ํฌ๊ธฐ๋ฅผ ๋ณ๊ฒฝํ๊ฑฐ๋ ์์์ ๋ณํํ๋ ๋ฑ์ ๊ฐ๋จํ ์ ๋๋ฉ์ด์ ์ ์์ฑํ ์ ์๋ค.
Animator
Animator ์ปดํฌ๋ํธ๋ ์ ๋๋ฉ์ด์ ์ ์ํ๋ฅผ ์ ์ดํ๊ณ ์ ํ์ ๊ด๋ฆฌํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
Animator ์ปดํฌ๋ํธ๋ Animation Controller๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋๋ฉ์ด์ ์ ๋ณต์กํ ์ํ ๊ธฐ๊ณ๋ฅผ ๊ตฌํํ ์ ์๋ค.
Animator ์ปดํฌ๋ํธ๋ ์ฌ๋ฌ ์ ๋๋ฉ์ด์ ํด๋ฆฝ์ ์กฐ์ ํ๊ณ , ์ ๋๋ฉ์ด์ ๊ฐ์ ์ ํ์ ์ ์ดํ๊ณ , ๋ณต์กํ ์ ๋๋ฉ์ด์ ์ํ์ค๋ฅผ ๊ตฌํํ๋ ๋ฐ ์ ํฉํ๋ค.
์๋ฅผ ๋ค์ด, ์บ๋ฆญํฐ์ ๊ฑท๊ธฐ, ๋ฐ๊ธฐ, ์ ํ ๋ฑ์ ์ ๋๋ฉ์ด์ ์ ๊ด๋ฆฌํ๋ ๋ฐ ์ฌ์ฉ๋ ์ ์๋ค.
Animator ์ปดํฌ๋ํธ๋ Mecanim ์ ๋๋ฉ์ด์ ์์คํ ์ ์ผ๋ถ๋ก์, ์ ๋๋ฉ์ด์ ๋ธ๋ ๋ฉ, ํธ๋ฆฌ, ์ํ ๋จธ์ ๋ฑ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
์ ๋ํฐ ์ ๋๋ฉ์ด์ ์์คํ ์์ ์์ฃผ ์ฌ์ฉ๋๋ ๊ธฐ๋ฅ์ด๋ค. ๋ฌธ์์ด์ ํด์ ๊ฐ์ผ๋ก ๋ณํํ๋ฉฐ, ์ด๋ก์จ ์ฑ๋ฅ์ ๊ฐ์ ํ๊ณ ์ฝ๋์ ๊ฐ๋ ์ฑ์ ์ ์งํ๋ค.
๋ฌธ์์ด ๋น๊ต๋ ์๋์ ์ผ๋ก ์ฐ์ฐ ๋น์ฉ์ด ํฐ ์์ ์ด๋ค. "StringToHash"๋ ๋ฌธ์์ด์ ๊ณ ์ ํ ์ ์ ๊ฐ์ธ ํด์ ๊ฐ์ผ๋ก ๋ณํํ์ฌ ์ด ์ฐ์ฐ ๋น์ฉ์ ํฌ๊ฒ ์ค์ผ ์ ์๋ค.
ํด์ ๊ฐ์ ๊ณ ์ ํ๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๋ฌธ์์ด์ด ๋์ผํ ํด์ ๊ฐ์ ๊ฐ๋ ํ๋ฅ ์ด ๋งค์ฐ ๋ฎ๋ค. ์ด๋ฅผ ํ์ฉํด ๋ฌธ์์ด์ ํด์๋ก ๋ณํํ๋ฉด ํจ์จ์ ์ธ ๋ฌธ์์ด์ ๋น๊ตํ ์ ์๋ค.
์ ๋๋ฉ์ด์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ง์ ํ ๋ ๋ฌธ์์ด ๋์ ํด์ ๊ฐ์ ์ฌ์ฉํ๋ฉด CPU์๊ฐ์ ์ ์ฝํ๊ณ ์ ๋๋ฉ์ด์ ์ฑ๋ฅ์ ํฅ์์ํฌ ์ ์๋ค.
๊ทธ๋ฌ๋ ์ฃผ์ํ ์ ์ ๋์ผํ ๋ฌธ์์ด์ ํญ์ ๋์ผํ ํด์ ๊ฐ์ ๋ฐํํ์ง๋ง, ๋ฐ๋๋ก ๋์ผํ ํด์ ๊ฐ์ด ํญ์ ๋์ผํ ๋ฌธ์์ด์ ๋ฐํํ์ง๋ ์๋๋ค. ์ด ์ ์ผ๋ก ์ธํด ํด์ ์ถฉ๋์ด ๋ฐ์ํ ์ ์๋ค.
๋ํ, "StringToHash" ํจ์๋ ๋ฌธ์์ด์ ํด์ ๊ฐ์ผ๋ก ๋ณํํ ๋ ์ผ๋ฐฉํฅ์ผ๋ก ์๋ํ๊ธฐ ๋๋ฌธ์, ํด์ ๊ฐ์ ๋ค์ ์๋์ ๋ฌธ์์ด๋ก ๋ณํํ๋ ๊ฒ์ ๋ถ๊ฐ๋ฅํ๋ค.
public class TopDownAnimations : MonoBehaviour
{
protected Animator animator;
protected TopDownCharacterController controller;
protected virtual void Awake()
{
animator = GetComponentInChildren<Animator>();
controller = GetComponent<TopDownCharacterController>();
}
}
public class TopDownAnimationController : TopDownAnimations
{
// StringToHash : ํน์ ํ ๋ฌธ์์ด์ ์ผ์ ํ ๊ณต์์ ์ํด์ ์ซ์๊ฐ(Hash๊ฐ)์ผ๋ก ๋ณํ์ ํ๋ค.
// ์ฌ์ฉํ๋ ์ด์ ? : Animator์์์ ํค๊ฐ์ string์ผ๋ก ์ ๊ณต์ ํ์ ๋, ๋ฌธ์์ด ์ฐ์ฐ์ ๋น์ฉ์ด ๋งค์ฐ ๋๋ค.
// ๊ทธ๋์ ๋ฌธ์์ด์ ๋น๊ตํ์ง ๋ง๊ณ Hash๊ฐ(์ซ์๊ฐ)์ ๋น๊ตํ๋ผ๊ณ ์ฒ๋ฆฌ๋ฅด ํด์ฃผ๋ ๊ฒ์ด๋ค.
// ์ด๋ฏธ ๊ณ ์ ํ Hashtable์ด ์กด์ฌํ๊ธฐ ๋๋ฌธ์ Hash๊ฐ์ผ๋ก ์ค๋ ๋์ผํ๊ฒ ์ฒ๋ฆฌ๊ฐ ์ด๋ฃจ์ด์ง๋ค.
private static readonly int IsWalking = Animator.StringToHash("IsWalking");
private static readonly int Attack = Animator.StringToHash("Attack");
private static readonly int IsHit = Animator.StringToHash("IsHit");
protected override void Awake()
{
base.Awake();
}
void Start()
{
controller.OnAttackEvent += Attacking;
controller.OnMoveEvent += Move;
}
private void Move(Vector2 obj)
{
animator.SetBool(IsWalking, obj.magnitude > .5f);
}
private void Attacking(AttackSO obj)
{
animator.SetTrigger(Attack);
}
private void Hit()
{
animator.SetBool(IsHit, true);
}
private void OnBecameInvisible()
{
animator.SetBool(IsHit, false);
}
}
Player - MainSprte ํด๋ฆญ
Ctrl + 6 ๋๋ Window โ Animation โ Animation ํด๋ฆญ
Creat ํด๋ฆญ โ Animations - Player ํด๋ ์์ฑ โ player_idle ์ด๋ฆ ์ค์ โ ํ์ธ
Samples 12 ์ค์ ( ์์ผ๋ฉด ๋ฐ์ ์ฌ์ง์ฒ๋ผ ์ค์ ๋ณ๊ฒฝ )
knight_f_idle_anim_f0 ~ 3 ์ ํ ํ์ฌ ๋ฃ๊ธฐ
๋ ์นธ์ฉ ๋์ด ๋ฐฐ์น ( ์์ฐ์ค๋ฝ๊ฒ ์์์ ์กฐ์ )
Animations - Player ํด๋ โ player_run ์ด๋ฆ ์ค์ โ ํ์ธ
Samples 12 ์ค์
knight_f_run_anim_f0 ~ 3 ์ ํ ํ ๋ฃ๊ธฐ
Creat New Clip ํด๋ฆญ
Animations - Player ํด๋ โ player_hit ์ด๋ฆ ์ค์ โ ํ์ธ
Samples 12 ์ค์
knight_f_hit-anim_f0 ์ ํ ํ 2ํ ๋ฃ๊ธฐ
Creat New Clip ํด๋ฆญ
Animations - Player ํด๋ โ player_attack ์ด๋ฆ ์ค์ โ ํ์ธ
Samples 12 ์ค์
WeaponSprite์ Position๊ณผ Scale ์ถ๊ฐ
Window โ Animation โ Animator ํด๋ฆญ
Animations/Player/MainSprite ํด๋ฆญ
idle, hit, run ๋จ๊ธฐ๊ณ ์ญ์
Entry ์ฐํด๋ฆญ โ Set StateMachine Default State โ idle ํด๋ฆญ
idle ์ฐํด๋ฆญ โ Make Transition โ run ํด๋ฆญ
Transition ์ค์ ๋ณ๊ฒฝ โ Has Exit Time ์ฒดํฌ ํด์ , Condition ์ถ๊ฐ
run ์ฐํด๋ฆญ โ Make Transition โ idle ํด๋ฆญ
Transition ์ค์ ๋ณ๊ฒฝ โ Has Exit Time ์ฒดํฌ ํด์ , Condition ์ถ๊ฐ
"FindGameObjectWithTag"๋ ์ง์ ๋ ํ๊ทธ์ ์ผ์นํ๋ ์ฒซ ๋ฒ์งธ ํ์ฑ GameObject๋ฅผ ๋ฐํํ๋ค. ์ด ๋ฉ์๋๋ ํน์ ํ๊ทธ๋ฅผ ๊ฐ์ง ์ค๋ธ์ ํธ๋ฅผ ๋น ๋ฅด๊ฒ ์ฐพ์ ์ ์๋๋ก ๋๋๋ค.
ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ ๋ด์์ ํน์ ์ ํ์ ์ค๋ธ์ ํธ๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์์ผ๋ฉฐ, ์ฝ๋์์ ๊ฒ์ ์ค๋ธ์ ํธ๋ฅผ ์ฐธ์กฐํ ๋ ์ ์ฉํ๋ค.
๊ทธ๋ฌ๋ "FindGameObjectWithTag"๋ ๋งค์ฐ ๋น์ผ ์ฐ์ฐ์ด๋ค. ์ฆ, ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด CPU๋ฅผ ๋ง์ด ์ฌ์ฉํ๊ฒ ๋๋ค. ๋ฐ๋ผ์ ์ด ๋ฉ์๋๋ฅผ ๋งค ํ๋ ์๋ง๋ค ํธ์ถํ๋ฉด ๊ฒ์ ์ฑ๋ฅ์ ์ฌ๊ฐํ ์ํฅ์ ๋ฏธ์น ์ ์๋ค.
๋ฐ๋ผ์ ์ผ๋ฐ์ ์ผ๋ก๋ ์ด ๋ฉ์๋๋ฅผ Start๋ Awake์ ๊ฐ์ ์ด๊ธฐํ ๋ฉ์๋์์ ํ ๋ฒ๋ง ํธ์ถํ๋ ๊ฒ์ด ๊ถ์ฅ๋๋ค. ์ค๋ธ์ ํธ๋ฅผ ์ฐพ์ ํ์๋ ์ฐธ์กฐ๋ฅผ ์ ์ฅํ๊ณ ๋์ค์ ์ฌ์ฌ์ฉํ๋ค.
๊ฒ์ ์ค๋ธ์ ํธ๊ฐ ๋ง์ ํฐ ์ฌ์์๋ ํ๊ทธ ๋์ ๋ ์ด์ด๋ ๋ค๋ฅธ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ํจ์จ์ ์ผ ์ ์๋ค.
Raycasting์ ์ผ๋ จ์ ์ฝ๋ผ์ด๋์ ๊ต์ฐจํ๋์ง๋ฅผ ๊ฐ์งํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๊ธฐ์ ์ด๋ค. ์ด๋ ๋ ์ด์ ํฌ์ธํฐ๋ ์ด์์ ์๋ ํจ๊ณผ๋ฅผ ๋ง๋ค๊ฑฐ๋, ํ๋ ์ด์ด์ ์์ผ๋ฅผ ๊ณ์ฐํ๋ ๋ฑ ๋ค์ํ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ๋๋ค.
Unity์์๋ Physics.Raycast
๋๋ Physics2D.Raycast
๋ฅผ ์ฌ์ฉํ์ฌ Raycast๋ฅผ ์ํํ ์ ์๋ค. ์ด๋ฅผ ๋ฉ์๋๋ ์์์ , ๋ฐฉํฅ, ์ต๋ ๊ฑฐ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์ ํ์ ์ผ๋ก ๋ ์ด์ด ๋ง์คํฌ๋ฅผ ๋งค๊ฐ๋ณ์๋ก ๋ฐ๋๋ค.
Raycast๋ hit์ ๋ณด๋ฅผ ๋ฐํํ๋ค. ์ด ์ ๋ณด์๋ ์ถฉ๋ํ ๊ฐ์ฒด, ์ถฉ๋ ์ง์ , ์ถฉ๋ ์ง์ ์ ์ ๊ทํ๋ ๋ฒกํฐ ๋ฑ์ด ํฌํจ๋๋ค.
Raycast๋ ์ถฉ๋ ๊ฒ์ฌ์ ๋นํด ๊ณ์ฐ ๋น์ฉ์ด ๋ง์ด ๋ ๋ค. ๋ฐ๋ผ์ ๋ถํ์ํ Raycast๋ฅผ ์ค์ด๊ณ , ๊ฐ๋ฅํ ๊ฒฝ์ฐ ๋ ์ด์ด ๋ง์คํฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒ์ฌ ๋ฒ์๋ฅผ ์ ํํ๋ ๊ฒ์ด ์ฑ๋ฅ์ ์ข๋ค.
Raycast๋ ๋น์ฃผ์ผ ๋๋ฒ๊น
์ ์ํด Debug.DrawRay
์ ํจ๊ป ์ฌ์ฉํ ์ ์๋ค. ์ด๋ฅผ ํตํด Scene๋ทฐ์์ Raycast์ ๊ฒฝ๋ก๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์๋ค.
public class GameManager : MonoBehaviour
{
public static GameManager instance;
public Transform Player { get; private set; }
[SerializeField] private string playerTag = "Player";
private void Awake()
{
instance = this;
Player = GameObject.FindGameObjectWithTag(playerTag).transform;
}
}
public class TopDownEnemyController : TopDownCharacterController
{
GameManager gameManager;
protected Transform ClosestTarget { get; private set; }
protected override void Awake()
{
base.Awake();
}
protected virtual void Start()
{
gameManager = GameManager.instance;
ClosestTarget = gameManager.Player;
}
protected virtual void FixedUpdate()
{
}
// ๊ฐ๊น์ด ์ ๊ณผ์ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ๋ ๋ฉ์๋
protected float DistanceToTarget()
{
return Vector3.Distance(transform.position, ClosestTarget.position); // ํ์ฌ ์ค๋ธ์ ํธ์ ๊ฐ๊น์ด ํ๊ฒ ๊น์ง์ ๊ฑฐ๋ฆฌ
}
// ํ๊ฒ์ ๋ฐฉํฅ์ ๊ตฌํ๋ ๋ฉ์๋
protected Vector2 DirectionToTarget()
{
// transform.position์์ ClosestTarget.position๋ฅผ ๋ฐ๋ผ๋ณด๋ ๋ฐฉํฅ
return (ClosestTarget.position - transform.position).normalized; // normalized : ์ ๊ทํ ํ์ฌ ๋ฐฉํฅ๋ง ๋จ๊ธด๋ค.
}
}
public class TopDownContactEnemyController : TopDownEnemyController
{
[SerializeField][Range(0f, 100f)] private float followRange;
[SerializeField] private string targetTag = "Player";
private bool _isCollidingWithTarget;
[SerializeField] private SpriteRenderer characterRendere;
protected override void Start()
{
base.Start();
}
protected override void FixedUpdate()
{
base.FixedUpdate();
Vector2 direction = Vector2.zero;
if (DistanceToTarget() < followRange)
{
direction = DirectionToTarget();
}
CallMoveEvent(direction);
Rotate(direction);
}
private void Rotate(Vector2 direction)
{
float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
characterRendere.flipX = Mathf.Abs(rotZ) > 90f;
}
}
public class TopDownRangeEnemyController : TopDownEnemyController
{
[SerializeField] private float followRange = 15f;
[SerializeField] private float shootRange = 10f;
protected override void FixedUpdate()
{
base.FixedUpdate();
float distance = DistanceToTarget();
Vector2 direction = DirectionToTarget();
IsAttacking = false;
if(distance <= followRange)
{
if(distance <= shootRange)
{
int layerMaskTarget = Stats.CurrentStates.attackSO.target;
// Enemy์ Player ์ฌ์ด์ ์งํ(์ฅ์ ๋ฌผ)์ด ์๋ค๋ฉด ๊ณต๊ฒฉ(์๊ฑฐ๋ฆฌ)ํ ํ์๊ฐ ์๋ค. ๋งํ์๋ ์งํ์ด ์๋์ง ๊ฒ์ฌํ๋ ์ฝ๋
RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, 11f, (1 << LayerMask.NameToLayer("Level")) | layerMaskTarget);
if(hit.collider != null && layerMaskTarget == (layerMaskTarget | (1 << hit.collider.gameObject.layer)))
{
CalllLookEvent(direction);
CallMoveEvent(Vector2.zero);
IsAttacking = true;
}
else
{
CallMoveEvent(direction);
}
}
else
{
CallMoveEvent(direction);
}
}
else
{
CallMoveEvent(direction);
}
}
}
๋น ์ค๋ธ์ ํธ ์์ฑ - Orc_Shaman ์ด๋ฆ ๋ณ๊ฒฝ
ํ์์ ๋น ์ค๋ธ์ ํธ ์์ฑ - mainSprite ์ด๋ฆ ๋ณ๊ฒฝ โ Sprite Renderer ์ถ๊ฐ โ orc_shaman_idle_anim_f0 ์ค์ , Order in Layer 4
๋๋ฐฑ์ ๊ฑธ์ด์ฃผ๋ฉด, ์๊ฐ๊ณผ ํ์ ์ฃผ๊ณ ๊ทธ ๊ฐ์ ์ ์ฅํด ๋จ๋ค๊ฐ ๋๋ฐฑ์ ํด์ผํ ๋, ์๋์ ์ผ๋ก ๋๋ฐฑ์ด ์งํ๋๊ณ , duration์ด ์ค์ด๋ค์ด 0์ด๋๋ฉด ๋์ด์ ๋๋ฐฑ์ ๋ํด์๋ ์ฒ๋ฆฌ๋ฅผ ํ์ง ์๋๋ค.
--------------------- ์๋ต ---------------------
private Vector2 _knockback = Vector2.zero;
private float knockbackDuration = 0.0f;
private void FixedUpdate()
{
ApplyMovment(_movementDirection);
if(knockbackDuration > 0.0f) // ์ถ๊ฐ
{
knockbackDuration -= Time.fixedDeltaTime; // ์ถ๊ฐ
}
}
--------------------- ์๋ต ---------------------
public void ApplyKnockback(Transform other, float power, float duration) // ์ถ๊ฐ
{
knockbackDuration = duration; // ์ถ๊ฐ
_knockback = -(other.position - transform.position).normalized * power; // ์ถ๊ฐ
}
// ๋๋ฐฑ์ ๊ฑธ์ด์ฃผ๋ฉด, ์๊ฐ๊ณผ ํ์ ์ฃผ๊ณ ๊ทธ ๊ฐ์ ์ ์ฅํด ๋จ๋ค๊ฐ ๋๋ฐฑ์ ํด์ผํ ๋, ์๋์ ์ผ๋ก ๋๋ฐฑ์ด ์งํ๋๊ณ
// duration์ด ์ค์ด๋ค์ด 0์ด๋๋ฉด ๋์ด์ ๋๋ฐฑ์ ๋ํด์๋ ์ฒ๋ฆฌ๋ฅผ ํ์ง ์๋๋ค.
private void ApplyMovment(Vector2 direction)
{
direction = direction * _stats.CurrentStates.speed;
if(knockbackDuration > 0.0f) // ์ถ๊ฐ
{
direction += _knockback; // ์ถ๊ฐ
}
_rigidbody.velocity = direction;
}
public class HealthSystem : MonoBehaviour
{
[SerializeField] private float healthChangeDelay = .5f;
private CharacterStatsHandler _statsHandler;
private float _timeSinceLastChange = float.MaxValue;
public event Action OnDamage;
public event Action OnHeal;
public event Action OnDeath;
public event Action OnInvincibilityEnd;
public float CurrentHealth { get; private set; }
public float MaxHealth => _statsHandler.CurrentStates.maxHealth;
private void Awake()
{
_statsHandler = GetComponent<CharacterStatsHandler>();
}
private void Start()
{
CurrentHealth = _statsHandler.CurrentStates.maxHealth;
}
private void Update()
{
if (_timeSinceLastChange < healthChangeDelay) // ๋๋ ์ด ์ฒดํฌ
{
_timeSinceLastChange += Time.deltaTime;
if (_timeSinceLastChange >= healthChangeDelay )
{
OnInvincibilityEnd?.Invoke(); // ๋ฌด์ ์๊ฐ ๋
}
}
}
// ์ฒด๋ ฅ ํ๋ณต or ๊ฐ์ ๋ฉ์๋
public bool ChangeHealth(float change)
{
if (change == 0 || _timeSinceLastChange < healthChangeDelay)
{
return false;
}
_timeSinceLastChange = 0f;
CurrentHealth += change;
CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth; // ์ต๋ ์ฒด๋ ฅ์ ๋์ง ๋ชปํ๋ ์ฒ๋ฆฌ
if ( change > 0) // change๊ฐ์ด ์์? ์ฒด๋ ฅ ํ๋ณต
{
OnHeal?.Invoke();
}
else // change๊ฐ์ด ์์? ์ฒด๋ ฅ ๊ฐ์
{
OnDamage?.Invoke();
}
if (CurrentHealth <= 0f)
{
CallDeath();
}
return true;
}
private void CallDeath()
{
OnDeath?.Invoke();
}
}
Player - HealthSystem ์ถ๊ฐ
Goblin, OrcShaman Prefab - HealthSystem ์ถ๊ฐ
--------------------- ์๋ต---------------------
private HealthSystem _healthSystem;
protected override void Awake()
{
base.Awake();
_healthSystem = GetComponent<HealthSystem>(); // ์ถ๊ฐ
}
--------------------- ์๋ต---------------------
void Start()
{
controller.OnAttackEvent += Attacking;
controller.OnMoveEvent += Move;
if(_healthSystem != null) // ์ถ๊ฐ
{
_healthSystem.OnDamage += Hit; // ์ถ๊ฐ
_healthSystem.OnInvincibilityEnd += InvincibilityEnd; // ์ถ๊ฐ
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
// ๋นํธ์ฐ์ฐ 1 << collision.gameObject.layer : collision.gameObject.layer ๋ฅผ ์ผ์ชฝ์ผ๋ก ํ๋ ๋ฐ๊ณ
// levelCollisionLayer.value์ | ์ฐ์ฐ ํ๋๋ผ๋ 1์ด๋ฉด 1
if (levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
{
// ClosestPoint : ๊ฐ์ฅ ๊ฐ๊น์ด position , _direction * .2f : ๋ฒฝ์ ๋ถ๋ชํ ๋ฐ์ ์กฐ๊ธ ์์ชฝ์ผ๋ก ์ค๊ฒ ํ๋ ์ฐ์ฐ
DestoryProjectile(collision.ClosestPoint(transform.position) - _direction * .2f, fxOnDestory);
}
// ๋ฐ์ ๋ค ์ถ๊ฐ
else if (_attackData.target.value == (_attackData.target.value | (1 << collision.gameObject.layer)))
{
HealthSystem healthSystem = collision.GetComponent<HealthSystem>();
if (healthSystem != null)
{
healthSystem.ChangeHealth(-_attackData.power);
if (_attackData.isOnKnockback)
{
TopDownMovement movement = collision.GetComponent<TopDownMovement>();
if (movement != null)
{
movement.ApplyKnockback(transform, _attackData.knockbackPower, _attackData.knockbackTime);
}
}
}
DestoryProjectile(collision.ClosestPoint(transform.position), fxOnDestory);
}
}
Player_RangedAttackData โ Target โ Enemy
Goblin_DefaultAttackData, Orc_Shaman_RangedAttackData โ Target โ Player
private HealthSystem healthSystem;
private HealthSystem _collidingTargetHealthSystem;
private TopDownMovement _collidingMovement;
protected override void Start()
{
base.Start();
healthSystem = GetComponent<HealthSystem>();
healthSystem.OnDamage += OnDamage;
}
// "์์ "์ด ๋ฐ๋ฏธ์ง๋ฅผ ๋ฐ์์ ๋, ์ฒ๋ฆฌํ ํจ์
// ์ด๋ ๊ฒ ๋ง๋ค์ด๋์ง ์์ผ๋ฉด ๊ณต๊ฒฉ์ ๋ฐ์๋ ๋ฐ๋ผ๊ฐ์ง ์๊ณ ๊ฐ๋งํ ์์๋๋ค.
// ๊ทธ๋์ "์์ "์ด ๋ฐ๋ฏธ์ง๋ฅผ ๋ฐ์์ ๋, followRange๋ฅผ ํฌ๊ฒ ๋๋ ค์ฃผ์ด์ ๋ฐ๋ผ์ค๊ฒ ๋ง๋๋ ๊ฒ.
private void OnDamage()
{
followRange = 100f;
}
protected override void FixedUpdate()
{
base.FixedUpdate();
if(_isCollidingWithTarget)
{
ApplyHealthChange();
}
-------------------------- ์๋ต --------------------------
private void OnTriggerEnter2D(Collider2D collision)
{
GameObject receiver = collision.gameObject;
if(!receiver.CompareTag(targetTag))
{
return;
}
_collidingTargetHealthSystem = receiver.GetComponent<HealthSystem>();
if( _collidingTargetHealthSystem != null )
{
_isCollidingWithTarget = true;
}
_collidingMovement = receiver.GetComponent<TopDownMovement>();
}
private void OnTriggerExit2D(Collider2D collision)
{
if (!collision.CompareTag(targetTag))
{
return;
}
_isCollidingWithTarget = false;
}
private void ApplyHealthChange()
{
AttackSO attackSO = Stats.CurrentStats.attackSO;
bool hasBeenChanged = _collidingTargetHealthSystem.ChangeHealth(-attackSO.power);
if(attackSO.isOnKnockback && _collidingMovement != null )
{
_collidingMovement.ApplyKnockback(transform, attackSO.knockbackPower, attackSO.knockbackTime);
}
}
Circle Collider 2D ์ถ๊ฐ
isTrigger ์ฒดํฌ
public class DisappearOnDeath : MonoBehaviour
{
private HealthSystem _healthSystem;
private Rigidbody2D _rigidbody;
private void Start()
{
_healthSystem = GetComponent<HealthSystem>();
_rigidbody = GetComponent<Rigidbody2D>();
_healthSystem.OnDeath += OnDeath;
}
void OnDeath()
{
// ์ฃฝ์์ ๋, ์์ง์ด๋ฉด ์ด์ํ๋ค. ๊ทธ๋์ ๋ชป ์์ง์ด๊ฒ ๊ณ ์ .
_rigidbody.velocity = Vector3.zero;
// "๋"๋ฅผ ํฌํจํด์ ๊ทธ ํ์์ ์๋ ๋ชจ๋ SpriteRenderer๋ฅผ ์ฐพ์์์ ์ปฌ๋ฌ๋ฅผ ์กฐ์ ( ํฌ๋ช
ํ๊ฒ ์กฐ์ )( ์ฐ์ถ )
foreach (SpriteRenderer renderer in transform.GetComponentsInChildren<SpriteRenderer>())
{
Color color = renderer.color;
color.a = 0.3f;
renderer.color = color;
}
// Behaviour : MonoBehaviour ์์ ์๋(๋ถ๋ชจ) ํด๋์ค (์ปจํธ๋กค ํด๋ฆญ์ผ๋ก ํ๊ณ ๋ค์ด๊ฐ์ ํ์ธ ๊ฐ๋ฅ)
// Behaviour์ด Component์ ํค๊ณ ๋๋ ๊ธฐ๋ฅ์ ๊ด๋ฆฌํ๊ธฐ์ ๋์ํ๋ ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ๊บผ์ค๋ค. (์ฃฝ์์ผ๋๊น)
// Behaviour : Component , Behaviour์ด Component๋ฅผ ์์๋ฐ์
foreach (Behaviour component in transform.GetComponentsInChildren<Behaviour>())
{
component.enabled = false;
}
Destroy(gameObject, 2f);
}
}
ํํฐํด ์์คํ ์ ์ ์ฒ๊ฐ์ ์์ 2D ๋๋ 3D ์ค๋ธ์ ํธ๋ค์ ๊ด๋ฆฌํ๊ณ , ๊ทธ๋ค์ ๋์๊ณผ ์์ ๋ฅผ ์ ์ดํ๋ค. ๊ฐ๊ฐ์ ์์ ์ค๋ธ์ ํธ๋ฅผ 'ํํฐํด'์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค.
ํํฐํด ์์คํ ์ ์ฃผ์ ์ปดํฌ๋ํธ๋ 'emitter'(๋ฐ์ฌ์ฒด), 'particles'(ํํฐํด), 'animator'(์ ๋๋ฉ์ดํฐ), 'renderer'(๋ ๋๋ฌ)๋ฑ์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.
Unity์ ํํฐํด ์์คํ ์ ์๊ฐ์ ๋ฐ๋ฅธ ํํฐํด์ ํ๋์ ์๋ฎฌ๋ ์ด์ ํ๋ฉฐ, ์ด๋ฅผ ์ํด ๊ฐ ํํฐํด์ ๋ํ ์์น, ์๋, ์๋ช , ์์, ํฌ๊ธฐ ๋ฑ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
ํํฐํด ์์คํ ์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ์๋ฅผ ๋ค์ด, ์์คํ ์ ์ต๋ ํํฐํด ์๋ฅผ ์ ํํ๊ฑฐ๋, ํํฐํด์ ์ ์ฉ๋ฒ์๋ฅผ ์ ํํ๋ ๋ฑ์ ๊ธฐ๋ฅ์ด ์๋ค.
์ ๋๋ฉ์ด์ ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๋๋ฉ์ด์ ์ด ์งํ๋๋ ๋์ ์ฝ๋๋ฅผ ์คํ์ํฌ ์ ์๋ค. ์๋ฅผ ๋ค์ด, ์บ๋ฆญํฐ๊ฐ ํน์ ๋์์ ํ ๋ ์ฌ์ด๋๋ฅผ ์ฌ์ํ๊ฑฐ๋, ํน์ ์ ๋๋ฉ์ด์ ํ๋ ์์์ ํํฐํด ์์คํ ์ ๋ฐ์ฌํ๋ ๋ฑ์ ์์ ์ ํ ์ ์๋ค.
์ ๋๋ฉ์ด์ ์ด๋ฒคํธ๋ Unity ์ ๋๋ฉ์ด์ ํธ์ง๊ธฐ์์ ์ค์ ํ ์ ์๋ค. ํธ์ง๊ธฐ๋ฅผ ํตํด ์ ๋๋ฉ์ด์ ํ์๋ผ์ธ์ ์ด๋ฒคํธ๋ฅผ ์ถ๊ฐํ๊ณ , ํด๋น ์ด๋ฒคํธ๊ฐ ํธ์ถํ ํจ์๋ฅผ ์ง์ ํ ์ ์๋ค.
์ ๋๋ฉ์ด์ ์ด๋ฒคํธ๋ ํด๋น ์ ๋๋ฉ์ด์ ํด๋ฆฝ์ด ์ฌ์๋๋ ๊ฒ์ ์ค๋ธ์ ํธ์ ์ฐ๊ฒฐ๋ ๋ชจ๋ ์คํฌ๋ฆฝํธ์์ ํธ์ถํ ์ ์๋ ํจ์๋ฅผ ์คํํ ์ ์๋ค.
์ด๋ฒคํธ๋ ํน์ ํ๋ ์์์๋ง ์คํ๋๋ฉฐ, ์ ๋๋ฉ์ด์ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋์๋ ์๋์ผ๋ก ์คํ๋์ง ์๋๋ค.
์ ๋๋ฉ์ด์ ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๋๋ฉ์ด์ ๊ณผ ์ฝ๋์ ์ํธ์์ฉ์ ๋์ฑ ์ ์ฐํ๊ฒ ๊ด๋ฆฌํ ์ ์๋ค. ์ด๋ฅผ ํตํด ์ ๋๋ฉ์ด์ ์ ์๊ฐ์ ํจ๊ณผ์ ์ฌ์ด๋ ํจ๊ณผ ๋ฑ์ ํ๋ก๊ทธ๋๋ฐ์ ์์๋ฅผ ์กฐํ๋กญ๊ฒ ํตํฉํ ์ ์๋ค.
์ ๋๋ฉ์ด์ ์ด๋ฒคํธ๋ฅผ ํตํด ํธ์ถ๋๋ ํจ์๋ ์ผ๋ฐ์ ์ผ๋ก ๊ณต์ฉ ํจ์(public function)์ด์ด์ผ ํ๋ฉฐ, ๋งค๊ฐ๋ณ์๊ฐ ์๊ฑฐ๋ ์ต๋ ํ๋์ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ์ง ์ ์๋ค.
์ตํ๋จ Renderer - Order in Layer 4
๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝ
public class DustParticleControl : MonoBehaviour
{
[SerializeField] private bool createDustOnWalk = true;
[SerializeField] private ParticleSystem dustParticleSystem;
public void CreateDustParticles()
{
if (createDustOnWalk)
{
// bool๊ฐ์ด ๋ค์ด์ค๋ฉด ํํฐํด์ ๋ฉ์ถ๊ณ ๋ค์ ์์ํ๊ฒ ํ๋ค.
dustParticleSystem.Stop();
dustParticleSystem.Play();
}
}
}
Player - MainSprite โ DustParticleControl ์ถ๊ฐ
Ctrl + 6 ๋๋ Window - Animation - Animation ํด๋ฆญ
player_run 1, 3 ํ๋ ์ ์ ๋๋ฉ์ด์ ์ด๋ฒคํธ ์ถ๊ฐ
์ตํ๋จ Renderer - Order in Layer 4
๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝ
public void CreateImpactParticlesAtPosition(Vector3 position, RangedAttackData attackData)
{
_impactParticleSystem.transform.position = position; // ํํฐํด ์์คํ
์ ํด๋น ์์น๋ก ๋ณด๋ธ๋ค.
ParticleSystem.EmissionModule em = _impactParticleSystem.emission; // EmissionModule์ ๊ฐ์ ธ์จ๋ค EmissionModule ?: ์์ฑํ๋ ๋๋
em.SetBurst(0, new ParticleSystem.Burst(0, Mathf.Ceil(attackData.size * 5))); // ์ฌ์ด์ฆ๋ณ๋ก ํฌ๊ธฐ๋ฅผ ๋ค๋ฅด๊ฒ, ์ถฉ๊ฒฉํ๊ฐ ์ปค์ง๊ฒ
ParticleSystem.MainModule mainModule = _impactParticleSystem.main; // mainํํฐํด ์์คํ
์ ๊ฐ์ ธ์จ๋ค.
mainModule.startSpeedMultiplier = attackData.size * 10f; // ์ฒ์์ ์๋๋ฅผ ๊ณฑํด์ฃผ๋ ๊ฐ์ ๋ง๋ค์ด์ค๋ค. ( ์ค์ ์ฌ์ด์ฆ์ ๋น๋กํ๊ฒ ์ปค์ง๊ฒ ๋ง๋ค์ด์ค๋ค. )
_impactParticleSystem.Play();
// ์ฐ๋ฆฌ๊ฐ ๋ง๋ ๊ฑธ ๊ทธ ์์น๋ก ๊ฐ์ ธ๊ฐ์ ์๋๊ฐ ๊ฐ์ง๊ณ ์๋ EmissionModule์ด๋ MainModule์ ๊ฐ์ง๊ณ ์
// ํ๋ฒ Play๋ฅผ ๋์ง๋ ๊ฒ์ด๋ค. ๊ทธ๋ฌ๊ณ ๋ ๋ค๋ฅธ๊ณณ์ ๊ฐ์ ๋ ๋์ง๊ณ
}
void DestoryProjectile(Vector3 position, bool createFx)
{
if(createFx)
{
_projectileManager.CreateImpactParticlesAtPosition(position, _attackData);
}
gameObject.SetActive(false);
}
AudioClip :
AudioClip์ ์ฌ์ด๋ ํ์ผ์ ์ ๋ํฐ์์ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ ๋ฐ์ดํฐ ํ์ ์ด๋ค.
.wav, .mp3, .ogg ๋ฑ ๋ค์ํ ํ์์ ์ค๋์ค ํ์ผ์ ์ง์ํ๋ค.
AudioSource :
AudioSource ์ปดํฌ๋ํธ๋ ์ฌ์ด๋๋ฅผ ์ฌ์ํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
AudioSource์ AudioClip์ ์ฐ๊ฒฐํ์ฌ ์ฌ์ํ ์ ์๋ค.
AudioSource๋ 3D์ฌ์ด๋ ์ค์ , ๋ณผ๋ฅจ ์กฐ์ , ์ฌ์ด๋ ๋ฐ๋ณต ์ฌ์ ๋ค์ ์ค์ ์ ์ ๊ณตํ๋ค.
AudioListener :
AudioListener ์ปดํฌ๋ํธ๋ ์ฌ์ด๋๋ฅผ ๋ฃ๋ ํฌ์ธํธ๋ฅผ ๋ํ๋ธ๋ค.
์ผ๋ฐ์ ์ผ๋ก ์ฃผ์ ์นด๋ฉ๋ผ์ AudioListener๊ฐ ์์นํ๋ค.
๊ฒ์์๋ ํ๋์ AudioListener๋ง ์์ด์ผ ํ๋ค.
public class SoundManager : MonoBehaviour
{
public static SoundManager instance;
[SerializeField][Range(0f, 1f)] private float soundEffectVolume;
[SerializeField][Range(0f, 1f)] private float soundEffectPitchVariance;
[SerializeField][Range(0f, 1f)] private float musicVolume;
private ObjectPool objectPool;
// AudioSource : ์ค์ ๋ก ์ฌ์ด๋๋ฅผ ์ถ๋ ฅํ ์
// Scene ์ด๋๊ฐ์ AudioListener๊ฐ ์๋ฆฌ๋ฅผ ๋ค์ด์ฃผ๋ ์ญํ ๋ณดํต Camera(์นด๋ฉ๋ผ)์ ๋ฌ๋ ค์๋ค.
// ์ด ๊ฒ์์ 2D์ด๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก 3D ์ฌ์ด๋๋ฅผ ์ค์ ํ์ง ์๋๋ค. ๊ทธ๋์ ์ด๋์๋ ์๋ฆฌ๊ฐ ๋ค๋ฆฐ๋ค.
// 3D ๊ฒ์์์๋ ๊ฑฐ๋ฆฌ์ ๋ฐ๋ผ์ ์ค๋์ค์ ์ฐจ์ด๊ฐ ์๊ธฐ ๋๋ฌธ์, ๊ทธ๋ฐ ์ฒ๋ฆฌ๋ค๋ ์ด๋ฃจ์ด์ง ์ ์๋ค.
private AudioSource musicAudioSource;
public AudioClip musicClip; // AudioClip : ์ค์ ์์
private void Awake()
{
instance = this;
musicAudioSource = GetComponent<AudioSource>();
musicAudioSource.volume = musicVolume;
musicAudioSource.loop = true;
objectPool = GetComponent<ObjectPool>();
}
private void Start()
{
ChangeBackGroundMusic(musicClip);
}
// static ๋ฉ์๋๋ค์ static๋ณ์๋ค๋ง ์ฌ์ฉํ ์ ์๋ค. (์ ์ ์ธ ์ ๋ค๋ผ๋ฆฌ๋ง ์ฌ์ฉ ๊ฐ๋ฅ)
// ๊ทธ๋ ๊ธฐ์ ์ ์ ์ธ ๋ณ์ instance๋ฅผ ํตํด์๋ง ๊ทธ ๊ฐ์ฒด์ ๊ฐ์ ๊ฐ์ ธ์ค๊ณ ์ฌ์ฉํ ์ ์๋ค.
public static void ChangeBackGroundMusic(AudioClip music)
{
instance.musicAudioSource.Stop();
instance.musicAudioSource.clip = music;
instance.musicAudioSource.Play();
}
public static void PlayClip(AudioClip clip)
{
GameObject obj = instance.objectPool.SpawnFromPool("SoundSource");
obj.SetActive(true);
SoundSource soundSource = obj.GetComponent<SoundSource>();
soundSource.Play(clip, instance.soundEffectVolume, instance.soundEffectPitchVariance);
}
}
// Sound Clip์ ์ปจํธ๋กค ํ๊ธฐ ์ํ ํด๋์ค
public class SoundSource : MonoBehaviour
{
private AudioSource _audioSource;
public void Play(AudioClip clip, float soundEffectVolume, float soundEffectPitchVariance)
{
if (_audioSource == null)
_audioSource = GetComponent<AudioSource>();
CancelInvoke(); // ์ด์ ์ Invoke๊ฐ ๋จ์์๋ ์ํ์ฌ์ ์ฌ์ด๋๋ฅผ ์คํํด์ผ ํ๋๋ฐ ๊บผ์ ธ๋ฒ๋ฆฌ๋๊ฑธ ๋๋น Invoke์บ์ฌ ( ์ทจ์ )
// ๋ค์ ๋ฑ๋ก
_audioSource.clip = clip;
_audioSource.volume = soundEffectVolume;
_audioSource.Play();
_audioSource.pitch = 1f + Random.Range(-soundEffectPitchVariance, soundEffectPitchVariance);
Invoke("Disable", clip.length + 2); // clip.length + 2 ์๊ฐ ์ดํ์ Disable(); ์คํ
}
// ๋๋ ์์
๋ฉ์๋
public void Disable()
{
_audioSource.Stop();
gameObject.SetActive(false); // ์ฌ์ฌ์ฉ ๋๋น ํ์ฑํ / ๋นํ์ฑํ
}
}
GameManager ํ์์ SoundManager ๋น ์ค๋ธ์ ํธ ์์ฑ
SoundManager ์คํฌ๋ฆฝํธ ์ถ๊ฐ
๋น ์ค๋ธ์ ํธ ์์ฑ โ SoundSource ์ด๋ฆ ๋ณ๊ฒฝ
AudioSource ์ปดํฌ๋ํธ ์ถ๊ฐ
SoundSource ์คํฌ๋ฆฝํธ ์ถ๊ฐ
ํ๋ฆฌํน์ผ๋ก ๋ง๋ค๊ธฐ
public AudioClip shootingClip;
----------------- ์๋ต -----------------
private void CreatProjectile(RangedAttackData rangedAttackData, float angle)
{
// ( ๋ฐ์ฌ ์์น, ํ์ ๊ฐ, ๊ณต๊ฒฉ์ ๋ณด ) ๋ฅผ ๋๊ฒจ์ค๋ค.
_projectileManager.ShootBullet(projectileSpawnPosition.position, RotateVector2(_aimDirection, angle), rangedAttackData);
if (shootingClip)
SoundManager.PlayClip(shootingClip);
}
public AudioClip damageClip;
----------------- ์๋ต -----------------
public bool ChangeHealth(float change)
{
----------------- ์๋ต -----------------
if ( change > 0) // change๊ฐ์ด ์์? ์ฒด๋ ฅ ํ๋ณต
{
OnHeal?.Invoke();
}
else // change๊ฐ์ด ์์? ์ฒด๋ ฅ ๊ฐ์
{
OnDamage?.Invoke();
if (damageClip)
SoundManager.PlayClip(damageClip);
}
----------------- ์๋ต -----------------
}
TopDownShooting - DM-CGS-20 ์ถ๊ฐ
HealthSystem - DM-CGS-44 ์ถ๊ฐ
Unity์ ๊ธฐ๋ณธ UI์์คํ ์ผ๋ก ๊ฒ์ ๋ด์ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌ์ถํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
Canvas :
UGUI์์ ๋ชจ๋ UI์์๋ Canvas๋ผ๋ ์ปดํฌ๋ํธ ๋ด์ ๋ฐฐ์น๋๋ค.
Canvas๋ ์คํฌ๋ฆฐ ๊ณต๊ฐ, ์๋ ๊ณต๊ฐ, ์นด๋ฉ๋ผ ๊ณต๊ฐ์ 3๊ฐ์ง ๋ ๋ ๋ชจ๋๋ฅผ ์ง์ํ๋ค.
Rect Transform :
Unity์ ๊ธฐ๋ณธ Transform ๋์ UI์์์๋ Rect Transform์ด ์ฌ์ฉ๋๋ค.
์์น, ํฌ๊ธฐ, ํ์ , ์ค์ผ์ผ์ ์ง์ ํ๋๋ฐ ์ฌ์ฉ๋๋ฉฐ, ์ต์ปค ๋ฐ ํผ๋ฒ์ ์ฌ์ฉํ์ฌ ๋ถ๋ชจ์์ ์๋์ ์ธ ์์น๋ฅผ ์ง์ ํ๋ค.
UI Components :
Event System
UGUI์ ์ด๋ฒคํธ ์์คํ ์ UI์ํธ์์ฉ์ ๊ด๋ฆฌํ๋ค.
๋ง์ฐ์ค ํด๋ฆญ, ๋๋๊ทธ, ํค๋ณด๋ ์ ๋ ฅ ๋ฑ ๋ค์ํ ์ ๋ ฅ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ค.
TextMeshPro๋ ์๋ ๋ ๋ฆฝ์ ์ธ ๊ฐ๋ฐ์์ ์ํด Unity ์์ ์คํ ์ด์์ ํ๋งค๋์๋ค. ๊ทธ๋ฌ๋ ์ดํ Unity Technologies์ ์ธ์๋์ด Unity์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ผ๋ก ํฌํจ๋์๋ค. ์ด๋ฌํ ๋ณํ๋ 2017๋ ์ ๋ฐ์ํ์ผ๋ฉฐ, ๊ทธ ๋ฆฌํ๋ก TextMeshPro๋ Unity ์ฌ์ฉ์๋ค์๊ฒ ๋ฌด๋ฃ๋ก ์ ๊ณต๋๊ณ ์๋ค.
TextMeshPro๋ Unity์์ ์ ๊ณตํ๋ ๊ณ ๊ธ ํ ์คํธ ๋ ๋๋ง ์์คํ ์ด๋ค. ๊ธฐ๋ณธ ํ ์คํธ ๊ตฌ์ฑ ์์๋ณด๋ค ํจ์ฌ ๋ ๋ง์ ๊ธฐ๋ฅ๊ณผ ์ ํ์ฑ์ ์ ๊ณตํ๋ค.
ํ์์ UI - Image ์ถ๊ฐ
Alt ๋๋ฅธ ์ํ๋ก Stretch ํด๋ฆญ
ํ์์ UI - Slider ์ถ๊ฐ
Handle Slide Area ์ญ์
Background Color โ Wave Image ๊ฐ์ Color
Fill Area - Fill Color ๋ถ์ ๊ณ์ด๋ก ๋ณ๊ฒฝ
Wave ์ฐธ๊ณ ํ์ฌ ๋ค์๊ณผ ๊ฐ์ด ์์
Image, Text, Button * 2 ์ถ๊ฐ
Image ์ ์ฒด ์ฌ์ด์ฆ๋ก ๋๋ฆฌ๊ธฐ, ์ํ๊ฐ ์ค์ด๊ธฐ
GameManager๊ฐ ์๋ UIManager๋ฅผ ๋ฐ๋ก ํ์ฉํ์ฌ ๊ด๋ฆฌํ๋ ๊ฒ์ด ๋์ค์ ์๊ฐํ๋ฉด ๋ ์ข์๋ฏ ํ๋ค.
public class GameManager : MonoBehaviour
{
public static GameManager instance;
public Transform Player { get; private set; }
[SerializeField] private string playerTag = "Player";
private HealthSystem playerhealthSystem;
[SerializeField] private TextMeshProUGUI waveText;
[SerializeField] private Slider hpGaugeSlider;
[SerializeField] private GameObject gameOverUI;
private void Awake()
{
instance = this;
Player = GameObject.FindGameObjectWithTag(playerTag).transform;
playerhealthSystem = Player.GetComponent<HealthSystem>();
playerhealthSystem.OnDamage += UpdateHealthUI;
playerhealthSystem.OnHeal += UpdateHealthUI;
playerhealthSystem.OnDeath += GameOver;
gameOverUI.SetActive(false);
}
private void UpdateHealthUI()
{
hpGaugeSlider.value = playerhealthSystem.CurrentHealth / playerhealthSystem.MaxHealth;
}
private void GameOver()
{
gameOverUI.SetActive(true);
}
private void UpdateWaveUI()
{
// waveText.text =
}
public void RestartGame()
{
// GetActiveScene().buildIndex : ์ง๊ธ ์ผ์ ธ์๋ Scene์ ๋ฒํธ๋ฅผ ๊ฐ์ ธ์์ ๋ค์ Load๋ฅผ ์์ผ๋ผ.
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public void ExitGame()
{
Application.Quit();
}
}
GameManager์ ์ค์
๋ฉ์๋๋ฅผ ์ ํ
์ฝ๋ฃจํด์ ๋น๋๊ธฐ์ ์ผ๋ก ์คํ๋๋ ํจ์๋ก, ํน์ ์ฝ๋ ๋ธ๋ญ์ ์คํ์ ์ผ์์ ์ผ๋ก ์ค์งํ๊ณ ๋ค์ ์์ํ ์ ์๊ฒ ํด์ค๋ค.
IEnumerator
๋ฆฌํด ํ์
์ ํจ์์์ yield return
์ ์ฌ์ฉํ์ฌ ์ฝ๋ฃจํด์ ๊ตฌํํ ์ ์๋ค.
StartCoroutine
ํจ์๋ฅผ ํตํด ์ฝ๋ฃจํด์ ์์ํ ์ ์๊ณ , StopCoroutine
ํจ์๋ฅผ ํตํด ์ฝ๋ฃจํด์ ์ค์งํ ์ ์๋ค.
์ฝ๋ฃจํด์ ํ๋ ์ ๊ฐ์ ์ง์ฐ, ๋น๋๊ธฐ ์์ , ์๊ฐ์ ๋ฐ๋ฅธ ์ ๋๋ฉ์ด์ ๋ฑ์ ์์ ์ ์ฃผ๋ก ์ฌ์ฉ๋๋ค.
yield return null
์ ๋ค์ ํ๋ ์๊น์ง ๋๊ธฐ๋ฅผ ์๋ฏธํ๊ณ , yield return new WaitForSeconds(n)
์ n์ด ๋์ ๋๊ธฐ๋ฅผ ์๋ฏธํ๋ค.
์ฝ๋ฃจํด์ ๋ณ๋์ ์ค๋ ๋์์ ์คํ๋์ง ์๋๋ค. ๋ฐ๋ผ์ Unity์ ๋ฉ์ธ ์ค๋ ๋์์ ์์ ํ๊ฒ Unity API๋ฅผ ํธ์ถํ ์ ์๋ค.
์ฝ๋ฃจํด์ ์ผ๋ฐ ํจ์์๋ ๋ค๋ฅด๊ฒ ์คํ์ ์ผ์ ์ค๋จํ๊ณ ๋์ค์ ๋ค์ ์์ํ ์ ์์ด, ์๊ฐ ์ง์ฐ, ๋ฐ๋ณต, ์กฐ๊ฑด๋ถ ๋๊ธฐ ๋ฑ์ ์์ ์ ์ํํ ๋ ๋งค์ฐ ์ ์ฉํ๋ค.
GameManager
----------------------- ์๋ต -----------------------
[SerializeField] private int currentWaveIndex = 0;
private int currentSpawnCount = 0;
private int waveSpawnCount = 0;
private int waveSpawnPosCount = 0;
public float spawnInterval = .5f;
public List<GameObject> enemyPrefabs = new List<GameObject>(); // ๋์
๋๋ฆฌ๋ก ๋ค์ํ๊ฒ ๊ทธ๋ฃน์ ๋ง๋ค์ด์ ์คํฌ๋ง ๋์ฌ๋๋ ์คํฌ ๊ณ ๋ธ๋ฆฐ์ด ๋์ฌ๋ ๊ณ ๋ธ๋ฆฐ ๊ทธ๋ ๊ฒ๋ ๊ฐ๋ฅ
[SerializeField] private Transform spawnPositionsRoot;
private List<Transform> spawnPositions = new List<Transform>();
----------------------- ์๋ต -----------------------
private void Awake()
{
instance = this;
Player = GameObject.FindGameObjectWithTag(playerTag).transform;
playerhealthSystem = Player.GetComponent<HealthSystem>();
playerhealthSystem.OnDamage += UpdateHealthUI;
playerhealthSystem.OnHeal += UpdateHealthUI;
playerhealthSystem.OnDeath += GameOver;
gameOverUI.SetActive(false);
// spawnPosition์ ์์น๋ค์ ๋ค ๊ฐ์ ธ์์ ์ ์ฅ์ํจ๋ค.
for (int i = 0; i < spawnPositionsRoot.childCount; i++)
{
spawnPositions.Add(spawnPositionsRoot.GetChild(i)); // GetChild ๊ฐ transform์ ๋ฐํํ๊ธฐ ๋๋ฌธ์, .tranform์ ์ํด๋ ๋๋ค.
}
}
----------------------- ์๋ต -----------------------
private void Start()
{
StartCoroutine("StartNextWave");
}
// ์ฝ๋ฃจํด : IEnumerator returnํ์
์ ๊ฐ์ ธ์ ๋น๋๊ธฐ์ ์ผ๋ก ์คํํ ์ ์๊ฒ ํด์ค๋ค.
// ์ฝ๋๋ฅผ ์ผ์ ๋ถ๋ถ์์ ์ผ์์ ์ง ํ๊ฑฐ๋ ๋ฉ์ถ๊ฑฐ๋ ๋ค์ ์์ํ๊ฒ ํ๋ ์ฝ๋๋ฅผ ๊ตฌ์ฑํ ์ ์๊ฒ ํด์ค๋ค.
// Thread์์ ๋์ํ๋ ๊ฒ์ ์๋๊ณ Unity Main Thread์์์ ๋์ํ๋ ๊ตฌ๋ถ์ ์ ๊ฐ์ง๊ณ ์ ์ฝ๋๋ฅผ ๋์ํ๊ฒ ํ๋ค.
// ์ผ๋ฐ์ ์ธ 'return'๋ณด๋ค๋ 'yield return'์ด๋ผ๋ ๊ฒ์ ์ด๋ค.
// 'yield return'์ผ๋ก ์ด ์ฝ๋์ ์คํ์ ๋ฐํํด ๋จ๋ค๊ฐ ๋ค์ ๊ทธ ๋ถ๋ถ์ ๋์์จ๋ค.
// 'yield return'์ ๋ง๋๋ฉด ์คํ์ ๋ํ ์์๋ฅผ ๋ฐํํ๋ค๊ฐ ๋ค์ ๋์์์ ๋์ํ๋ค.
IEnumerator StartNextWave()
{
while(true)
{
if (currentSpawnCount == 0) // currentSpawnCount : ์ํ๋์ด ์๋ ๋ชน์ ๊ฐฏ์ (0 ? ์ฒ์ or ๋ค ์ก์)
{
UpdateWaveUI(); // Wave ์ต์ ํ
yield return new WaitForSeconds(2f); // new WaitForSeconds ๋ ๋ณ์๋ก ์บ์ฑํด์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค.
if (currentWaveIndex % 10 == 0) // currentWaveIndex ๊ฐ 10์ ๋ฐฐ์๋ค? ( 10 ๋จ์๋ก ๋์ด๋ ์ฆ๊ฐ )
{
// ์์ฑํ๋ ํฌ์ง์
์ฆ๊ฐ
waveSpawnPosCount = waveSpawnPosCount + 1 > spawnPositions.Count ? waveSpawnPosCount : waveSpawnPosCount + 1;
}
if (currentWaveIndex % 5 == 0)
{
}
if (currentWaveIndex % 3 == 0)
{
waveSpawnCount += 1; // ํ๋ฒ์ ๋ง๋ค์ด์ง๋ ๋ชฌ์คํฐ ์ฆ๊ฐ
}
// ๋ช ๊ตฐ๋ฐ์ ์ผ๋ง์ฉ ์์ฑ?
for(int i = 0; i < waveSpawnPosCount; i++) // ์์น ๊ฐฏ์
{
int posIdx = Random.Range(0, spawnPositions.Count); // ๋๋ค ์์น์
for(int j = 0; j < waveSpawnCount; j++) // ๋ง๋๋ ๊ฐฏ์
{
int prefabIdx = Random.Range(0, enemyPrefabs.Count); // ๋๋ค ๋ชฌ์คํฐ
GameObject enemy = Instantiate(enemyPrefabs[prefabIdx], spawnPositions[posIdx].position, Quaternion.identity);
enemy.GetComponent<HealthSystem>().OnDeath += OnEnemyDeath;
// enemy.GetComponent<CharacterStatsHandler>
currentSpawnCount++;
yield return new WaitForSeconds(spawnInterval);
}
}
currentWaveIndex++;
}
yield return null;
}
}
private void OnEnemyDeath()
{
currentSpawnCount--;
}
private void UpdateHealthUI()
{
hpGaugeSlider.value = playerhealthSystem.CurrentHealth / playerhealthSystem.MaxHealth;
}
private void GameOver()
{
gameOverUI.SetActive(true);
StopAllCoroutines(); // Coroutin์ด๋ผ๋ ๊ฒ ์์ฒด๊ฐ ์ค๋ธ์ ํธ์ ๊ท์๋๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์ด ์ค๋ธ์ ํธ์์ ๋๊ณ ์๋ ์ฝ๋ฃจํด์ ๋ค ๋ฉ์ถฐ๋ผ.
}
private void UpdateWaveUI()
{
waveText.text = (currentWaveIndex + 1).ToString(); // 0๋ถํฐ ์์ํ๊ธฐ์ + 1
}
Level ํ์ ๋น ์ค๋ธ์ ํธ ์์ฑ โ SpawnPositions ์ด๋ฆ ๋ณ๊ฒฝ
ํ์ ๋น ์ค๋ธ์ ํธ ์์ฑ
์ธ์คํํฐ์์ ๊ธฐ์ฆ๋ชจ ์ค์
Switch๋ฌธ : ํ๋ก๊ทธ๋๋ฐ์์ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅธ ๋์์ ์ํํ๋๋ก ํ๋ ์ ์ด ๊ตฌ์กฐ. ๋ค์ํ ๊ฒฝ์ฐ์ ์๋ฅผ ๊ฐ์ง ์กฐ๊ฑด์ ์ฒ๋ฆฌํ ๋ ํจ์จ์ .
ํจํด ๋งค์นญ : C# 7.0๋ถํฐ ์ถ๊ฐ๋ ๊ธฐ๋ฅ์ผ๋ก, ์ผ์นํ๋ ํจํด์ ๋ฐ๋ผ ์ฝ๋๋ฅผ ์คํ. switch
๋ฌธ์ ์ด์ฉํ์ฌ ๊ฐ์ฒด์ ํ์
์ด๋ ๊ฐ์ ๋ฐ๋ผ ์ฒ๋ฆฌ๋ฅผ ๋ค๋ฅด๊ฒ ํ ์ ์๋ค.
ํจํด ๋งค์นญ in switch๋ฌธ : ์ฝ๋์ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ ์ ์ง ๋ณด์๋ฅผ ์ฝ๊ฒ ํ๋๋ฐ ๋์์ ์ค๋ค. ๊ฐ์ฒด์ ํ์ ์ ํ์ธํ๊ฑฐ๋ ํน์ ์กฐ๊ฑด์ ์ถฉ์กฑํ๋์ง ์ฌ๋ถ๋ฅผ ํ์ธํ๋ฉด์ ๋์์ ๋ณ์์ ๊ฐ์ ํ ๋น ํ ์ ์๋ค.
์์ :
- **`case RangedAttackData _:`** **`CurrentStats.attackSO`**๊ฐ **`RangedAttackData`**ํ์
์ผ ๊ฒฝ์ฐ์ ์คํ.
case MeleeAttackConfig _:
CurrentStats.attackSO
๊ฐ MeleeAttackConfig
ํ์
์ผ ๊ฒฝ์ฐ์ ์คํ์ด๋ฐ ๋ฐฉ์์ ์ด์ฉํ๋ฉด, ์ฌ๋ฌ ํ์ ์ ๊ฐ์ฒด๋ฅผ ์ ์ ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด ํ์ฅ์ฑ๊ณผ ์ ์ฐ์ฑ์ด ํฅ์๋๋ค.
public class CharacterStatsHandler : MonoBehaviour
{
private const float MinAttackDelay = 0.03f;
private const float MinAttackPower = 0.5f;
private const float MinAttackSize = 0.4f;
private const float MinAttackSpeed = 0.1f;
private const float MinSpeed = 0.8f;
private const int MinMaxHealth = 5;
[SerializeField] private CharacterStats baseStats;
public CharacterStats CurrentStats { get; private set; }
public List<CharacterStats> statsModifiers = new List<CharacterStats>();
private void Awake()
{
UpdateCharacterStats();
}
#region Stats Add || Remove
// AddStatModifier();, RemoveStatModifier() : ์๋ก์ด ์บ๋ฆญํฐ ์คํฏ์ ์ถ๊ฐํ๊ฑฐ๋ ๋บ์์๊ฒ
// ์) ์์ดํ
์ ์ฅ์ฐฉํ๋ค ? ์์ดํ
์ ๋ํ ์คํฏ์ด ํฌํจ์ด ๋๊ณ ์๋ ์คํฏ์๋ค ์ด ์คํฏ์ ์ถ๊ฐํ๋ค.
public void AddStatModifier(CharacterStats statModifier)
{
statsModifiers.Add(statModifier);
UpdateCharacterStats();
}
public void RemoveStatModifier(CharacterStats statModifier)
{
statsModifiers.Remove(statModifier);
UpdateCharacterStats();
}
#endregion
#region UpdateStats
private void UpdateCharacterStats()
{
AttackSO attackSO = null;
if (baseStats.attackSO != null)
{
attackSO = Instantiate(baseStats.attackSO);
}
CurrentStats = new CharacterStats { attackSO = attackSO }; // CurrentStats ์์ฑ, baseStats์ (CurrentStats์๋ค๊ฐ)๋๊ฒจ ๋ฐ๋๋ค.
UpdateStats((a, b) => b, baseStats); // ( (a, b) => b : a, b๋ฅผ ๋ฐ์์ b๋ฅผ ์ฐ๊ฒ ๋ค. ) , (baseStats(CurrnetStats)์ ๊ฐ์ ธ๊ฐ์ UpdateStats์ ์ฐ๊ฒ ๋ค.)
if (CurrentStats.attackSO != null) // attackSO๊ฐ ์๋์ง ์ฒดํฌ
{
CurrentStats.attackSO.target = baseStats.attackSO.target; // ํ๊ฒ ์ค์
}
foreach (CharacterStats modifier in statsModifiers.OrderBy(o => o.statsChangeType)) // statsChangeType์ ๋ง์ถฐ์ OrderBy(์ค๋ฆ์ฐจ์)์ ๋ ฌ
{
if (modifier.statsChangeType == StatsChangeType.Override) // ๋ฎ์ด์์ฐ๊ธฐ
{
UpdateStats((o, o1) => o1, modifier);
}
else if (modifier.statsChangeType == StatsChangeType.Add) // ๋ํ๊ธฐ
{
UpdateStats((o, o1) => o + o1, modifier);
}
else if (modifier.statsChangeType == StatsChangeType.Multiple) // ๊ณฑํ๊ธฐ
{
UpdateStats((o, o1) => o * o1, modifier);
}
}
LimitAllStats(); // ์ต์ ์คํฏ ๋ฆฌ๋ฐ ์ฒดํฌ
}
// Func : ๋งค๊ฐ๋ณ์ ์์ 2๊ฐ๋ฅผ ๋ฐ์์ ๋ฐํ(3๋ฒ์งธ) ๋ฐ์ ๊ฒฝ์ฐ float์ 2๊ฐ ๋ฐ์์ float์ ๋ฐํ
private void UpdateStats(Func<float, float, float> operation, CharacterStats newModifier)
{
CurrentStats.maxHealth = (int)operation(CurrentStats.maxHealth, newModifier.maxHealth);
CurrentStats.speed = operation(CurrentStats.speed, newModifier.speed);
UpdateAttackStats(operation, CurrentStats.attackSO, newModifier.attackSO);
// ๋ด๊ฐ๊ฐ์ง ํ์
(CurrentStats)๊ณผ ๋๊ฒจ๋ฐ์ ํ์
(newModifier)์ด ๋ค๋ฅด๋ฉด ์ฒ๋ฆฌํ์ง ์๋๋ค.
// ์) Ranged Type์ธ๋ฐ ๋ค๋ฅธ ํ์
์ด ๋ค์ด์ค๋ฉด ? ์ฒ๋ฆฌ x
if (CurrentStats.attackSO.GetType() != newModifier.attackSO.GetType())
{
return;
}
// ์๋ก์ด ํ์
์ ์ฒ๋ฆฌ
switch (CurrentStats.attackSO)
{
case RangedAttackData _:
ApplyRangedStats(operation, newModifier);
break;
}
}
// AttackStats
private void UpdateAttackStats(Func<float, float, float> operation, AttackSO currentAttack, AttackSO newAttack)
{
if (currentAttack == null || newAttack == null || currentAttack.GetType() != newAttack.GetType())
{
return;
}
currentAttack.delay = operation(currentAttack.delay, newAttack.delay);
currentAttack.power = operation(currentAttack.power, newAttack.power);
currentAttack.size = operation(currentAttack.size, newAttack.size);
currentAttack.speed = operation(currentAttack.speed, newAttack.speed);
}
// RangedStats
private void ApplyRangedStats(Func<float, float, float> operation, CharacterStats newModifier)
{
RangedAttackData currentRangedAttacks = (RangedAttackData)CurrentStats.attackSO;
if (!(newModifier.attackSO is RangedAttackData))
{
return;
}
RangedAttackData rangedAttackModifier = (RangedAttackData)newModifier.attackSO;
currentRangedAttacks.multipleProjectilesAngel = operation(currentRangedAttacks.multipleProjectilesAngel, rangedAttackModifier.multipleProjectilesAngel);
currentRangedAttacks.spread = operation(currentRangedAttacks.spread, rangedAttackModifier.spread);
currentRangedAttacks.duration = operation(currentRangedAttacks.duration, rangedAttackModifier.duration);
currentRangedAttacks.numberofProjectilesPerShot = Mathf.CeilToInt(operation(currentRangedAttacks.numberofProjectilesPerShot, rangedAttackModifier.numberofProjectilesPerShot));
currentRangedAttacks.projectileColor = UpdateColor(operation, currentRangedAttacks.projectileColor, rangedAttackModifier.projectileColor);
}
// Color(Ranged)
private Color UpdateColor(Func<float, float, float> operation, Color currentColor, Color newColor)
{
return new Color(operation(currentColor.r, newColor.r),
operation(currentColor.g, newColor.g),
operation(currentColor.b, newColor.b),
operation(currentColor.a, newColor.a));
}
#endregion
#region LimitStats
private void LimitStats(ref float stat, float minVal)
{
stat = Mathf.Max(stat, minVal);
}
private void LimitAllStats()
{
if (CurrentStats == null || CurrentStats.attackSO == null)
{
return;
}
LimitStats(ref CurrentStats.attackSO.delay, MinAttackDelay);
LimitStats(ref CurrentStats.attackSO.power, MinAttackPower);
LimitStats(ref CurrentStats.attackSO.size, MinAttackSize);
LimitStats(ref CurrentStats.attackSO.speed, MinAttackSpeed);
LimitStats(ref CurrentStats.speed, MinSpeed);
CurrentStats.maxHealth = Mathf.Max(CurrentStats.maxHealth, MinMaxHealth);
}
#endregion
}
public abstract class PickupItem : MonoBehaviour
{
[SerializeField] private bool destroyOnPickup = true; // Pickupํ์ ๋, ์ญ์ ํ ๊ฒ์ธ๊ฐ?
[SerializeField] private LayerMask canBePickupBy; // ๋ด๊ฐ ๋จน์ ์ ์๋ ๊ฑด์ง Layer๋ก ์ฒดํฌ
[SerializeField] private AudioClip pickupSound;
private void OnTriggerEnter2D(Collider2D other)
{
// ๋ด๊ฐ ๋จน์ ์ ์๋ ๋
์์ธ์ง ๊ฒ์ฌ
if (canBePickupBy.value == (canBePickupBy.value | (1 << other.gameObject.layer)))
{
OnPickedUp(other.gameObject); // ํฝ์
if (pickupSound)
SoundManager.PlayClip(pickupSound); // ์ฌ์ด๋
if (destroyOnPickup)
{
Destroy(gameObject); // ์ญ์
}
}
}
// ์ถ์ ๋ฉ์๋ (์ด ํด๋์ค๋ฅผ ์์๋ฐ๋ ํด๋์ค๋ค์ ์ด ๋ฉ์๋๋ฅผ ๋ฌด์กฐ๊ฑด ๊ตฌํ ํด์ผํ๋ค.)
protected abstract void OnPickedUp(GameObject receiver);
}
public class PickupStatModifiers : PickupItem
{
[SerializeField] private List<CharacterStats> statsModifier;
// ์ด๋ ํ ์คํ
์ ๊ฐ์ง๊ณ ์๋ค๊ฐ CharacterStatsHandler์๋ค ์ถ๊ฐํด์ค๋ค.
protected override void OnPickedUp(GameObject receiver)
{
CharacterStatsHandler statsHandler = receiver.GetComponent<CharacterStatsHandler>();
foreach(CharacterStats stat in statsModifier)
{
statsHandler.AddStatModifier(stat);
}
}
}
public class PickupHeal : PickupItem
{
[SerializeField] int healValue = 10;
private HealthSystem _healthSystem;
protected override void OnPickedUp(GameObject receiver)
{
_healthSystem = receiver.GetComponent<HealthSystem>();
_healthSystem.ChangeHealth(healValue);
}
}
if(currentWaveIndex % 5 ==0)
{
CreateReward();
}
--------------- ์๋ต ---------------
void CreateReward()
{
// spawnPositions ๋๋ค์ค์ ํ๋๋ฅผ ๋ฐ์์ ๊ทธ ์์น์ ์์ฑํ๊ฒ ํ๋ค.
int idx = Random.Range(0, rewards.Count);
int posIdx = Random.Range(0, spawnPositions.Count);
GameObject obj = rewards[idx]; // rewards์ค ํ๋ ๋๋ค ํ ๋น
Instantiate(obj, spawnPositions[posIdx].position, Quaternion.identity);
}
yellow โ Pickup Stat Modifiers ์ถ๊ฐ
Ranged Attack Data ์์ฑ โ Yellow_RangedAttackData ์ด๋ฆ ๋ณ๊ฒฝ
์ ์ ๋ฐฐ์น๋ ์์ดํ ํ๋ฆฌํนํ โ ์ญ์
์ํ๋ ๋งํผ ๋ณด์์ ์ถ๊ฐ
public class GameManager : MonoBehaviour
{
... ์๋ต ...
[SerializeField] private CharacterStats defaultStats;
[SerializeField] private CharacterStats rangedStats;
... ์๋ต ...
private void Start()
{
UpgradeStatInit();
StartCoroutine("StartNextWave");
}
IEnumerator StartNextWave()
{
while(true)
{
if(currentSpawnCount == 0)
{
UpdateWaveUI();
yield return new WaitForSeconds(2f);
if(currentWaveIndex % 20 == 0)
{
RandomUpgrade();
}
... ์๋ต ...
for(int i = 0; i < waveSpawnPosCount;i++)
{
int posIdx = Random.Range(0, spawnPostions.Count);
for(int j = 0; j <waveSpawnCount;j++)
{
int prefabIdx = Random.Range(0,enemyPrefebs.Count);
GameObject enemy = Instantiate(enemyPrefebs[prefabIdx], spawnPostions[posIdx].position, Quaternion.identity);
enemy.GetComponent<HealthSystem>().OnDeath += OnEnemyDeath;
// enemy๊ฐ ์๋ก ์ฐ์ด์ง ๋ ๋ง๋ค GetComponent๋ก ๊ฐ์ ธ์ค๋ฏ๋ก ์จ์ด๋ธ์ ๋ฐ๋ผ ๋๋ค์ผ๋ก ์คํ
์ ์ฌ๋ ค๋ default, ranged ์คํ
์ ์ถ๊ฐ(๋ํ๊ธฐ)ํ์ฌ ๊ฐํ๊ฒ ๋ง๋ ๋ค.
enemy.GetComponent<CharacterStatsHandler>().AddStatModifier(defaultStats);
enemy.GetComponent<CharacterStatsHandler>().AddStatModifier(rangedStats);
currentSpawnCount++;
yield return new WaitForSeconds(spawnInterval);
}
}
currentWaveIndex++;
}
yield return null;
}
}
... ์๋ต ...
void UpgradeStatInit()
{
// ์บ๋ฆญํฐ ์ค๋ธ์ ํธ๋ฅผ ๋ฐ๋ก ์์ ํด๋ฒ๋ฆฌ๋ฉด ๋จ์๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์, ๋ฏธ๋ฆฌ ๋ณต์ฌ๋ฅผ ํ๋ค. (Instantiate)
defaultStats.statsChangeType = StatsChangeType.Add;
defaultStats.attackSO = Instantiate(defaultStats.attackSO);
rangedStats.statsChangeType = StatsChangeType.Add;
rangedStats.attackSO = Instantiate(rangedStats.attackSO);
}
void RandomUpgrade()
{
switch (Random.Range(0,6))
{
case 0:
defaultStats.maxHealth += 2;
break;
case 1:
defaultStats.attackSO.power += 1;
break;
case 2:
defaultStats.speed += 0.1f;
break;
case 3:
defaultStats.attackSO.isOnKnockback = true;
defaultStats.attackSO.knockbackPower += 1;
defaultStats.attackSO.knockbackTime = 0.1f;
break;
case 4:
defaultStats.attackSO.delay -= 0.05f;
break;
case 5:
RangedAttackData rangedAttackData = rangedStats.attackSO as RangedAttackData;
rangedAttackData.numberofProjectilesPerShot += 1;
break;
default:
break;
}
}
}
RangedAttackData, DefaultAttackData ์์ฑ
Default Stats์ Ranged Stats๋ฅผ ์์