게임개발 캠프 33일차

IIRU·2026년 7월 1일

PlayerController.cs

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();
    }

}

Weapon.cs

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;
        }
    }

}

Bow.cs

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;
        }
    }

}

Arrow.cs

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


MonsterController.cs

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;
}

MonsterWeapon.cs

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();
}

MonsterRangedWeapon.cs

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);
    }
}

Axe.cs

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);

도끼가 회전할 수 있도록 해주었음.


ObjectPoolManager.cs

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()
    {
        
    }
}

StageManager.cs

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);
        }

    }

}
profile
초보 개발자 블로그입니다!

0개의 댓글