Sirius's Apocaluse Nebula System

어느 한 세계의 의지가 만든 한 우주의 이야기.

시리우스의 블로그에 오신 것을 진심으로 환영합니다.

카테고리 없음

ΑΒΡΑΞΑΣ - 개발 후기 1편

siriussquare 2025. 8. 10. 21:19

본론으로 가기 전에 먼저

제 블로그를 찾아주신 수많은 분들에게 감사를 표합니다

 

아브락사스 - 설명

아브락사스는 로그라이트 액션  게임으로 2025년 1학년 1학기 경기게임마이스터고 개인프로젝트 수행평가에서 우수상을 받은 작품입니다(실제로 그만큼 우수한지는 잘 모르겠지만)

 

저의 본격적으로 공개한 첫 작품이기도 하고 이 작품을 만들면서 상당히 고생한 만큼. 저도 꽤나 애착이 가기도 합니다.

그러나. 한편으로는 매우 아쉽습니다. 제가 욕심을 조금만 버리고 조금만 더 능력이 있었다면 더 좋은 작품이 나올 수 있었을 텐데 말이죠.

 

이번 게시글은 제가 저의 게임을 돌아보면서 부족했던 점들을 찾고 비판하는 일기의 느낌으로 봐주시면 좋을 것 같습니다.

이번 편에서는 스텟 상승 아이템 시스템, 인챈트 효과 발생 시스템, 넣지 못한 방들에 대해 이야기하도록 하겠습니다.

스텟 상승 아이템 시스템

스텟 상승 아이템을 파는 상점에 서있는 플레이어

작중의 메인 시스템중 하나이자 그나마 운빨에 기대지 않을 수 있는 확정적인 스텟 상승의 수단입니다.

구매하기 위해선 일정량의 돈이 필요하며. 위쪽 화살표 키(가드 키와 동일)를 누르고 있는 상태에서 접촉할 시 먹으면 스텟을 증가시키는 아이템이 드롭됩니다.

 

더보기

사용 스크립트

using NUnit.Framework;
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public interface IItem
{
    float TargetDir { get; set; }
    float Speed { get; set; }
    LayerMask TargetLayer { get; set; }
    void UseItem(Entity entity);
}

public enum StatType
{
    MaxHealth,
    HealthRecoverPower,
    MaxStamina,
    StaminaRecoverPower,
    MaxMana,
    ManaRecoverPower,
    AttackPower,
    AttackPowerMultiplier,
    AttackSpeed,
    Defense,
    CriticalChance,
    CriticalDamageMultiplier,
    CriticalGardPenetration,
    KnockbackResistance,
    DefGardLevel,
    MoveSpeed,
    JumpForce,
    CooldownTime,
    MaxJumpCount,
    Gold,
    ArrowPoint,
    CurrentHealth,
    CurrentStamina,
    CurrentMana,
    CurrentShield,
    
}

[Serializable]
public struct StatIncreaseInfo
{
    public StatType stat;
    public float amount;
    public Color color;
}

public class CoinItemScript : MonoBehaviour, IItem, IPoolable
{
    [SerializeField] private string itemName;

    [SerializeField] private float targetDir = 2f;
    [SerializeField] private float speed = 2f;
    [SerializeField] private LayerMask targetLayer;
    [SerializeField] private float firstenableTime = 0.5f;
    [SerializeField] public List<StatIncreaseInfo> statIncreases;

    public string ItemName => itemName;
    public GameObject GameObject => gameObject;

    public float TargetDir { get => targetDir; set => targetDir = value; }
    public float Speed { get => speed; set => speed = value; }

    public LayerMask TargetLayer { get => targetLayer; set => targetLayer = value; }

