Coding Feature.

[Unity 3D] Vampire Survivors 같은 게임 만들기 #1 플레이어, 적 이동, 플레이어 공격 구현 본문

Toy Project/Bust'em, Igor! [Unity3D]

[Unity 3D] Vampire Survivors 같은 게임 만들기 #1 플레이어, 적 이동, 플레이어 공격 구현

codingfeature 2024. 2. 3. 23:40

Vampire Survivors 라는 게임이 있습니다.

 

https://poncle.itch.io/vampire-survivors

 

Vampire Survivors by poncle

Mow thousands of night creatures and survive until dawn!

poncle.itch.io

 

플레이어가 캐릭터를 움직이면서 여러 아이템을 획득하고 적을 무찌르는 단순하지만 중독성있는 게임입니다.

 

저는 기존 게임에서 몇 가지 요소를 추가하거나 삭제하여 색다른 게임을 만들어보고자 합니다.

 

대략적으로 다음을 생각하고 있습니다.

 

- 플레이어의 움직임 컨트롤 불가능

플레이어는 자동으로 가장 가까운 적으로 이동하게끔 구현하겠습니다.

 

- 카드 형식 업그레이드

플레이어는 적을 무찌르면서 여러 카드를 획득하게 됩니다. 이 카드들을 적절히 조합하여 일회용으로 사용하거나 지속적인 효과를 받도록 해보겠습니다.

 

 

제가 만들 게임의 이름은 현재 "Bust'em Igor"로 정했습니다!

 

 

우선 플레이어와 적의 이동 그리고 공격과 관련된 시스템을 구현해보겠습니다.

 

적이 한 마리가 아니라 여러 마리가 스폰이 될 예정이기 때문에 이를 한꺼번에 관리할 수 있도록 해주는 스크립트가 필요합니다. 저는 Enemy Manager를 구현하겠습니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyManager : MonoBehaviour
{
    private static EnemyManager instance;

    public GameObject originalSkeleton;
    public List<GameObject> skeletons = new List<GameObject>();

    public static EnemyManager Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    public void SpawnNewSkeleton(Vector3 pos)
    {
        GameObject newSkeleton = Instantiate(originalSkeleton);

        newSkeleton.SetActive(true);
        newSkeleton.transform.position = pos;

        skeletons.Add(newSkeleton);
    }

    public void RemoveSkeletonFromList(GameObject skeleton)
    {
        skeletons.Remove(skeleton);
    }
}

 

위 처럼 Enemy Manager는 싱글톤 패턴으로 구현하였습니다.

 

그리고 originalSkeleton 게임 오브젝트로 원형의 적 게임 오브젝트를 받고, skeletons라는 리스트 형식을 사용해서 skeleton들을 관리하도록 해보았습니다. 다만 현재는 skeleton이라는 특정 적에 대해서만 리스트를 지정했기 때문에 다른 특성의 적에 대해서는 따로 어떻게 구현할지 고민해보겠습니다.

 

SpawnNewSkeleton은 인자로 위치 정보인 pos를 받습니다.

이때 Instantiate 함수를 통해 새로운 게임 오브젝트를 만들고 pos 위치로 이동시킨 뒤에 리스트에 추가합니다.

 

RemoveSkeletonFromList는 추후 적이 죽거나 없어져야 할 경우, 인자로 그 게임 오브젝트를 받고 리스트에 그 게임오브젝트만 제거하는 것으로 구현했습니다.

 

 

그 다음은 적 Skeleton의 움직임과 체력 등을 관리하는 스크립트를 짜보았습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonScript : MonoBehaviour
{
    public float damage;
    public float hp;
    public float speed;

    // State 관련 구현 필요할 수도.

    Vector3 moveDirection;

    private void Update()
    {
        moveDirection = (GameManager.Instance.PlayerGameObject.transform.position - transform.position).normalized;
    }

    private void FixedUpdate()
    {
        transform.Translate(moveDirection * speed);
    }
}

 

Update 함수에서는 플레이어의 위치를 게임 매니저(바로 다음에 나옵니다)를 통해 받고 이를 이용해서 이동해야 되는 방향 벡터를 노말 벡터로 구하게 됩니다.

 

Skeleton이 이동하는 코드는 FixedUpdate 함수에서 Translate 함수를 사용하여 프레임에 무관한 동일한 속력으로 이동하도록 해주었습니다.

 

 

 

