using Unity.VisualScripting.InputSystem;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
Rigidbody2D rb;
Vector2 dir;
float moveSpeed;
[SerializeField] float maxHp;
[SerializeField] float nowHp;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
rb = GetComponent<Rigidbody2D>();
moveSpeed = 5f;
}
// Update is called once per frame
void Update()
{
dir = Vector2.zero;
if (Keyboard.current.aKey.isPressed)
{
dir += Vector2.left;
}
if (Keyboard.current.sKey.isPressed)
{
dir += Vector2.down;
}
if (Keyboard.current.dKey.isPressed)
{
dir += Vector2.right;
}
if (Keyboard.current.wKey.isPressed)
{
dir += Vector2.up;
}
dir = dir.normalized;
}
private void FixedUpdate()
{
rb.linearVelocity = dir * moveSpeed;
}
public void TakeDamage(int damage)
{
nowHp -= damage;
if (nowHp < 0)
{
nowHp = 0;
Die();
}
}
void Die()
{
StageManager.instance.ClearMonsterList();
}
}
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public abstract class Weapon : MonoBehaviour
{
protected Camera camera;
[SerializeField] float delay;
protected bool canAttack;
WaitForSeconds wait;
// Start is called once before the first execution of Update after the MonoBehaviour is created
protected virtual void Start()
{
canAttack = true;
delay = 1f;
wait = new WaitForSeconds(delay);
StartCoroutine(AttackDelay());
}
// Update is called once per frame
void Update()
{
}
protected abstract void Attack();
protected void LookMouse()
{
// 화면 기준으로 마우스 위치 좌표(스크린 좌표)
Vector2 mousePos = Mouse.current.position.ReadValue();
// 월드 좌표
Vector3 worldPos = camera.ScreenToWorldPoint(mousePos);
worldPos.z = 0f;
Vector2 dir = worldPos - transform.position;
float angle = Mathf.Atan2(dir.y, dir.x)*Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
}
IEnumerator AttackDelay()
{
while (true)
{
yield return new WaitWhile(() => canAttack);
yield return wait;
canAttack = true;
}
}
}
using UnityEngine;
using UnityEngine.InputSystem;
public class Bow : Weapon
{
[SerializeField] Transform firePosition;
[SerializeField] GameObject arrow;
// Start is called once before the first execution of Update after the MonoBehaviour is created
protected override void Start()
{
base.Start();
camera = Camera.main;
}
// Update is called once per frame
void Update()
{
LookMouse();
Attack();
}
protected override void Attack()
{
if (Mouse.current.leftButton.wasPressedThisFrame && canAttack)
{
//화살생성
GameObject arr = ObjectPoolManager.instance.GetObject("arrow");
arr.transform.position = firePosition.position;
arr.transform.rotation = transform.rotation;
arr.GetComponent<Arrow>().SetDamage(5);
canAttack = false;
}
}
}
using UnityEngine;
public class Arrow : MonoBehaviour
{
[SerializeField] float speed = 5f;
float lifeTime;
float timer;
[SerializeField] int damage;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
lifeTime = 3f;
timer = 0f;
}
private void OnEnable()
{
timer = 0f;
}
// Update is called once per frame
void Update()
{
timer += Time.deltaTime;
transform.position += transform.right * speed * Time.deltaTime;
if(timer > lifeTime)
{
ObjectPoolManager.instance.ReturnObject("arrow", this.gameObject);
}
}
public void SetDamage(int dmg)
{
damage = dmg;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.layer == 8) //Wall
{
ReturnPool();
}
else if(collision.gameObject.layer == LayerMask.NameToLayer("Monster"))
{
collision.gameObject.GetComponent<MonsterController>().TakeDamage(damage);
ReturnPool();
}
}
void ReturnPool()
{
ObjectPoolManager.instance.ReturnObject("arrow", this.gameObject);
}
}
마우스 위치 좌표를 따라가도록 하는 코드
protected void LookMouse()
{
// 화면 기준으로 마우스 위치 좌표(스크린 좌표)
Vector2 mousePos = Mouse.current.position.ReadValue();
// 월드 좌표
Vector3 worldPos = camera.ScreenToWorldPoint(mousePos);
worldPos.z = 0f;
Vector2 dir = worldPos - transform.position;
float angle = Mathf.Atan2(dir.y, dir.x)*Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
}
WaitWhile( canAttack이 false가 될때까지 기다리다가 1초를 기다리고 다시 true )
protected bool canAttack;
protected virtual void Start()
{
canAttack = true;
delay = 1f;
wait = new WaitForSeconds(delay);
StartCoroutine(AttackDelay());
}
IEnumerator AttackDelay()
{
while (true)
{
yield return new WaitWhile(() => canAttack);
yield return wait;
canAttack = true;
}
}
bow.cs에서 attack함수에서 canAttack을 false 해줌.
protected override void Attack()
{
if (Mouse.current.leftButton.wasPressedThisFrame && canAttack)
{
//화살생성
GameObject arr = ObjectPoolManager.instance.GetObject("arrow");
arr.transform.position = firePosition.position;
arr.transform.rotation = transform.rotation;
arr.GetComponent<Arrow>().SetDamage(5);
canAttack = false;
}
}
처음에 초기값이 canAttack이 true -> 마우스 왼쪽 클릭시 화살이 나감 -> canAttack이 false -> WaitWhile이 작동 -> wait으로 1초 기다림 -> canAttack이 다시 true
using UnityEngine;
using static UnityEngine.GraphicsBuffer;
public class MonsterController : MonoBehaviour
{
[SerializeField] int maxHp;
[SerializeField] int nowHp;
[SerializeField] Transform target;
float moveSpeed;
SpriteRenderer sr;
[SerializeField] float range;
MonsterWeapon mWeapon;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
moveSpeed = 3f;
sr = GetComponent<SpriteRenderer>();
mWeapon = GetComponent<MonsterWeapon>();
//테스트코드
target = GameObject.Find("Player").transform;
}
private void OnEnable()
{
nowHp = 10;
//maxHp = 10;
}
// Update is called once per frame
void Update()
{
CheckDistance();
}
void CheckDistance()
{
float distance = Vector3.Distance(transform.position, target.position);
if (distance < range)
{
//공격
mWeapon.SetDirection(GetDirection());
mWeapon.SetDistance(distance);
mWeapon.CanAttack(true);
}
else
{
//추적
Trace();
mWeapon.CanAttack(false);
}
}
void Trace()
{
sr.flipX = CheckFlip();
Vector3 dir = GetDirection().normalized;
Move();
}
Vector2 GetDirection()
{
return target.position - transform.position;
}
void Move()
{
transform.position = Vector3.MoveTowards(transform.position, target.position, moveSpeed*Time.deltaTime);
}
bool CheckFlip()
{
return transform.position.x > target.position.x ? true : false;
}
public void TakeDamage(int damage)
{
nowHp -= damage;
if (nowHp < 0)
{
nowHp = 0;
Die();
}
}
void Die()
{
StageManager.instance.RemoveMonster(this.gameObject);
ReturnPool();
}
public void ReturnPool()
{
ObjectPoolManager.instance.ReturnObject("Monster", this.gameObject);
}
}
몬스터를 플레이어에게 이동시키는 부분
void Update()
{
CheckDistance();
}
void CheckDistance()
{
float distance = Vector3.Distance(transform.position, target.position);
if (distance < range)
{
//공격
mWeapon.SetDirection(GetDirection());
mWeapon.SetDistance(distance);
mWeapon.CanAttack(true);
}
else
{
//추적
Trace();
mWeapon.CanAttack(false);
}
}
업데이트에 계속 플레이어와 몬스터 사이의 거리를 체크해주면서 거리가 설정한 range보다 적어지면 공격, 아니면 추적
추적할때는 몬스터가 공격못하게 canAttack false해줌.
void Trace()
{
sr.flipX = CheckFlip();
Vector3 dir = GetDirection().normalized;
Move();
}
MoveTowards로 적과 내 위치를 조금씩 좁혀나감.
void Move()
{
transform.position = Vector3.MoveTowards(transform.position, target.position, moveSpeed*Time.deltaTime);
}
타겟이 나를 바라볼수있게 플립.
bool CheckFlip()
{
return transform.position.x > target.position.x ? true : false;
}
using UnityEngine;
public abstract class MonsterWeapon : MonoBehaviour
{
[SerializeField] protected int damage;
[SerializeField] protected int range;
[SerializeField] protected int delay;
protected bool canAttack;
protected Vector2 dir;
protected float distance;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void SetDirection(Vector2 direction)
{
dir = direction;
}
public void SetDistance(float dis)
{
distance = dis;
}
public void CanAttack(bool c)
{
canAttack = c;
}
protected abstract void Attack();
}
using System.Collections;
using UnityEngine;
public class MonsterRangedWeapon : MonsterWeapon
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
Coroutine attackCoroutine = StartCoroutine(AttackDelay());
}
public void StartAtt()
{
}
// Update is called once per frame
void Update()
{
}
IEnumerator AttackDelay()
{
//사거리 안에 들어올때까지 공격 대기
//사거리안에 들어오면 WaitUntil에서 true로 넘어감
//공격
//쿨타임 대기
//쿨종료 후 사거리 안에 들어올때까지 다시 대기
//반복
WaitForSeconds wait = new WaitForSeconds(delay);
while (true)
{
yield return new WaitUntil(() => canAttack);
Attack();
yield return wait;
}
}
protected override void Attack()
{
GameObject axe = ObjectPoolManager.instance.GetObject("Axe");
axe.transform.position = transform.position;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
axe.GetComponent<Axe>().SetDirection(dir);
axe.GetComponent<Axe>().SetDamage(3);
}
}
using UnityEngine;
public class Axe : MonoBehaviour
{
[SerializeField] float speed = 5f;
[SerializeField] int damage;
float lifeTime;
float timer;
Rigidbody2D rb;
Vector2 dir;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
rb = GetComponent<Rigidbody2D>();
lifeTime = 3f;
timer = 0f;
}
private void OnEnable()
{
timer = 0f;
}
void Update()
{
if(timer >= lifeTime)
{
ObjectPoolManager.instance.ReturnObject("Axe", this.gameObject);
transform.rotation = Quaternion.identity;
}
}
public void SetDamage(int dmg)
{
damage = dmg;
}
public void SetDirection(Vector2 direction)
{
dir = direction;
}
private void FixedUpdate()
{
timer += Time.fixedDeltaTime;
rb.linearVelocity = dir * speed;
rb.MoveRotation(rb.rotation + 100f * Time.fixedDeltaTime);
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Player"))
{
collision.gameObject.GetComponent<PlayerController>().TakeDamage(damage);
ReturnPool();
}
else if(collision.gameObject.layer == LayerMask.NameToLayer("Wall"))
{
ReturnPool();
}
}
void ReturnPool()
{
ObjectPoolManager.instance.ReturnObject("Axe", this.gameObject);
transform.rotation = Quaternion.identity;
}
}
MonsterWeapon을 만들어서 Ranged 사거리가 긴 무기가 상속받도록 하였음.
IEnumerator AttackDelay()
{
//사거리 안에 들어올때까지 공격 대기
//사거리안에 들어오면 WaitUntil에서 true로 넘어감
//공격
//쿨타임 대기
//쿨종료 후 사거리 안에 들어올때까지 다시 대기
//반복
WaitForSeconds wait = new WaitForSeconds(delay);
while (true)
{
yield return new WaitUntil(() => canAttack);
Attack();
yield return wait;
}
}
마찬가지로 몬스터도 공격 딜레이를 주었음.
protected override void Attack()
{
GameObject axe = ObjectPoolManager.instance.GetObject("Axe");
axe.transform.position = transform.position;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
axe.GetComponent<Axe>().SetDirection(dir);
axe.GetComponent<Axe>().SetDamage(3);
}
어택 함수에 보면
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
axe.GetComponent<Axe>().SetDirection(dir);
axe.GetComponent<Axe>().SetDamage(3);
도끼가 회전할 수 있도록 해주었음.
using System.Collections.Generic;
using UnityEngine;
public class ObjectPoolManager : MonoBehaviour
{
public static ObjectPoolManager instance;
[SerializeField] List<GameObject> objList = new List<GameObject>();
Dictionary<string, Queue<GameObject>> pools = new Dictionary<string, Queue<GameObject>>();
int poolSize;
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
Destroy(gameObject);
}
DontDestroyOnLoad(gameObject);
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
poolSize = 0;
foreach(GameObject obj in objList)
{
pools[obj.name] = new Queue<GameObject>();
GameObject parentPool = new GameObject($"{obj.name}_Pool");
parentPool.transform.SetParent(this.transform);
for(int i=0; i<poolSize; i++)
{
GameObject go = Instantiate(obj, parentPool.transform);
go.SetActive(false);
pools[obj.name].Enqueue(go);
}
}
}
public GameObject GetObject(string name)
{
if (!pools.ContainsKey(name))
{
return null;
}
if (pools[name].Count > 0)
{
GameObject go = pools[name].Dequeue();
go.SetActive(true);
return go;
}
else
{
GameObject go = Instantiate(objList.Find(obj => obj.name == name));
return go;
}
}
public void ReturnObject(string name, GameObject go)
{
if (!pools.ContainsKey(name))
{
Destroy(go);
return;
}
go.SetActive(false);
pools[name].Enqueue(go);
}
// Update is called once per frame
void Update()
{
}
}
using System.Collections.Generic;
using System.Collections;
using UnityEditor.Rendering;
using UnityEngine;
public class StageManager : MonoBehaviour
{
public static StageManager instance;
[SerializeField] float summonDelay;
[SerializeField] List<Rect> spawnArea;
[SerializeField] Color color = new Color(1, 0, 0, 0.5f);
WaitForSeconds wait;
List<GameObject> monsterList = new List<GameObject>();
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
Destroy(gameObject);
}
DontDestroyOnLoad(gameObject);
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
summonDelay = 3f;
wait = new WaitForSeconds(summonDelay);
StartCoroutine(SummonMonster());
}
// Update is called once per frame
void Update()
{
}
IEnumerator SummonMonster()
{
while (true)
{
yield return wait;
SummonEnemy();
}
}
private void SummonEnemy()
{
Rect spawnRect = spawnArea[Random.Range(0, spawnArea.Count)];
Vector2 randPos = new Vector2(Random.Range(spawnRect.xMin, spawnRect.xMax), Random.Range(spawnRect.yMin, spawnRect.yMax));
GameObject enemy = ObjectPoolManager.instance.GetObject("Monster");
enemy.transform.position = randPos;
monsterList.Add(enemy);
}
public void RemoveMonster(GameObject monster)
{
monsterList.Remove(monster);
}
public void ClearMonsterList()
{
foreach(GameObject monster in monsterList)
{
monster.GetComponent<MonsterController>().ReturnPool();
//RemoveMonster(monster);
}
}
private void OnDrawGizmosSelected()
{
if (spawnArea == null)
{
return;
}
Gizmos.color = color;
foreach(var area in spawnArea)
{
Vector3 center = new Vector3(area.x + area.width / 2, area.y + area.height / 2);
Vector3 size = new Vector3(area.width, area.height);
Gizmos.DrawCube(center, size);
}
}
}