Projectile은 게임 내에서 발사되는 발사체(또는 투사체) 오브젝트이다.

 

Projectile

구현 방법

Rigidbody를 통해 충돌처리를 구현하고, 코루틴을 통해 발사체의 지속시간을 구현했다.

 

 

구현 기능

OnTriggerEnter2D(Collider2D collision): 몇 가지 예외를 제외하고 Creature를 공격한다.
OnDisable(): 코루틴을 중단한다.
SetInfo(Creature owner): Creature에 대한 발사체 정보를 초기화하고 코루틴 중복을 방지한다.
CoDestroy(): 발사체의 지속시간만큼 대기한다.

public class Projectile : Thing
{
    public Creature Owner { get; protected set; } // 발사체 주인.

    // public new Creature.Status; // Creature의 Status에서 지속시간과 데미지 가져오는 방법 TODO.
    public float Duration { get; protected set; } // 발사체 지속시간.
    public float Damage { get; protected set; } // 발사체 데미지.
    public Vector2 Velocity { get; set; } // 발사체 속도.

    protected Rigidbody2D _rigidbody; // 충돌처리를 위한 Rigidbody.
    private Coroutine _coDestroy; // 코루틴이 null인지 활성화 되어있는지 확인하기 위한 필드.

    // Rigidbody가 붙은 발사체에 대한 속도.
    protected virtual void FixedUpdate()
    {
        _rigidbody.velocity = Velocity;
    }

    protected virtual void OnTriggerEnter2D(Collider2D collision)
    {
        Creature creature = collision.GetComponent<Creature>();
        if (!creature.IsValid() || !this.IsValid()) return;
        if (creature == Owner) return; // Creature가 발사체의 주인이라면.
        if (creature.State.Current == CreatureState.Dead) return; // Creature의 현재 상태가 죽었다면.

        creature.OnHit(Owner, Damage, new() { time = 0.1f, speed = 10f, direction = (creature.transform.position - this.transform.position).normalized }); // Creature 공격.

        _rigidbody.velocity = Velocity = Vector2.zero;
        if (this.IsValid()) Main.Object.DespawnProjectile(this);
    }

    protected void OnDisable()
    {
        StopAllCoroutines(); // 모든 코루틴 중단.
        _coDestroy = null;
    }

    public override bool Initialize()
    {
        if (!base.Initialize()) return false;

        _rigidbody = this.GetComponent<Rigidbody2D>();

        return true;
    }

    public virtual Projectile SetInfo(Creature owner) // Creature에 대한 발사체 정보.
    {
        this.Owner = owner;
        Duration = 5; // 임시
        Damage = 10; // 임시

        if (_coDestroy != null) StopCoroutine(this._coDestroy); // 코루틴 중복 방지.
        _coDestroy = StartCoroutine(CoDestroy());

        return this;
    }

    private IEnumerator CoDestroy()
    {
        yield return new WaitForSeconds(Duration); // 발사체 지속시간만큼 대기.
        _coDestroy = null;
        Main.Object.DespawnProjectile(this); // 오브젝트가 비활성화 시.
    }
}

 

 

'부트캠프 > Project' 카테고리의 다른 글

Enemy - FSM(1)  (0) 2024.02.07
Skill 구상  (0) 2024.01.23
Stat / Status 구현  (0) 2024.01.18
StatModifier 구현 계획  (0) 2024.01.17
Resource Manager 구현  (0) 2024.01.15

캐릭터나 오브젝트의 다양한 수치적 속성(생명력, 공격력 등)을 관리하는 Stat과 그 스탯들을 관리하는 Status를 구현한다.

 

Stat

구현 방법

새로운 스탯 인스턴스를 생성하여 초기값을 생성하는 생성자를 만들고, StatModifier에서 modifier의 값을 받아 스탯의 수정치를 확인하고 수정하는 함수를 구현한다.

 

 

구현 기능

Stat(): 새로운 스탯 인스턴스를 생성하고 초기값을 생성하는 생성자.
SetValue(): 값을 받으면 바뀐 값을 대입한다.
AddModifier() /RemoveModifier(): 수정치를 추가하거나 삭제한다.
GetModifyValue(): Value를 해당하는 StatModifierType으로 계산한다.