    public Rigidbody2D rb;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        if (firstenableTime > 0)
        {
            firstenableTime -= Time.deltaTime;
            rb.linearVelocityX = Mathf.Lerp(rb.linearVelocity.x, 0, 2f * Time.deltaTime);
        }
        if (firstenableTime < 0)
        {
            Collider2D hit = Physics2D.OverlapCircle(transform.position, targetDir, targetLayer);
            Collider2D hit2 = Physics2D.OverlapCircle(transform.position, 0.5f, targetLayer);
            if (hit != null && hit.TryGetComponent<Entity>(out Entity entity))
            {
                Vector2 direction = (entity.transform.position - transform.position);

                rb.AddForce(direction * speed, ForceMode2D.Impulse);
                rb.gravityScale = 0f;

            }
            else
            {
                rb.linearVelocity = new Vector2(Mathf.Lerp(rb.linearVelocity.x, 0, 2f * Time.deltaTime), Mathf.Lerp(rb.linearVelocity.y, 0, 2f * Time.deltaTime));
                
                rb.gravityScale = 2.5f;
            }

            if (hit2 != null && hit2.TryGetComponent<Entity>(out entity))
            {
                UseItem(entity);
                PoolManager.Instance.Push(this);
            }
        }
        
    }

    public void UseItem(Entity entity)
    {
        if (entity.isDead)
        {
            return;
        }
        foreach (var info in statIncreases)
        {
            if (info.amount == 0) continue;

            ApplyStatIncrease(entity.statObject, info.stat, info.amount);
            entity.HPBarSystem?.SetHP();
            entity.HPBarSystem?.SetMana();

            if (info.stat != StatType.CurrentHealth || info.stat != StatType.CurrentShield)
            {
                IPoolable statText = PoolManager.Instance.Pop("ItemText");
                if (statText.GameObject != null)
                {

                    ItemTextScript itemTextScript = statText.GameObject.GetComponent<ItemTextScript>();
                    if (itemTextScript != null)
                    {

                        itemTextScript.SettingText(info.amount, info.stat.ToString(), info.color);
                        statText.GameObject.transform.position = (Vector2)transform.position + new Vector2(UnityEngine.Random.Range(-0.5f, 0.5f), UnityEngine.Random.Range(-0.5f, 0.5f));
                        statText.GameObject.SetActive(true);
                    }
                }
            }
            IPoolable statEffect = PoolManager.Instance.Pop("ItemCollectEffect");
            if (statEffect.GameObject != null)
            {
                statEffect.GameObject.transform.position = transform.position;
                statEffect.GameObject.GetComponent<Effect>().SetColor(info.color);
                statEffect.GameObject.SetActive(true);
            }

        }
        
            var itemEventHolder = GetComponent<EntityEventHolder>();
            if (itemEventHolder != null && entity.entityEventHolder != null)
            {
                foreach (var itemEvent in itemEventHolder.pendingEvents)
                {
                    entity.entityEventHolder.AddEvent(itemEvent);

                    
                    string eventText = SettigTextsa(itemEvent);
                    string eventLogText = SettigTextnonum(itemEvent);
                    FindAnyObjectByType<MenuScript>().ResumeTextPlus(eventLogText);
                    IPoolable statText = PoolManager.Instance.Pop("ItemText");
                    if (statText.GameObject != null)
                    {
                        ItemTextScript itemTextScript = statText.GameObject.GetComponent<ItemTextScript>();
                        if (itemTextScript != null)
                        {
                            itemTextScript.SettingText(0, eventText, Color.white,true);
                            statText.GameObject.transform.position = (Vector2)transform.position + new Vector2(UnityEngine.Random.Range(-0.5f, 0.5f), UnityEngine.Random.Range(-0.5f, 0.5f));
                            statText.GameObject.SetActive(true);
                        }
                    }
                
            }
        }
    }
    private string SettigTextsa(PendingEvent pe)
    {
        string returnstring = "";


        if (EventManager.GameEventNames.TryGetValue(pe.eventType, out var eventName))
            returnstring += $"{eventName}\n";


        if (EventManager.GameConditionNames.TryGetValue(pe.condition.conditionType, out var cond1))
        {
            if (cond1.Length == 1 || pe.condition.conditionType == EventCondition.ConditionType.FloorBetween)
                returnstring += $"{cond1[0]}\n";
            else
                returnstring += $"{cond1[0]} {pe.condition.threshold.ToString("F1")} {cond1[1]}\n";
        }


        if (pe.secondaryConditions != null && pe.secondaryConditions.Length > 0)
        {
            var secondCondition = pe.secondaryConditions[0];
            if (EventManager.GameSecondaryConditionNames.TryGetValue(secondCondition.conditionType, out var cond))
            {
                if (cond.Length == 1)
                    returnstring += $"{secondCondition.parameters[0].ToString("F1")}% {cond[0]}\n";
                else
                    returnstring += $"{cond[0]} {secondCondition.parameters[0].ToString("F1")} {cond[1]}\n";
            }
        }

        if (EventManager.EffectTypeNames.TryGetValue(pe.effect.effectType, out var effectNames))
        {
            var effectParams = pe.effect.parameters;

            for (int i = 1; i < effectNames.Length; ++i)
            {
                if (i - 1 < effectParams.Count)
                {
                    returnstring += $"{effectNames[i]} {effectParams[i - 1].ToString("F2")} ";
                }
            }

            returnstring += $"{effectNames[0]}\n";
        }

        return returnstring.TrimEnd();
    }

    private string SettigTextnonum(PendingEvent pe)
    {
        string returnstring = "";

        if (EventManager.GameEventNames.TryGetValue(pe.eventType, out var eventName))
        {
            returnstring += $"{eventName}";
        }


        if (EventManager.GameConditionNames.TryGetValue(pe.condition.conditionType, out var cond1))
        {
            if (cond1.Length == 1 || pe.condition.conditionType == EventCondition.ConditionType.FloorBetween)
                returnstring += $"{cond1[0]}";
            else
                returnstring += $"{cond1[0]} {pe.condition.threshold} {cond1[1]}";
        }


        if (EventManager.GameSecondaryConditionNames.TryGetValue(pe.secondaryConditions[0].conditionType, out var cond))
        {
            if (cond.Length == 1)
                returnstring += $"{pe.secondaryConditions[0].parameters[0]}% {cond[0]}";
            else
                returnstring += $"{cond[0]} {pe.secondaryConditions[0].parameters[0]} {cond[1]}";
        }


        if (EventManager.EffectTypeNames.TryGetValue(pe.effect.effectType, out var effectNames))
        {
            var effectParams = pe.effect.parameters;

            for (int i = 1; i < effectNames.Length; ++i)
            {

                if (i - 1 < effectParams.Count)
                {
                    returnstring += $"{effectNames[i]} {effectParams[i - 1]} ";
                }
            }


            returnstring += $"{effectNames[0]}";
        }

        return returnstring.TrimEnd();
    }
    private void OnDestroy()
    {
        if (PoolManager.HasInstance)
        {
            PoolManager.Instance.Remove(this);
        }
    }

    private void ApplyStatIncrease(StatManager stats, StatType stat, float amount)
    {
        switch (stat)
        {
            case StatType.MaxHealth:
                stats.MaxHealth += amount;
                break;
            case StatType.HealthRecoverPower:
                stats.HealthRecoverPower += amount;
                break;
            case StatType.MaxStamina:
                stats.MaxStamina += amount;
                break;
            case StatType.StaminaRecoverPower:
                stats.StaminaRecoverPower += amount;
                break;
            case StatType.MaxMana:
                stats.MaxMana += amount;
                break;
            case StatType.ManaRecoverPower:
                stats.ManaRecoverPower += amount;
                break;
            case StatType.AttackPower:
                stats.AttackPower += amount;
                break;
            case StatType.AttackPowerMultiplier:
                stats.AttackPowerMultiplier += amount;
                break;
            case StatType.AttackSpeed:
                stats.AttackSpeed += amount;
                break;
            case StatType.Defense:
                stats.Defense += amount;
                break;
            case StatType.CriticalChance:
                stats.CriticalChance += amount;
                break;
            case StatType.CriticalDamageMultiplier:
                stats.CriticalDamageMultiplier += amount;
                break;
            case StatType.CriticalGardPenetration:
                stats.CriticalGardPenetration += (int)amount;
                break;
            case StatType.KnockbackResistance:
                stats.KnockbackResistance += amount;
                break;
            case StatType.DefGardLevel:
                stats.DefGardLevel += (int)amount;
                break;
            case StatType.MoveSpeed:
                stats.MoveSpeed += amount;
                break;
            case StatType.JumpForce:
                stats.JumpForce += amount;
                break;
            case StatType.CooldownTime:
                stats.CooldownTime -= amount;
                if (stats.CooldownTime < 0.01f) stats.CooldownTime = 0.01f;
                break;
            case StatType.MaxJumpCount:
                stats.MaxJumpCount += (int)amount;
                break;
            case StatType.Gold:
                stats.Gold += (int)amount;
                break;
            case StatType.ArrowPoint:
                stats.ArrowPoint += (int)amount;
                break;
            case StatType.CurrentHealth:
                stats.gameObject.GetComponent<Entity>().Heal(amount);
                break;
            case StatType.CurrentStamina:
                stats.gameObject.GetComponent<Entity>().Stamina += amount;
                break;
            case StatType.CurrentMana:
                stats.gameObject.GetComponent<Entity>().Mana += amount;
                break;
            case StatType.CurrentShield:
                stats.gameObject.GetComponent<Entity>().AddShield(amount);
                break;
        }
    }


    public void ResetItem()
    {
        statIncreases.Clear();
        firstenableTime = 0.5f;
        rb.linearVelocity = Vector2.zero;
        rb.angularVelocity = 0f;
        rb.gravityScale = 2.5f;
    }

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, targetDir);
    }