그 다음 게임 매니저를 구현해보았습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private static GameManager instance;

    public GameObject PlayerGameObject;

    public static GameManager Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        EnemyManager.Instance.SpawnNewSkeleton(new Vector3(2f, 5f, 2f));
        EnemyManager.Instance.SpawnNewSkeleton(new Vector3(-3f, 5f, 3f));
        EnemyManager.Instance.SpawnNewSkeleton(new Vector3(4f, 5f, -4f));
        EnemyManager.Instance.SpawnNewSkeleton(new Vector3(-5f, 5f, -5f));
    }
}

 

게임 매니저도 Enemy 매니저와 동일하게 싱글톤 패턴을 이용해서 구현하였으며 현재는 Start 함수에서 Enemy 매니저의 SpawnNewSkeleton 함수를 이용해서 특정 위치에 적 4 마리를 스폰하도록 구현하였습니다.

 

 

그리고 플레이어의 움직임, 공격 등을 다루는 Player 스크립트를 다음과 같이 구현해보았습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerScript : MonoBehaviour
{
    public float damage;
    public float speed;
    public float hp;
    public float attackRate;

    Vector3 moveDirection;
    GameObject targetEnemy;

    private void FixedUpdate()
    {
        targetEnemy = FindNearestEnemy(); // 최적화를 위해 Update 함수 대신 다른 곳에서 특정 상황 되면 호출되도록 구현 필요.

        moveDirection = (targetEnemy.transform.position - transform.position).normalized;
        transform.Translate(moveDirection * speed);
    }

    GameObject FindNearestEnemy()
    {
        GameObject nearestEnemy = null;
        float minimumDistance = 9999f;
        foreach (GameObject enemy in EnemyManager.Instance.skeletons)
        {
            float tempDistance = (transform.position - enemy.transform.position).magnitude;
            if(tempDistance < minimumDistance)
            {
                minimumDistance = tempDistance;
                nearestEnemy = enemy;
            }
        }

        if (nearestEnemy != null)
            return nearestEnemy;
        
        return null;
    }

    void Attack(GameObject enemy)
    {
        enemy.GetComponent<SkeletonScript>().hp -= damage;
        if(enemy.GetComponent<SkeletonScript>().hp <= 0)
        {
            Destroy(enemy);
            EnemyManager.Instance.RemoveSkeletonFromList(enemy);
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        GameObject go = collision.gameObject;
        if (go.CompareTag("Enemy"))
        {
            Attack(go);
        }
    }
}

 

 

FindNearestEnemy 함수는 플레이어와 가장 가까운 적의 게임 오브젝트를 반환합니다. 이는 Enemy 매니저의 리스트에 담겨진 모든 적 게임 오브젝트와의 거리를 비교해서 구하게 됩니다.

 

위 함수 자체는 큰 문제가 없겠으나 적이 100마리, 1000마리 등 감당하기 힘들 정도로 많은 경우, 위 함수를 지속적으로 호출한다면 상당한 오버헤드가 발생할 것이라 예상됩니다.

 

현재는 FixedUpdate 함수에서 호출되도록 했지만 이는 60프레임인 경우에는 1초당 리스트를 60번이나 탐색한다는 것이므로 좋지 않겠죠..

 

이 부분은 가장 가까운 적이 죽었을 때에만 호출되도록 하거나 다르게 구현하는 것으로 해결해야 합니다..!

 

Attack 함수는 인자로 적의 게임 오브젝트를 받아서 그 오브젝트의 hp를 플레이어의 damage 만큼 깎는 형식으로 구현했습니다. 그리고 적의 체력이 0 이하일 경우 게임 오브젝트를 제거하고 Enemy 매니저의 리스트에서 그 적을 제거함으로써 적의 죽음을 처리했습니다.

 

현재는 플레이어에게 적이 닿기만 해도 공격하는 것으로 구현을 해보았습니다.

 

 

위에서 사용한 3D 모델은 아래 크리에이터의 것을 사용했습니다. (무료, 출처 남김 필수)

 

Kay Lousberg - itch.io

 

Kay Lousberg - itch.io

Hey, I'm Kay! I make 3D Game Assets, mostly for free :) you can check out the full catalog below. Check out my Twitter for my work, or my website if you're interested in hiring me for custom assets or artwork. You can Support me on Patreon here to get

kaylousberg.itch.io

 

 

 

다음에는 적의 스폰과 관련된 코드를 작성하고(적 스폰 빈도, 공격력 설정 등), 플레이어의 공격 빈도와 관련된 시스템을 구현해보겠습니다.