public class Stat
{
    public StatType Type { get; private set; }
    public float Min { get; private set; }
    public float Max { get; private set; }
    public float Value { get; private set; }
    public float OriginValue { get; private set; }

    private List<StatModifier> _modifiers = new(); // 스탯이 가지고 있는 수정치 목록.

    public event Action<Stat> OnChanged;

    // 새로운 스탯 인스턴스를 생성하고 초기값을 생성하는 생성자.
    public Stat(StatType type, float value = 0, float min = 0, float max = float.MaxValue)
    {
        this.Type = type;
        this.Min = min;
        this.Max = max;
        SetValue(value);
    }

    // 값을 받으면 바뀐 값을 대입.
    public void SetValue(float value)
    {
        OriginValue = value;
        Value = GetModifyValue();
        OnChanged?.Invoke(this);
    }

    // 수정치를 추가하거나 삭제하는 기능.
    public void AddModifier(StatModifier modifier)
    {
        _modifiers.Add(modifier);
        Value = GetModifyValue();
        OnChanged?.Invoke(this); // 능력치에 변화가 있다면 호출.
    }
    public void RemoveModifier(StatModifier modifier)
    {
        _modifiers.Remove(modifier);
        Value = GetModifyValue();
        OnChanged?.Invoke(this); // 능력치에 변화가 있다면 호출.
    }


    // _modifiers 리스트에 들어있는 객체들의 StatModifierType을 비교하여 Value를 해당하는 StatModifierType으로 계산한다.
    private float GetModifyValue()
    {
        float value = OriginValue;
        for (int i = 0; i < _modifiers.Count; i++)
        {
            // Stat 계산 방법.
            if (_modifiers[i].Type == StatModifierType.Add) value += _modifiers[i].Value;
            else if (_modifiers[i].Type == StatModifierType.Multiple) value *= _modifiers[i].Value;
            else if (_modifiers[i].Type == StatModifierType.Override) value = _modifiers[i].Value;
        }
        value = Mathf.Clamp(value, Min, Max);
        return value;
    }
}

 

 

Status

구현 방법

수정할 스탯 리스트를 받아 알맞은 스탯에 추가하거나 제거하는 기능을 구현한다.

 

 

구현 기능

StatType: 스탯 타입들. COUNT는 갯수를 파악하기 위한 상수이다.

StatModifierType: 수정치 계산 종류.

Stat this: 스탯 종류를 받아온다.

Status(): 스탯 종류를 확인 후, 딕셔너리에 추가.

Status(CreatureData data): 기존 데이터를 받아와 스탯에 대입한다.

AddModifiers / RemoveModifiers: 변화될 스탯을 리스트에서 찾아 스탯에 변화를 주는 함수들를 추가하거나 제거한다.

public enum StatType
{
    HpMax,
    HpRegen,
    Damage,
    Defense,
    MoveSpeed,
    AttackSpeed,
    //Cost,
    //Range,
    //Sight,
    COUNT // StatType 갯수 파악을 위한 상수.
}

public enum StatModifierType
{
    Add,
    Multiple,
    Override,
}


public class Status
{
    private Dictionary<StatType, Stat> _stats;

    public Stat this[StatType type]
    {
        get => _stats[type];
    }

    // 스탯 타입을 확인하고 딕셔너리에 추가.
    public Status()
    {
        _stats = new();
        for (int i = 0; i < (int)StatType.COUNT; i++)
        {
            _stats.Add((StatType)i, new Stat((StatType)i));
        }
    }

    // 크리처의 기존 데이터를 받아와서 스탯에 대입.
    public Status(CreatureData data)
    {
        _stats = new()
        {
            [StatType.HpMax] = new(StatType.HpMax, data.HpMax),
            [StatType.HpRegen] = new(StatType.HpRegen, data.HpRegen),
            [StatType.Damage] = new(StatType.Damage, data.Damage),
            [StatType.Defense] = new(StatType.Defense, data.Defense),
            [StatType.MoveSpeed] = new(StatType.MoveSpeed, data.MoveSpeed),
            [StatType.AttackSpeed] = new(StatType.AttackSpeed, data.AttackSpeed),
        };
    }