#endif
}

스크립트의 이름은 CoinItemScript이지만 실상은 그냥 아이템스크립트의 역할을 하는 기적을 보유하고 있습니다.

 

이 당시에 나는 새로 스크립트를 생성할 시 생기는 로딩 시간이 길어 매우 귀찮았기 때문에 인터페이스, enum, 스텟 상승 정보들이 한 곳에 적혀있는 기괴한 장면을 볼 수 있었습니다.

(당연히 이렇게 짜시는 것은 절대로 추천드리지 않습니다.)

 

코드 구조 요약 시

업데이트에선 생성되고 몇 초 동안은 중력과 기존의 힘만 작용하다가 몇 초가 지난 후엔 근처에 접근한 플레이어에게 자동으로 따라오고 접촉한 엔티티에게 statIncreases에 저장된 StatIncreaseInfo들의 정보에 따라 차례대로 그에 맞는 스텟증감 이밴트를 실행하고 이팩트와 올라간 스텟을 표시하는 텍스트 오브젝트를 호출하는 것으로 요약할 수 있습니다.

 

보시면 알겠지만 딱히 플레이어인지 아닌지 검증하는 부분은 따로 존재하지 않는 걸 볼 수 있습니다.

딱히 레이어 말고는 플레이어를 검증할 것이 따로 없었기도 하고. (실질적으로 플레이어와 적의 차이는 조종, 레이어 외엔 존재하지 않았습니다.)

놀랍게도 개발 초기에는 적들도 스텟 증가 아이템을 먹을 수 있었습니다.(심지어 돈, 후술 할 인챈트 아이템 또한 획득하였습니다.)

개발 진행 도중 이 시스템은 넣기에 부적절하다고 판단하여 이 시스템은 제거되었습니다.

 

 

 

인챈트 시스템

더보기

사용 스크립

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.SceneManagement;
using static SecondaryCondition;
#region Serializable Event Structures

[Serializable]
public class EventCondition
{
    public enum ConditionType
    {
        HealthAbove, HealthBelow,
        StaminaAbove,
        ManaAbove,
        FloorAbove, FloorBetween, FloorBelow,
 Always
    }

    public ConditionType conditionType;
    public float threshold;
    public float multiplier = 1f;

}

[Serializable]
public class SecondaryCondition
{
    public enum SecondaryConditionType
    {
        None, RandomChance,
         PlayerDamageAbove,
         PlayerHitDamageAbove
    }

    public SecondaryConditionType conditionType;
    public List<float> parameters = new(2);
}

[Serializable]
public class EventEffect
{
    public enum EffectType
    {
        Bleed, Stun, Burn, Frost, Drench, Poison, Shock,
        AttackUp, DefenseUp, Shield,
        Impact, Explosion, HeatWave, Vampirism, Wave
    }

    public EffectType effectType;
    public List<float> parameters = new(5);
}

[Serializable]
public class CustomEventData
{
    public Entity target;
    public Transform source;
    public List<EventCondition> conditions = new();
    public List<SecondaryCondition> secondaryConditions = new();
    public List<EventEffect> effects = new();
    public float recentDamageDealt;
    public float recentDamageTaken;
}

#endregion

public enum GameEventType
{

    PlayerAttack,
    PlayerAttackHit,
    PlayerBasicAttack,
    PlayerBasicAttackHit,
    PlayerPierce,
    PlayerPierceHit,
    PlayerTriSlash,
    PlayerTriSlashHit,
    PlayerUppercut,
    PlayerUppercutHit,
    PlayerSpinSlash,
    PlayerSpinSlashHit,
    PlayerCounter,
    PlayerCounterHit,
    PlayerHorizontal,
    PlayerHorizontalHit,
    EquinoxMultiPierce,
    EquinoxMultiPierceHit,
    EquinoxMultiCut,
    EquinoxMultiCutHit,
    EquinoxBigCut,
    EquinoxBigCutHit,

    PlayerGuard,
    PlayerHit,
}


public class EventManager : MonoBehaviour
{
    [SerializeField] private GameObject impecteffect;
    [SerializeField] private GameObject explosion;
    [SerializeField] private GameObject magiceffect;
    [SerializeField] private CinemachineImpulseSource impulseSource;
    public static EventManager Instance { get; private set; }

    private static readonly HashSet<EventEffect.EffectType> IgnoreInVectorEffectTypes = new()
    {
        EventEffect.EffectType.Bleed, EventEffect.EffectType.Stun, EventEffect.EffectType.Burn,
        EventEffect.EffectType.Frost, EventEffect.EffectType.Drench,
        EventEffect.EffectType.Poison, EventEffect.EffectType.Shock,
        EventEffect.EffectType.AttackUp, EventEffect.EffectType.DefenseUp,
        EventEffect.EffectType.Shield, EventEffect.EffectType.Impact
    };

    private static readonly HashSet<SecondaryCondition.SecondaryConditionType> IgnoreInVectorSecondaryTypes = new()
    {

        SecondaryCondition.SecondaryConditionType.PlayerDamageAbove,

        SecondaryCondition.SecondaryConditionType.PlayerHitDamageAbove
    };