    // 변화될 스탯을 리스트에서 찾아 스탯에 변화를 주는 함수를 추가 / 제거.
    public void AddModifiers(List<StatModifier> modifiers)
    {
        for (int i = 0; i < modifiers.Count; i++)
        {
            this[modifiers[i].Stat].AddModifier(modifiers[i]);
        }
    }
    public void RemoveModifiers(List<StatModifier> modifiers)
    {
        for (int i = 0; i < modifiers.Count; i++)
        {
            this[modifiers[i].Stat].RemoveModifier(modifiers[i]);
        }
    }
}

'부트캠프 > Project' 카테고리의 다른 글

Skill 구상  (0) 2024.01.23
Projectile 구현  (0) 2024.01.19
StatModifier 구현 계획  (0) 2024.01.17
Resource Manager 구현  (0) 2024.01.15
<프로젝트> 팀과제 Unity 리듬 게임 회고  (2) 2024.01.09

스탯을 각 개체 클래스의 필드로 직접 관리하게 되면, 다양한 유형의 수정치를 적용하거나 스탯 간 복잡한 상호작용을 구현하기 어렵고 스탯 관리 로직이 특정 클래스와 밀접하게 결합되어 있으면 다른 클래스나 시스템에서 재사용하기 어렵다.

새로운 스탯 타입 또는 수정치를 추가하기 위해 기존 클래스를 크게 수정해야 할 수도 있어 유지보수가 어렵고 비슷한 스탯 관리 코드가 중복될 가능성도 있다.

그렇기 때문에 기존 스탯과 결합하여 스탯을 변화시키는 수정치인 StatModifier를 따로 구현하기로 했다.

 

구현 계획

  • 어떤 스탯을 얼마나 어떻게 수정할 것인지 정보를 받아온다.
  • 새로운 수정치 인스턴스를 생성하고 초기값을 설정하는 생성자
  • string 값(Modifiers)을 받아 새 인스턴스를 생성하는 생성자

 

Stat에서 캐릭터나 오브젝트의 다양한 수치적 속성(생명력, 공격력 등)을 관리하고 Status에서 모든 Stat을 관리하도록 Class를 분리한다.

public class StatModifier
{
    public StatType Stat { get; set; }
    public StatModifierType Type { get; set; }
    public float Value { get; set; }

    // 기본 생성자.
    public StatModifier() { }

    // Modifier 값을 받아온다.
    public StatModifier(StatType stat, StatModifierType type, float value)
    {
        Stat = stat;
        Type = type;
        Value = value;
    }

    // json 파일의 Modifier를 나눠서 Enum으로 변환한다. 
    public StatModifier(string s)
    {
        string[] strings = s.Split('_');
        Stat = (StatType)Enum.Parse(typeof(StatType), strings[0]);
        Type = (StatModifierType)Enum.Parse(typeof(StatModifierType), strings[1]);
        Value = float.Parse(strings[2]);
    }

    // 받은 값들을 깊은복사한다.
    public StatModifier Copy()
    {
        return new(Stat, Type, Value);
    }
}

 

'부트캠프 > Project' 카테고리의 다른 글

Projectile 구현  (0) 2024.01.19
Stat / Status 구현  (0) 2024.01.18
Resource Manager 구현  (0) 2024.01.15
<프로젝트> 팀과제 Unity 리듬 게임 회고  (2) 2024.01.09
<프로젝트> 팀과제 Unity 리듬 게임(4)  (1) 2024.01.05

Prefab 하나로 Data에 따라 Sprite나 Animation 등을 바꿔주며 다른 Object가 되도록 설정할 수 있도록 구현하기 위해 Data와 Resource를 구분하였다. 프로젝트에서 내가 맡은 부분은 Resource로 리소스를 불러오고 외부에서 리소스를 사용하라 수 있도록 했다.

 

구현 방법

소규모 프로젝트에서는 Resources.Load 방식으로 구현하는 것이 더 간단하기 때문에 모든 리소스를 Resources 폴더에 넣고 Resources.Load 또는 Resources.LoadAll 함수를 통해 리소스를 불러오는 방식을 택하였다. 

 

구현 기능

게임 시작 시 게임에 Resource 경로에서 필요한 리소스(Sprite, Prefab, JsonData, Animation)를 불러온다.

Initialize( ): 리소스를 불러올 경로를 지정하여 Dictionary에 추가한다. 현재 Sprites의 경로가 지정되어 있지 않기 때문에 후에 수정해야 한다.

Load~( ): Dictionary의 key값으로 리소스가 있는지 확인 후, 리턴한다.

Instantiate( ): 오브젝트가 풀 안에 있는지 확인 후, parent 하위에 prefab을 생성하거나 내보낸다.

Destroy( ): 필요 없는 오브젝트를 파괴한다.

public class ResourceManager : MonoBehaviour
{
    private Dictionary<string, Sprite> _sprites = new();
    private Dictionary<string, GameObject> _prefabs = new();
    private Dictionary<string, TextAsset> _jsonData = new();
    private Dictionary<string, RuntimeAnimatorController> _animControllers = new();

    public void Initialize()
    {
        Sprite[] sprites = Resources.LoadAll<Sprite>("Sprites/"); // TODO: 경로 지정.
        foreach (Sprite sprite in sprites)
        {
            _sprites.Add(sprite.name, sprite);
        }

        GameObject[] objs = Resources.LoadAll<GameObject>("Prefabs");
        foreach (GameObject obj in objs)
        {
            _prefabs.Add(obj.name, obj);
        }

        TextAsset[] texts = Resources.LoadAll<TextAsset>("JsonData");
        foreach (TextAsset t in texts)
        {
            _jsonData.Add(t.name, t);
        }

        RuntimeAnimatorController[] controllers = Resources.LoadAll<RuntimeAnimatorController>("Animations");
        foreach (RuntimeAnimatorController controller in controllers)
        {
            _animControllers.Add(controller.name, controller);
        }
    }

    // 리소스가 있는지 확인.
    public GameObject LoadPrefab(string key)
    {
        if (!_prefabs.TryGetValue(key, out GameObject prefab))
        {
            Debug.LogError($"[ResourceManager] LoadPrefab({key}): Failed to load prefab.");
            return null;
        }
        return prefab;
    }
    public Sprite LoadSprite(string key)
    {
        if (!_sprites.TryGetValue(key, out Sprite sprite))
        {
            Debug.LogError($"[ResourceManager] LoadSprite({key}): Failed to load sprite.");
            return null;
        }
        return sprite;
    }
    public TextAsset LoadJsonData(string key)
    {
        if (!_jsonData.TryGetValue(key, out TextAsset data))
        {
            Debug.LogError($"[ResourceManager] LoadJsonData({key}): Failed to load jsonData.");
            return null;
        }
        return data;
    }
    public RuntimeAnimatorController LoadAnimController(string key)
    {
        if (!_animControllers.TryGetValue(key, out RuntimeAnimatorController controller))
        {
            Debug.LogError($"[ResourceManager] LoadJsonData({key}): Failed to load animController.");
            return null;
        }
        return controller;
    }

    // 오브젝트가 풀 안에 있는지 없는지 확인 후 생성.
    public GameObject Instantiate(string key, Transform parent = null, bool pooling = false)
    {
        GameObject prefab = LoadPrefab(key);
        if (prefab == null)
        {
            Debug.LogError($"[ResourceManager] Instantiate({key}): Failed to load prefab.");
            return null;
        }

        if (pooling) return Main.Pool.Pop(prefab);

        GameObject obj = GameObject.Instantiate(prefab, parent);
        obj.name = prefab.name;
        return obj;
    }

    // 필요없는 오브젝트 파괴.
    public void Destroy(GameObject obj)
    {
        if (obj == null) return;

        if (Main.Pool.Push(obj)) return;

        Object.Destroy(obj);
    }
}

 

 

+ Recent posts