    public static readonly Dictionary<GameEventType, float> GameEventMultipliers = new()
    {
        { GameEventType.PlayerAttack, 0.25f },
        { GameEventType.PlayerAttackHit, 0.15f },
        { GameEventType.PlayerBasicAttack, 1f },
        { GameEventType.PlayerBasicAttackHit, 0.5f },
        { GameEventType.PlayerPierce, 1.5f },
        { GameEventType.PlayerPierceHit, 0.75f },
        { GameEventType.PlayerTriSlash, 2f },
        { GameEventType.PlayerTriSlashHit, 1f },
        { GameEventType.PlayerUppercut, 3f },
        { GameEventType.PlayerUppercutHit, 1.5f },
        { GameEventType.PlayerSpinSlash, 4f },
        { GameEventType.PlayerSpinSlashHit, 0.25f },
        { GameEventType.PlayerCounter, 5f },
        { GameEventType.PlayerCounterHit, 1f },
        { GameEventType.PlayerHorizontal, 2f },
        { GameEventType.PlayerHorizontalHit, 1f },
        { GameEventType.EquinoxMultiPierce, 5f },
        { GameEventType.EquinoxMultiPierceHit, 2f },
        { GameEventType.EquinoxMultiCut, 7f },
        { GameEventType.EquinoxMultiCutHit, 1f },
        { GameEventType.EquinoxBigCut, 10f },
        { GameEventType.EquinoxBigCutHit, 3f },
        { GameEventType.PlayerGuard, 1f },
        { GameEventType.PlayerHit, 1f }
    };
    public static readonly Dictionary<EventCondition.ConditionType, string[]> GameConditionNames = new()
{
    { EventCondition.ConditionType.HealthBelow, new[] { "체력이", "% 이하일 때 " } },
    { EventCondition.ConditionType.HealthAbove, new[] { "체력이", "% 이상일 때 " } },

    { EventCondition.ConditionType.StaminaAbove, new[] { "스태미나가", "% 이상일 때 " } },

    { EventCondition.ConditionType.ManaAbove, new[] { "마나가", "% 이상일 때 " } },
    { EventCondition.ConditionType.FloorBelow, new[] { "플로어가", "층 이하일 때 " } },
    { EventCondition.ConditionType.FloorAbove, new[] { "플로어가", "층 이상일 때 " } },
    { EventCondition.ConditionType.FloorBetween, new[] { "플로어가 10의 배수일 때 ", } },
    { EventCondition.ConditionType.Always, new[] { "항상 " } }
};
    public static readonly Dictionary<SecondaryCondition.SecondaryConditionType, string[]> GameSecondaryConditionNames = new()
{
    { SecondaryCondition.SecondaryConditionType.RandomChance, new[] { "확률로 " } },
        { SecondaryCondition.SecondaryConditionType.PlayerDamageAbove, new[] { "데미지가"," 이상일 시 " } },
        { SecondaryCondition.SecondaryConditionType.PlayerHitDamageAbove, new[] { "데미지가", " 이상일 시 " } },

    };
    public static readonly Dictionary<GameEventType, string> GameEventNames = new()
{
    { GameEventType.PlayerAttack, "플레이어 공격 시 " },
    { GameEventType.PlayerAttackHit, "플레이어 공격 적중 시 " },
    { GameEventType.PlayerBasicAttack, "플레이어 기본 공격 사용 시 " },
    { GameEventType.PlayerBasicAttackHit, "플레이어 기본 공격 적중 시 " },
    { GameEventType.PlayerPierce, "찌르기 공격 시 " },
    { GameEventType.PlayerPierceHit, "찌르기 적중 시 " },
    { GameEventType.PlayerTriSlash, "트라이슬래시 사용 시 " },
    { GameEventType.PlayerTriSlashHit, "트라이슬래시 적중 시 " },
    { GameEventType.PlayerUppercut, "올려치기 사용 시 " },
    { GameEventType.PlayerUppercutHit, "올려치기 적중 시 " },
    { GameEventType.PlayerSpinSlash, "회전베기 사용 시 " },
    { GameEventType.PlayerSpinSlashHit, "회전베기 적중 시 " },
    { GameEventType.PlayerCounter, "스톰블레이드 사용 시 " },
    { GameEventType.PlayerCounterHit, "스톰블레이드 적중 시 " },
    { GameEventType.PlayerHorizontal, "가로베기 사용 시 " },
    { GameEventType.PlayerHorizontalHit, "가로배기 적중 시 " },
    { GameEventType.EquinoxMultiPierce, "이퀴녹스 공간절단 사용 시 " },
    { GameEventType.EquinoxMultiPierceHit, "이퀴녹스 공간절단 적중 시 "  },
    { GameEventType.EquinoxMultiCut, "이퀴녹스 절단난무 사용 시 " },
    { GameEventType.EquinoxMultiCutHit, "이퀴녹스 절단난무 적중 시 " },
    { GameEventType.EquinoxBigCut, "이퀴녹스 대절단 사용 시 " },
    { GameEventType.EquinoxBigCutHit, "이퀴녹스 대절단 적중 시 " },
    { GameEventType.PlayerGuard, "플레이어 가드 시 " },
    { GameEventType.PlayerHit, "플레이어 피격 시 " }
};
    public static readonly Dictionary<EventEffect.EffectType, string[]> EffectTypeNames = new()
{
        {EventEffect.EffectType.Bleed, new[] { "스택만큼 출혈 부과 ", " "}},
        {EventEffect.EffectType.Stun, new[] { "스택만큼 기절 부과 ", " "}},
        {EventEffect.EffectType.Burn, new[] { "스택만큼 화상 부과 ", " "}},
        {EventEffect.EffectType.Frost, new[] { "스택만큼 서리 부과 ", " "}},
        {EventEffect.EffectType.Drench, new[] { "스택만큼 침수 부과 ", " " }},
        {EventEffect.EffectType.Poison, new[] { "스택만큼 독 부과 ", ""}},
        {EventEffect.EffectType.Shock, new[] { "스택만큼 감전 부과 ", ""}},
         {EventEffect.EffectType.AttackUp, new[] {"스택 공격력 버프 ", ""}},
        {EventEffect.EffectType.DefenseUp, new[] {"스택 방어력 버프 ", ""}},
        {EventEffect.EffectType.Shield, new[] {"스택 쉴드 생성 ", ""}},
        {EventEffect.EffectType.Impact, new[] {"충격 발생 ", "피해레벨 ", "기절량 "}},
        {EventEffect.EffectType.Explosion, new[] {"폭파 발생 ", "피해레벨 ", "범위레벨 "}},
        {EventEffect.EffectType.Wave, new[] {"파동 발생 ", "피해레벨 ", "생성 간격 ", "생성 횟수 "}},
        {EventEffect.EffectType.HeatWave, new[] {"열파 발생 ", "피해레벨 ", "지속 시간 ", "생성 범위 오차 "}},
        {EventEffect.EffectType.Vampirism, new[] { "%만큼 흡혈 ", ""}}

};

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); return; }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void EventMain(Entity owner, Entity target, EventCondition ec, SecondaryCondition[] sec, EventEffect effect, Statuseffect[] se, GameEventType eventType)
    {
        Debug.Log($"이밴트 실행> {owner},{target},{ec},{sec[0]},{effect},{eventType}");
        if (!CheckPrimaryCondition(owner, ec, se)) return;
        float multiplier = GameEventMultipliers.TryGetValue(eventType, out var baseMultiplier)
                        ? baseMultiplier
                        : 1f;
        Debug.Log($"이밴트 실행> {owner},{target},{ec},{sec[0]},{effect},{eventType}");
        if (!CheckSecondaryCondition(owner, target, sec[0])) return;
        ApplyEffectWithMultiplier(owner, target, effect, multiplier, sec[0]);
    }

    public void EventMain(Entity owner, Vector2 targetPos, EventCondition ec, SecondaryCondition[] sec, EventEffect effect, Statuseffect[] se, GameEventType eventType)
    {
        Debug.Log($"이밴트 실행> {owner},{targetPos},{ec},{sec[0]},{effect},{eventType}");
        if (!CheckPrimaryCondition(owner, ec, se)) return;
        float multiplier = GameEventMultipliers.TryGetValue(eventType, out var baseMultiplier)
                        ? baseMultiplier
                        : 1f;
        Debug.Log($"이밴트 실행> {owner},{targetPos},{ec},{sec[0]},{effect},{eventType}");
        if (!CheckSecondaryCondition(owner, sec[0])) return;
        ApplyEffectWithMultiplier(owner, targetPos, effect, multiplier);

    }

    private bool CheckPrimaryCondition(Entity entity, EventCondition ec, Statuseffect[] se)
    {
        Debug.Log($"CheckPrimaryCondition이밴트 실행> {entity},{ec}");
        return ec.conditionType switch
        {
            EventCondition.ConditionType.HealthAbove => (entity.Health / entity.MaxHealth) * 100 > ec.threshold,
            EventCondition.ConditionType.HealthBelow => (entity.Health / entity.MaxHealth) * 100 < ec.threshold,
            EventCondition.ConditionType.StaminaAbove => (entity.Stamina / entity.MaxStamina) * 100 > ec.threshold,

            EventCondition.ConditionType.ManaAbove => (entity.Mana / entity.MaxMana) * 100 > ec.threshold,

            EventCondition.ConditionType.FloorAbove => StageManager.Instance.Floor > ec.threshold,
            EventCondition.ConditionType.FloorBelow => StageManager.Instance.Floor < ec.threshold,
            EventCondition.ConditionType.FloorBetween => StageManager.Instance.Floor % 10 == 0,
            EventCondition.ConditionType.Always => true,
            _ => false,
        };
    }

    private bool CheckSecondaryCondition(Entity owner, Entity target, SecondaryCondition sc)
    {
        Debug.Log($"CheckSecondaryCondition이밴트 실행> {owner},{target},{sc}");
        return sc.conditionType switch
        {
            SecondaryCondition.SecondaryConditionType.None => true,
            SecondaryCondition.SecondaryConditionType.RandomChance => UnityEngine.Random.Range(0f,100f) <= (sc.parameters.Count > 0 ? sc.parameters[0] : 1f),

            SecondaryCondition.SecondaryConditionType.PlayerDamageAbove =>
                owner != null && sc.parameters.Count > 0 && sc.parameters[1] > sc.parameters[0],

            SecondaryCondition.SecondaryConditionType.PlayerHitDamageAbove =>
                owner != null && target != null && sc.parameters.Count > 0 && sc.parameters[1] > sc.parameters[0],
            _ => true,
        };
    }
    private bool CheckSecondaryCondition(Entity owner, SecondaryCondition sc)
    {
        return sc.conditionType switch
        {
            SecondaryCondition.SecondaryConditionType.None => true,
            SecondaryCondition.SecondaryConditionType.RandomChance => UnityEngine.Random.Range(0f, 100f) <= (sc.parameters.Count > 0 ? sc.parameters[0] : 1f),
            _ => true,
        };
    }

    public void ApplyEffectWithMultiplier(Entity owner, Entity target, EventEffect effect, float multiplier, SecondaryCondition sc)
    {
        Debug.Log($"이팩트 부여,{owner},{target},{effect},{multiplier}");
        switch (effect.effectType)
        {
            case EventEffect.EffectType.Bleed:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Bleed,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Stun:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Stun,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Frost:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Frost,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Burn:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Burn,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Drench:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Drench,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Poison:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Poison,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Shock:
                {
                    target.entityStatusManager.ApplyEffect(StatusEffectType.Shock,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.AttackUp:
                {
                    owner.entityStatusManager.ApplyEffect(StatusEffectType.AttackUp,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.DefenseUp:
                {
                    owner.entityStatusManager.ApplyEffect(StatusEffectType.DefenseUp,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Shield:
                owner.AddShield((effect.parameters.Count > 0 ? effect.parameters[0] : 50f) * multiplier);
                break;
            case EventEffect.EffectType.Impact:
                float dmg = (effect.parameters.Count > 0 ? effect.parameters[0] * owner.statObject.FinalAttackPower : 10f) * multiplier;
                float stun = (effect.parameters.Count > 1 ? effect.parameters[1] : 1f) * multiplier;
                IPoolable impectEffect = PoolManager.Instance.Pop("ImpectEffect");
                impectEffect.GameObject.transform.position = target.transform.position;
                target.TakeDamage(dmg * owner.statObject.FinalAttackPower * 0.5f, 5, Vector2.up * 5, AttackType.Impect, owner, target.transform, impecteffect);
                target.ApplyStun(stun);
                impulseSource.GenerateImpulse(stun / 3.5f);
                impectEffect.GameObject.SetActive(true);
                break;
            case EventEffect.EffectType.Explosion:
                float hDmga = (effect.parameters.Count > 0 ? effect.parameters[0] * owner.statObject.FinalAttackPower : 5f) * multiplier;
                float hRada = (effect.parameters.Count > 2 ? effect.parameters[1] : 1f);
                IPoolable hb = PoolManager.Instance.Pop("AttackHitBox");

                hb.GameObject.GetComponent<AttackTriggerScript>().ChangeSetting(hDmga,3,new Vector2(10,10),magiceffect,5f,AttackType.Magic,AttackElement.Null,owner,0.5f);
                Vector3 basePosEx = target.transform.position;
                hb.GameObject.transform.position = new Vector3(basePosEx.x, basePosEx.y, basePosEx.z);
                bool isPlayerOwnera = (owner.Faction == "Player");
                string waveKeya = isPlayerOwnera ? "Explosion" : "EnemyExplosion";
                IPoolable meff = PoolManager.Instance.Pop(waveKeya);
                meff.GameObject.transform.position = new Vector3(basePosEx.x, basePosEx.y, basePosEx.z);
                meff.GameObject.transform.localScale = new Vector3(hRada, hRada, hRada);
                hb.GameObject.transform.localScale = new Vector3(hRada, hRada, hRada);
                hb.GameObject.SetActive(true);
                meff.GameObject.SetActive(true);
                impulseSource.GenerateImpulse(hRada / 7);
                break;
            case EventEffect.EffectType.HeatWave:
                {
                    Vector2 spawnPos;
                    if (TryGetGroundPosition((Vector2)target.transform.position + Vector2.up * 1f, out spawnPos))
                    {
                        float hDmg = (effect.parameters.Count > 0 ? effect.parameters[0] * owner.statObject.FinalAttackPower : 0.2f) * multiplier;
                        float hDur = (effect.parameters.Count > 1 ? effect.parameters[1] : 0.2f);
                        float hRange = (effect.parameters.Count > 2 ? effect.parameters[2] : 1f);
                        IPoolable heatWave;
                        bool isPlayerOwner = (owner.Faction == "Player");
                        string waveKey = isPlayerOwner ? "HeatWave" : "EnemyHeatWave";
                        heatWave = PoolManager.Instance.Pop(waveKey);
                        heatWave.GameObject.GetComponent<HeatWave>().OnSetting(hDmg, hDur, owner,magiceffect);
                        heatWave.GameObject.transform.position = spawnPos + new Vector2(UnityEngine.Random.Range(-hRange,hRange),0);
                        
                        heatWave.GameObject.SetActive(true);
                    }
                    break;
                }

            case EventEffect.EffectType.Wave:
                {
                    Vector2 spawnPos;
                    if (TryGetGroundPosition((Vector2)target.transform.position + Vector2.up * 1f, out spawnPos))
                    {
                        float wDmg = (effect.parameters.Count > 0 ? effect.parameters[0]  * owner.statObject.FinalAttackPower : 1f) * multiplier;
                        float wIntervalTime = (effect.parameters.Count > 1 ? effect.parameters[1] : 0.3f);
                        int wCount = (effect.parameters.Count > 2 ? (int)effect.parameters[2] : 5);
                        float wSpeed = (effect.parameters.Count > 3 ? effect.parameters[3] : 1f);

                        StartCoroutine(SpawnWave(owner, spawnPos, wDmg, wIntervalTime, wCount, wSpeed));
                    }
                    break;
                }
            case EventEffect.EffectType.Vampirism:
                float vamp = (sc.parameters[1] / (effect.parameters.Count > 0 ? effect.parameters[0] : 50f) * multiplier);
                owner.Heal(vamp);
                break;

        }
    }
    private bool TryGetGroundPosition(Vector2 origin, out Vector2 groundPos)
    {
        RaycastHit2D hit = Physics2D.Raycast(origin, Vector2.down, 10f, LayerMask.GetMask("Ground"));
        if (hit.collider != null)
        {
            groundPos = hit.point;
            return true;
        }
        groundPos = Vector2.zero;
        return false;
    }
    private IEnumerator SpawnWave(Entity owner, Vector2 centerPosition, float damage, float intervalTime, int count, float speed)
    {
        bool isPlayerOwner = (owner.Faction == "Player");
        string waveKey = isPlayerOwner ? "Wave" : "EnemyWave";
        Debug.Log($"SpawnWave 시작: {owner}, {centerPosition}, {damage}, {intervalTime}, {count}, {speed}");
        float pluspos = 0f;
        for (int i = 0; i < count; i++)
        {

            
            IPoolable waveLeft = PoolManager.Instance.Pop(waveKey);
            var leftTransform = waveLeft.GameObject.transform;
            leftTransform.position = new Vector3(centerPosition.x - 1.5f - pluspos, centerPosition.y, 0f);
            leftTransform.localScale = new Vector3(1f, 1f, 1f);

            waveLeft.GameObject.GetComponent<AttackTriggerScript>().ChangeSetting(
                damage,
                3,
                new Vector2(1f, 8f),
                magiceffect,
                1,
                AttackType.Magic,
                AttackElement.Null,
                owner,
                0.5f
            );
            waveLeft.GameObject.SetActive(true);

            IPoolable waveRight = PoolManager.Instance.Pop(waveKey);
            var rightTransform = waveRight.GameObject.transform;
            rightTransform.position = new Vector3(centerPosition.x + 1.5f + pluspos, centerPosition.y, 0f);
            rightTransform.localScale = new Vector3(1f, 1f, 1f);

            waveRight.GameObject.GetComponent<AttackTriggerScript>().ChangeSetting(
                damage,
                3,
                new Vector2(1f, 8f),
                magiceffect,
                1,
                AttackType.Magic,
                AttackElement.Null,
                owner,
                0.5f
            );
            waveRight.GameObject.SetActive(true);
            pluspos += 1.5f;
            yield return new WaitForSeconds(intervalTime);
        }
    }


    private int GetFinalStack(List<float> parameters, float multiplier)
    {
        float baseStack = parameters.Count > 0 ? parameters[0] : 1f;
        float finalValue = baseStack * multiplier;

        if (finalValue >= 1f)
        {
            return Mathf.RoundToInt(finalValue);
        }
        else
        {
            return UnityEngine.Random.value < finalValue ? 1 : 0;
        }
    }
    public void ApplyEffectWithMultiplier(Entity owner, Vector2 target, EventEffect effect, float multiplier)
    {
        Debug.Log($"Vector2 - 이팩트 부여,{owner},{target},{effect},{multiplier}");
        switch (effect.effectType)
        {
            case EventEffect.EffectType.AttackUp:
                {
                    owner.entityStatusManager.ApplyEffect(StatusEffectType.AttackUp,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.DefenseUp:
                {
                    owner.entityStatusManager.ApplyEffect(StatusEffectType.DefenseUp,
                        GetFinalStack(effect.parameters, multiplier));
                    break;
                }
            case EventEffect.EffectType.Shield:
                owner.AddShield((effect.parameters.Count > 0 ? effect.parameters[0] : 50f) * multiplier);
                break;

            case EventEffect.EffectType.Explosion:
                float hDmga = (effect.parameters.Count > 0 ? effect.parameters[0] * owner.statObject.FinalAttackPower : 5f) * multiplier;
                float hRada = (effect.parameters.Count > 2 ? effect.parameters[1] : 1f);
                IPoolable hb = PoolManager.Instance.Pop("AttackHitBox");

                hb.GameObject.GetComponent<AttackTriggerScript>().ChangeSetting(hDmga, 3, new Vector2(10, 10), magiceffect, 5f, AttackType.Magic, AttackElement.Null, owner, 0.5f);
                Vector3 basePosEx = target;
                hb.GameObject.transform.position = new Vector3(basePosEx.x, basePosEx.y, basePosEx.z);
                bool isPlayerOwnera = (owner.Faction == "Player");
                string waveKeya = isPlayerOwnera ? "Explosion" : "EnemyExplosion";
                IPoolable meff = PoolManager.Instance.Pop(waveKeya);
                meff.GameObject.transform.position = new Vector3(basePosEx.x, basePosEx.y, basePosEx.z);
                meff.GameObject.transform.localScale = new Vector3(hRada, hRada, hRada);
                hb.GameObject.transform.localScale = new Vector3(hRada, hRada, hRada);
                hb.GameObject.SetActive(true);
                meff.GameObject.SetActive(true);
                impulseSource.GenerateImpulse(hRada / 7);
                break;
            case EventEffect.EffectType.HeatWave:
                {
                    Vector2 spawnPos;
                    if (TryGetGroundPosition(target + Vector2.up * 1f, out spawnPos))
                    {
                        float hDmg = (effect.parameters.Count > 0 ? effect.parameters[0] * owner.statObject.FinalAttackPower : 0.2f) * multiplier;
                        float hDur = (effect.parameters.Count > 1 ? effect.parameters[1] : 0.2f);
                        float hRange = (effect.parameters.Count > 2 ? effect.parameters[2] : 1f);
                        IPoolable heatWave;
                        bool isPlayerOwner = (owner.Faction == "Player");
                        string waveKey = isPlayerOwner ? "HeatWave" : "EnemyHeatWave";
                        heatWave = PoolManager.Instance.Pop(waveKey);
                        heatWave.GameObject.GetComponent<HeatWave>().OnSetting(hDmg, hDur, owner, magiceffect);
                        heatWave.GameObject.transform.position = spawnPos + new Vector2(UnityEngine.Random.Range(-hRange, hRange), 0);

                        heatWave.GameObject.SetActive(true);
                    }
                    break;
                }

            case EventEffect.EffectType.Wave:
                {
                    Vector2 spawnPos;
                    if (TryGetGroundPosition((Vector2)target + Vector2.up * 1f, out spawnPos))
                    {
                        float wDmg = (effect.parameters.Count > 0 ? effect.parameters[0] * owner.statObject.FinalAttackPower : 1f) * multiplier;
                        float wIntervalTime = (effect.parameters.Count > 1 ? effect.parameters[1] : 0.3f);
                        int wCount = (effect.parameters.Count > 2 ? (int)effect.parameters[2] : 5);
                        float wSpeed = (effect.parameters.Count > 3 ? effect.parameters[3] : 1f);

                        StartCoroutine(SpawnWave(owner, spawnPos, wDmg, wIntervalTime, wCount, wSpeed));
                    }
                    break;
                }

        }
    }

    public bool HasBuff(Entity owner, Entity target, Statuseffect buffSample)
    {
        return target.statusEffects.Exists(buff => buff.name == buffSample.name);
    }
    
}

 

인챈트로 인한 효과 발생 등을 관리하는 스크립트입니다.

인챈트와 관련된 정보(발생하는 행동, 발생하는 행동으로 인챈트 발동 시 인챈트의 위력 증가 비율, 효과)들도 이곳에 작성되어 있습니다.

전체적으로 인챈트 발동 시 주 조건 > 보조 조건 > 효과 순으로 진행하며 각자 조건에 맞지 않을 경우 발동을 취소하고.

조건이 둘 다 만족되있는 경우 target으로 받은 값이 Vector인지, Target인지에 따라 각자 다른 효과 발동 방식(디버프, 흡혈, 충격은 발동되지 않으며. 파동, 열파,폭발은 target일 경우 target의 위치, Vector일 경우 Vector 값에서 발생하는 형식입니다.

 

전체적으로는 그나마 괜찮다고 느껴지지만 코드 가독성이 매우 떨어졌기 때문에 앞으로는 반드시 명심해야 할 요소인 것 같습니다.

넣지 못한 방들

사실 아브락사스는 여러가지 방을 중심으로 개발한 작품이다 보니까 여러 개의 방이 존재할 예정이었습니다.

 

그러나. 생각보다 효과 개발이 극도로 늦어지면서 결국 상점, 일반 방 외엔 특색없는 방들밖에 남지 않게 되었고. 전체적으로 꽤나 아쉬운 게임성이 되어버렸죠.

시간 분배와 능력 부족으 인해 꽤나 괜찮은 방들도 있었지만 빛을 보지 못했던 것이 너무 아쉽습니다.

원래는 일반 방도 층수에 따라 다양한 형태가 존재하도록 할 예정이였습니다.

 

넣으려고 했었던 방들. 이것들은 아예 요소 하나 완성할 시간도 부족하였기 떄문에 폐기하였다

 

결론

전체적으로 스텟 상승 아이템 시스템이 굉장히 난잡하게 이루어졌습니다.

또한 인챈트 발생 스크립트는 몇개를 스크립터블 오브젝트로 대체하였으면 더 좋았을 것 같습니다.

 

개인 평가 : 4점/10점