본문 바로가기

포트폴리오

팀 프로젝트로 제작한 3D 소울 라이크 게임

기획자, 디자이너, 프로그래머 가 모두 모인 프로젝트 팀을 구성해서 약 4달가량 제작한 3D 소울라이크 게임 입니다.

 

 

이 두번째 영상은 위에 올린 팀 트로젝트가 종료된 이후, 해당 프로젝트에 참여했던 프로그래머들 끼리 따로 모여서 아쉬웠던 부분들을 수정하고 추가하고 싶었던 요소들을 추가해서 업그레이드 시킨 버전 입니다.

저는 해당 프로젝트에서 플레이어의 물리기반 움직임(경사로를 오르고 내릴때 속도 증가&감소, 밑으로 떨어질때 자연스럽게 가속도에 따라 속도 증가, 계단 오르내리기, 움직이는 발판 위에 서의 움직임)을 포함한 모든 공격(일반 공격 스킬 공격), 방어 시스템 구현, 인벤토리, 셰이더랩을 이용한 바다 구현, 공용으로 사용할 애니메이션 재생기, 콜라이더(판정)생성기, 이펙트 생성기 + 오브젝트 풀링 등등을 구현 하였습니다.

 

 

 

1.  Playable 캐릭터 클래스 기본 구조

더보기

구조 설명

해당 게임의 특성 상 UI, 몬스터, 맵, 공격 스킬 오브젝트 등등에서 캐릭터의 위치, 현재 Status 변수들, 카메라 GameObject, 등등 에 대한 다양한 접근이 필요하기 때문에 Character가 사용하는 모든 Component, GameObject, 변수들을 아울러서 보관하고 관리할 PlayableCharacter 클래스를 만들고 해당 클래스는 외부에서 쉽게 접근할 수 있도록 하기 위해 싱글톤으로 구성 했습니다. 

 

캐릭터, 몬스터 공용 Status Class

 일단 모든 캐릭터, 몬스터들이 공통적으로 필요한, HP, Stamina, GroggyPoint등등의 Status를 관리하고, 변경사항이 발생 했을때 UI와 연동되어 값을 변경해주고 지정해준 리커버리 수치들에 따라서 재생까지 해주는 클래스를 만들었습니다.

public class BaseStatus:MonoBehaviour
{
    [Header("=========================")]
    [Header("초기 세팅값")]
    [Header("이름")]
    //캐릭터 이름
    [SerializeField]
    public string character_Name;
    [Header("hp")]
    //캐릭터 hp 총량
    [SerializeField]
    public int player_HP;

    //캐릭터 방어력
    [SerializeField]
    public int player_Def;

    [Header("스테미나 총량")]
    //캐릭터 Stamina 총량
    [SerializeField]
    public int player_Stamina;

    [Header("스테미나 자동 회복 시간")]
    // Stamina 자동회복 시간 
    [SerializeField]
    public int player_Stamina_Recovery_Time;

    [Header("스테미나 자동 회복 값")]
    // Stamina 자동회복 값
    [SerializeField]
    public int player_Stamina_Recovery_Val;

    [Header("그로기값 총량")]
    //그로기값 최대치
    [SerializeField]
    public int player_Groggy;

    [Header("그로기값 자동회복 시간")]
    // 그로기값 자동회복 시간 
    [SerializeField]
    public int player_Groggy_Recovery_Time;

    [Header("그로기값 자동회복 값")]
    // 그로기값 자동회복 값
    [SerializeField]
    public int player_Groggy_Recovery_Val;

    [Header("경직상태에 빠지는 그로기값 (누적값이 아니라 한번에 들어온 값으로 판단)")]
    //경직 상태에 빠지는 그로기값
    [SerializeField]
    public int player_Stagger_Groggy;

    [Header("경직상태에 빠지는 그로기값 (누적값으로 판단)")]
    //다운 상태에 빠지는 그로기값
    [SerializeField]
    public int player_Down_Groggy;

    ////캐릭터 움직임 속도
    //[SerializeField]
    //private int player_MoveSpeed;

    ////캐릭터 움직임 속도
    //[SerializeField]
    //private int player_RunSpeed;

    //[SerializeField]
    //private int player_MouseSpeed;

    //[SerializeField]
    //private float player_RotSpeed;

    [SerializeField]
    public Vector2 player_UIPos;


    [Header("=========================")]
    [Header("자동 세팅 변경금지 <Status>")]
    [SerializeField]
    private int curLevel;
    [SerializeField]
    private float maxHP;
    [SerializeField]
    private float curHP;
    [SerializeField]
    private float maxStamina;
    [SerializeField]
    private float curStamina;
    [SerializeField]
    public float Damage;//공격력
    [SerializeField]
    public float Defense;//방어력
    [SerializeField]
    private float maxMP;
    [SerializeField]
    private float curMP;
    [SerializeField]
    public int CurExp;
    [SerializeField]
    public int NextExp;
    [SerializeField]
    private float maxGroggy;
    [SerializeField]
    private float curGroggy;



    [SerializeField]
    public Dictionary<string, CharacterInformation> CharacterDBInfoDic;

    [SerializeField]
    //private DataLoad_Save DBController;
    public UICharacterInfoPanel uiPanel;

    CorTimeCounter timecounter = new CorTimeCounter();
    delegate bool invoker(float val);
    

    IEnumerator CorSTMCount;
    IEnumerator CorSTMRecover;

    IEnumerator CorGroggyCount;
    IEnumerator CorGroggyRecover;

    public void Init(UICharacterInfoPanel uipanel)
    {
        //this.DBController = DBController;
        this.uiPanel = uipanel;

        MaxHP = player_HP;
        MaxStamina = player_Stamina;
        Defense = player_Def;
        MaxGroggy = player_Groggy;
        CurGroggy = 0;
        //CurLevel = 1;

    }

    public float MaxHP
    {
        get => maxHP;
        set
        {
            maxHP = value;
            uiPanel.HPBar.SetMaxValue(value);
            CurHP = maxHP;
        }
    }

    public float CurHP
    {
        get => curHP;
        set
        {
            curHP = value;
            if (curHP >= MaxHP)
            {
                curHP = MaxHP;
            }
            if (curHP <= 0)
            {
                curHP = 0;
            }

            uiPanel.HPBar.SetCurValue(curHP);
        }
    }

    public bool HPUp(float val)
    {
        CurHP = CurHP + val;
        if (CurHP == MaxHP)
        {
            return false;
        }
        return true;
    }

    public bool HPDown(float val)
    {
        CurHP = CurHP - val;
        if (CurHP == 0)
        {
            return false;
        }
        return true;
    }

    public float MaxGroggy
    {
        get => maxGroggy;
        set
        {
            maxGroggy = value;
        }
    }

    public float CurGroggy
    {
        get => curGroggy;
        set
        {
            //현재 스테미나보다 변경될 값이 클때
            if (curGroggy < value)
            {
                //돌고있는 카운트가 있거나 이미 회복중이면 중단해준다.
                if (CorGroggyCount != null)
                {
                    StopCoroutine(CorGroggyCount);
                    CorGroggyCount = null;
                }
                if (CorGroggyRecover != null)
                {
                    StopCoroutine(CorGroggyRecover);
                    CorGroggyRecover = null;
                }

            }


            curGroggy = value;
            if (curGroggy >= MaxGroggy)
            {
                curGroggy = MaxGroggy;
            }
            if (curGroggy <= 0)
            {
                curGroggy = 0;
            }
            //uiPanel.HPBar.SetCurValue(value);


            //그로기값이 있고
            if (curGroggy != 0 && CorGroggyCount == null && CorGroggyRecover == null)
            {
                CorGroggyCount = timecounter.Cor_TimeCounter(player_Groggy_Recovery_Time, GroggyRecoveryStart);
                StartCoroutine(CorGroggyCount);
            }


        }
    }

    public bool GroggyUp(float val)
    {
        CurGroggy = CurGroggy + val;

        //Debug.Log("그로기 증가 현재 그로기 : " + CurGroggy);

        //플레이어가 다운될정도의 그로기 값이 모이면 플레이어 다운
        if (CurGroggy>=player_Down_Groggy)
        {
            //Debug.Log("그로기 증가로 다운 실행");
            CMoveComponent movecom = PlayableCharacter.Instance.GetMyComponent(CharEnumTypes.eComponentTypes.MoveCom) as CMoveComponent;
            movecom.KnockDown();
            return true;
        }

        //들어온 그로기 값이 경직에 빠지게 하는 그로기값이면 경직
        if (val>=player_Stagger_Groggy)
        {
            //Debug.Log("그로기 증가로 경직 실행");
            CMoveComponent movecom = PlayableCharacter.Instance.GetMyComponent(CharEnumTypes.eComponentTypes.MoveCom) as CMoveComponent;
            movecom.KnockBack();

            return true;
        }

        

        if (CurGroggy == MaxGroggy)
        {
            return false;
        }
        return true;
    }

    public bool GroggyDown(float val)
    {
        CurGroggy = CurGroggy - val;
        if (CurGroggy == 0)
        {
            return false;
        }
        return true;
    }

    public float MaxStamina
    {
        get => maxStamina;
        set
        {
            maxStamina = value;
            uiPanel.Staminabar.SetMaxValue(value);
            CurStamina = maxStamina;
        }
    }
    public float CurStamina
    {
        get => curStamina;
        
        set
        {
            //현재 스테미나보다 변경될 값이 작을때
            if(curStamina>value)
            {
                //돌고있는 카운트가 있거나 이미 회복중이면 중단해준다.
                if (CorSTMCount != null)
                {
                    StopCoroutine(CorSTMCount);
                    CorSTMCount = null;
                }
                if (CorSTMRecover != null)
                {
                    StopCoroutine(CorSTMRecover);
                    CorSTMRecover = null;
                }

            }
            
            //값을 변경해주고
            curStamina = value;

            if (curStamina >= MaxStamina)
            {
                curStamina = MaxStamina;
            }
            if (curStamina <= 0)
            {
                curStamina = 0;
            }

            //ui에 반영시켜준다.
            uiPanel.Staminabar.SetCurValue(curStamina);

            //모든 값이 변경된 뒤에 stamina가 최대치가 아니고 이미 회복중이 아니거나 카운트가 돌고있는 중이 아니면 회복을 위한 카운터를 시작해준다. 
            if (curStamina != MaxStamina && CorSTMCount == null&&CorSTMRecover==null)
            {
                CorSTMCount = timecounter.Cor_TimeCounter(player_Stamina_Recovery_Time, STMRecoveryStart);
                StartCoroutine(CorSTMCount);
            }
                
        }
    }

    public bool StaminaUp(float val)
    {
        CurStamina = CurStamina + val;
        if (CurStamina == MaxStamina)
        {
            return false;
        }
        return true;
    }

    public bool StaminaDown(float val)
    {
        CurStamina = CurStamina - val;
        if (CurStamina == 0)
        {
            return false;
        }
        return true;
    }
    


    

    public void STMRecoveryStart()
    {
        //Debug.Log("Staminarecover 시작");
        StopCoroutine(CorSTMCount);
        CorSTMCount = null;

        CorSTMRecover = Recovery(StaminaUp, player_Stamina_Recovery_Val);
        StartCoroutine(CorSTMRecover);
    }

    public void GroggyRecoveryStart()
    {
        //Debug.Log("그로기 회복 시작");
        StopCoroutine(CorGroggyCount);
        CorGroggyCount = null;

        CorGroggyRecover = Recovery(GroggyDown, player_Groggy_Recovery_Val);
        StartCoroutine(CorGroggyRecover);
    }

    IEnumerator Recovery(invoker _invoker,float val)
    {

        while(true)
        {
            //Debug.Log($"{val}회복");

            if (!_invoker(val))
            {
                //Debug.Log("회복 종료");
                yield break;
            }
                

            yield return new WaitForSeconds(1.0f);
        }
    }

    
}

BaseComponent Class

캐릭터가 사용하는 모든 클래스들은 서로서로 변수들을 참조해야할 일이 존재하기 때문에 모든 각각의 클래스들이  BaseComponet 클래스를 상속 받고 해당 클래스들을 PlayableCharacter 클래스에서 관리 함으로써 각각의 클래스들이 쉽게 서로서로를 참조할 수 있도록 하였습니다.

public abstract class BaseComponent : MonoBehaviour
{
    [SerializeField]
    CharEnumTypes.eComponentTypes comtype;

    public CharEnumTypes.eComponentTypes p_comtype
    {
        get
        {
            return comtype;
        }
        set
        {
            comtype = value;
        }
    }
    public abstract void InitComtype();

    public virtual void Init()
    {
        InitComtype();
    }


    public virtual void Awake()
    {
        InitComtype();
    }

    public virtual void InitSetting()
    {
        
    }
}

PlayableCharacter Class

그러고 위의 두 클래스를 변수로 포함시켜 관리하고 외부에서의 다양한 접근을 용이하게 하고 입력 담당 클래스로부터 입력정보를 받아와서 FSM을 이용해 각 동작을 제어해줄 클래스인 PlayalbCharacter를 만들어서 PlayableCharacter 클래스를 구현 하였습니다. 

public class PlayableCharacter : MonoBehaviour
{
    public enum States
    {
        Idle,
        Walk,
        Run,
        Attack,
        Rolling,
        Guard,
        GuardStun,
        OutOfControl,
        AutoMove,
        AreaOfEffect,
    }


    [Header("================BaseComponent================")]
    public BaseComponent[] components = new BaseComponent[(int)CharEnumTypes.eComponentTypes.comMax];

    [SerializeField]
    public BaseStatus status;

    [Header("================캐릭터 UI================")]
    public UICharacterInfoPanel CharacterUIPanel;

    [Header("================피격 이펙트================")]
    public GameObject HitEffect;
    public string HitEffectAdressableName;
    public EffectManager effectmanager;
    public AnimationClip DeadAnimation;

    [Header("================StateMachine================")]
    public MyStateMachine.StateMachine<States, MyStateMachine.Drive> fsm;
    public States curState;

    CMoveComponent movecom;
    AnimationController animator;

    /*싱글톤*/
    static PlayableCharacter _instance;
    public static PlayableCharacter Instance
    {
        get
        {
            return _instance;
        }
    }

    /*초기화*/
    private void Awake()
    {
        _instance = this;
    }

    public bool ComponentInit()
    {
        BaseComponent[] temp = GetComponentsInChildren<BaseComponent>();
        status = GetComponent<BaseStatus>();


        foreach (BaseComponent a in temp)
        {
            if (a.gameObject.activeSelf)
                components[(int)a.p_comtype] = a;
        }

        if (components[1] == null)
            return false;

        return true;
    }

    bool flag = false;

    /*초기화*/
    private void Start()
    {
        //필요한 매니저들이 존재하는지 확인하고 없으면 만들어준다.
        if (FindObjectOfType<ResourceCreateDeleteManager>() == null)
        {
            GameObject obj = new GameObject(typeof(ResourceCreateDeleteManager).Name);
            obj.AddComponent<ResourceCreateDeleteManager>();
        }

        if (FindObjectOfType<EffectManager>() == null)
        {
            GameObject obj = new GameObject(typeof(EffectManager).Name);
            obj.AddComponent<EffectManager>();
        }

        if (FindObjectOfType<ColliderSpawnManager>() == null)
        {
            GameObject obj = new GameObject(typeof(ColliderSpawnManager).Name);
            obj.AddComponent<ColliderSpawnManager>();
        }

        //FSM 생성 & 초기화
        fsm = new MyStateMachine.StateMachine<States, MyStateMachine.Drive>(this);
        SetState(States.Idle);

        //캐릭터에서 사용하는 컴포넌트들 초기화
        ComponentInit();
        movecom = GetMyComponent(CharEnumTypes.eComponentTypes.MoveCom) as CMoveComponent;
        animator = GetComponentInChildren<AnimationController>();


        //UI가 존재하지 않는 경우 UI객체를 로드해서 생성시켜 준다.
        if (CharacterUIPanel == null)
        {
            //if (UIManager.Instance != null)
            //    CharacterUIPanel = UIManager.Instance.Prefabsload(Global_Variable.CharVar.CharacterUI, Canvas_Enum.CANVAS_NUM.player_cavas).GetComponent<UICharacterInfoPanel>();
            //else
            //    CharacterUIPanel = GameMG.Instance.Resource.Instantiate<UICharacterInfoPanel>(Global_Variable.CharVar.CharacterUI);

            if(UIManager.Instance==null)
                CharacterUIPanel = ResourceCreateDeleteManager.Instance.InstantiateObj<UICharacterInfoPanel>(Global_Variable.CharVar.CharacterUI);
            else
                CharacterUIPanel = UIManager.Instance.Prefabsload(Global_Variable.CharVar.CharacterUI, Canvas_Enum.CANVAS_NUM.player_cavas).GetComponent<UICharacterInfoPanel>();

            //CharacterUIPanel = ResourceCreateDeleteManager.Instance.InstantiateObj<UICharacterInfoPanel>(Global_Variable.CharVar.CharacterUI);
        }


        //UI 연동 부분
        status.Init(CharacterUIPanel);

        

        CharacterUIPanel.transform.localPosition = status.player_UIPos;

        if(UIManager.Instance!=null)
        {
            GameObject tempui = UIManager.Instance.Canvasreturn(Canvas_Enum.CANVAS_NUM.start_canvas);
            MainOption mainoption = tempui.GetComponent<MainOption>();
            mainoption.r_invoker = SetReverseMouseRot;
            mainoption.a_invoker = SetCameraColl;
            mainoption.l_invoker = SetOutoFocus;
            mainoption.m_invoker = SetMouseSpeed;

            SetOutoFocus(mainoption.LooKon);
            SetMouseSpeed(mainoption.MouseSensetive);
            SetReverseMouseRot(mainoption.ReverseMouse);
            SetCameraColl(mainoption.AutoeVade);
        }
    }

    //업데이트
    private void Update()
    {
        curState = fsm.GetCurState();
    }

    //자동 이동
    public void AutoMove(Vector3 destpos, float moveTime, CMoveComponent.ActionInvoker invoker = null)
    {
        movecom.AutoMove(destpos, moveTime, invoker);
    }

    //duration 시간동안 목표위치로 이동한다.
    public void DoMove(Vector3 destpos, float duration)
    {
        //movecom.DoMove(destpos, duration);
    }

    //Vector3 방향 * float 거리
    public void Move(Vector3 moveVec,float speed)
    {
        movecom.Move(moveVec, speed);
    }

    //FSM 현재 상태 리턴
    public States GetState()
    {
        return fsm.GetCurState();
    }

    //FSM 이전 상태 리턴
    public States GetLastState()
    {
        return fsm.GetPreState();
    }

    //FSM 상태 변경
    public void SetState(States state)
    {
        fsm.ChangeState(state);
    }

    //자동 포커싱 ON/OFF
    public void SetOutoFocus(bool val)
    {
        OutoFocus = val;
    }

    //마우스 감도 설정
    public void SetMouseSpeed(float val)
    {
        //0~100의 값을 0~5의 값으로 변환해서 넣어준다.
        val = val * 5 * 0.01f;
        movecom.moveoption.RotMouseSpeed = val;
    }

    //마우스 반전 ON/OFF
    public void SetReverseMouseRot(bool val)
    {
        movecom.moveoption.RightReverse = val;
    }

    //카메라 벽 통과 방지 ON/OFF
    public void SetCameraColl(bool val)
    {
        movecom.CameraCollOn = val;
    }

    //UI 생성
    public void CeateUI(GameObject obj)
    {
        CharacterUIPanel = GameObject.Instantiate(obj).GetComponent<UICharacterInfoPanel>();
    }

    /*MyComponent 관련 메소드*/
    public BaseComponent GetMyComponent(CharEnumTypes.eComponentTypes type)
    {
        if(components[(int)type] ==null)
        {
            ComponentInit();
        }

        return components[(int)type];
    }

    //해당 컴포넌트를 비활성화 시켜준다.
    public void InActiveMyComponent(CharEnumTypes.eComponentTypes type)
    {
        if (components[(int)type] == null)
        {
            ComponentInit();
        }

        components[(int)type].enabled = false;
    }

    //해당 컴포넌트를 활성화 시켜준다.
    public void ActiveMyComponent(CharEnumTypes.eComponentTypes type)
    {
        if (components[(int)type] == null)
        {
            ComponentInit();
        }

        components[(int)type].enabled = true;
    }

    //캐릭터의 시점 1인칭 3인칭 에따라 현재 활성화 되어있는 메인 카메라를 리턴해준다.
    public Camera GetCamera()
    {
        //CMoveComponent movecom = GetMyComponent(CharEnumTypes.eComponentTypes.MoveCom) as CMoveComponent;
        if(movecom==null)
            movecom = GetMyComponent(CharEnumTypes.eComponentTypes.MoveCom) as CMoveComponent;

        return movecom.GetCamera();
    }

    /*플레이어 캐릭터 상호작용 메소드*/

    /*플에이어가 공격을 받았을때 해당 함수를 호출
      현재 플레이어의 상태에 따라서 넉백, 가드넉백, 회피 등등의 동작을 결정한다.
      공격을 당했을때 공격을 당한 위치(충돌한 위치)도 함께 넘겨준다.(피격 이펙트를 출력하기 위해)*/
    public void BeAttacked(float damage, Vector3 hitpoint, float Groggy)
    {

        States state = fsm.GetCurState();


        //1. 무조건 공격이 성공하는 상태(Idle, Move, OutOfControl)
        if (state == States.Idle ||
            state == States.Run ||
            state == States.Walk ||
            state == States.OutOfControl)
        {
            Damaged(damage, hitpoint, Groggy);
        }

        //2. 가드중 
        //밸런스게이지가 충분이 남아 있으면 가드에 성공하고 밸런스 게이지를 감소 시킨다.
        //밸런스 게이지가 충분히 남아 있지 않으면 가드에 실패하고 데미지를 입는다.
        else if(state == States.Guard)
        {
            CGuardComponent guardcom = GetMyComponent(CharEnumTypes.eComponentTypes.GuardCom) as CGuardComponent;

            guardcom.Damaged_Guard(damage, hitpoint,Groggy);
        }

        //3. 회피중
        //캐릭터가 회피중이고 무적시간일때는 공격 회피에 성공하고
        //캐릭터가 회피중이지만 무적시간이 아닐때는 회피에 실패하고 데미지를 입는다.
        else if(state == States.Rolling)
        {
            //CMoveComponent movecom = GetMyComponent(CharEnumTypes.eComponentTypes.MoveCom) as CMoveComponent;

            if(!movecom.curval.IsNoDamage)
                movecom.Damaged_Rolling(damage, hitpoint,Groggy);

        }

        //4. 공격중
        else if(state == States.Attack)
        {
            //PlayerAttack attackcom = GetMyComponent(CharEnumTypes.eComponentTypes.AttackCom) as PlayerAttack;
            CAttackComponent attackcom = GetMyComponent(CharEnumTypes.eComponentTypes.AttackCom) as CAttackComponent;
            attackcom.Damaged_Attacking(damage, hitpoint, Groggy);
            //Damaged();
        }

    }

    
    public void Damaged(float damage,Vector3 hitpoint, float Groggy)
    {
        EffectManager.Instance.InstantiateEffect(HitEffect, hitpoint);
        //최종 데미지 = 상대방 데미지 - 나의 현재 방어막
        float finaldamage = damage - status.Defense;
        
        status.GroggyUp(Groggy);
        status.CurHP -= finaldamage;
        //SoundManager.Instance.effectSource.GetComponent<AudioSource>().PlayOneShot(SoundManager.Instance.Player_Audio[2]);

        //캐릭터 사망
        //사망 애니메이션 출력하고 씬 재시작 함수 호출
        if (status.CurHP<=0)
        {
            SetState(States.OutOfControl);

            movecom.com.animator.Play(DeadAnimation.name, 1.0f, 0.0f, 0.2f, Restart);
        }

    }

    public void Restart()
    {
        SetState(States.Idle);
        GameData_Load.Instance.ChangeScene(Scenes_Stage.restart_Loading);
    }

    public void Restart(string _val)
    {
        SetState(States.Idle);
        GameData_Load.Instance.ChangeScene(Scenes_Stage.restart_Loading);
    }

    public BaseStatus GetCharacterStatus()
    {
        return status;
    }
    
    //경험치 획득
    public void GetExp(int exp)
    {
        status.CurExp += exp;
    }

    #region OutoFocusing
    //적 탐색 & 포커싱 세팅
    enum eSearchPoint
    {
        Center,
        Top,
        Right,
        Bottom,
        Left,
        SMax
    }

    [System.Serializable]
    public class Battle_Character_Info
    {
        public Battle_Character_Info(GameObject monster, BoxCollider coll)
        {
            _monster = monster;
            _coll = coll;
            _distance = 0;
            _index = -1;
            _isFocused = false;
            _isBlocked = false;
            _searchPoint = new Vector3[5];
            //피봇으로부터 얼마만큼 떨어져 있는지
            Vector3 center = coll.center;
            Vector3 size = coll.size;
            _searchPoint[(int)eSearchPoint.Center] = center;
            _searchPoint[(int)eSearchPoint.Top] = center + new Vector3(0, size.y/2, 0);
            _searchPoint[(int)eSearchPoint.Right] = center + new Vector3(size.x/2, 0, 0);
            _searchPoint[(int)eSearchPoint.Bottom] = center + new Vector3(0, -size.y / 2, 0);
            _searchPoint[(int)eSearchPoint.Left] = center + new Vector3(-size.x / 2, 0, 0);
        }

        public GameObject _monster;
        public BoxCollider _coll;
        public float _distance;
        public int _index;
        public bool _isFocused;
        public bool _isBlocked;
        public Vector3[] _searchPoint;
    }

    public bool _outoFocus = false;
    public bool OutoFocus
    {
        get
        {
            return _outoFocus;
        }
        set
        {
            _outoFocus = value;
            if (value)
            {
                if (MonsterSearchCor==null)
                {
                    MonsterSearchCor = MonsterSearchCoroutine();
                    StartCoroutine(MonsterSearchCor);
                }
                FocusTab();
            }

        }
    }

    public List<Battle_Character_Info> _monsterObject = new List<Battle_Character_Info>();
    public float _monsterSearchTime = 3.0f;
    private float lastsearchTime;

    public int CurMonsterIndex = 0;
    private bool _isFocusingOn = false;
    public bool IsFocusingOn
    {
        get
        {
            return _isFocusingOn;
        }
        set
        {
            _isFocusingOn = value;
            if(value)
            {
                movecom.com.TpCam.parent = movecom.com.TpCamPos2;
                movecom.com.TpCam.localPosition = Vector3.zero;
                movecom.com.TpCam.localRotation = Quaternion.Euler(0, 0, 0);
            }
            else
            {
                movecom.com.TpCam.parent = movecom.com.TpCamPos;
                movecom.com.TpCam.localPosition = Vector3.zero;
                movecom.com.TpCam.localRotation = Quaternion.Euler(0, 0, 0);
            }
        }
    }
    public int CurFocusedIndex = -1;
    public GameObject CurFocusedMonster;
    public IEnumerator MonsterSearchCor = null;

    public LayerMask Bosslayer;



    //일정 시간마다 화면에 있는 몬스터들을 확인해서 거리별로 리스트에 넣는다.
    //해당 몬스터의 콜라이더를 받아온다.
    public IEnumerator MonsterSearchCoroutine()
    {
        GameObject[] temp;
        List<Battle_Character_Info> tempViewMonster = new List<Battle_Character_Info>();
        BoxCollider Coll;
        RaycastHit hit;
        while (true)
        {
            //Debug.Log("[focus]몬스터 탐지 시작");
            tempViewMonster.Clear();

            temp = AddressablesLoadManager.Instance.ActiveObjectReturn<GameObject>().ToArray();
            // = 

            //해당 몬스터가 카메라 안에 있는지 확인
            for (int i = 0; i < temp.Length; i++)
            {
                if(temp[i].CompareTag("Enemy"))
                {
                    Coll = temp[i].GetComponent<BoxCollider>();
                    Vector3 screenPos = GetCamera().WorldToViewportPoint(Coll.bounds.center);
                    if (screenPos.x >= 0 && screenPos.x <= 1 && screenPos.y >= 0 && screenPos.y <= 1 && screenPos.z >= 0)
                    {
                        //Debug.Log(temp[i].gameObject.name + "[focus]화면에 탐지");
                        
                        Battle_Character_Info info = new Battle_Character_Info(temp[i], Coll);
                        tempViewMonster.Add(info);
                    }
                }
            }

            //카메라 안에 있으면 해당 물체로 ray를 쏴서 장애물이 있는지와 거리를 확인한다.
            //Ray는 캐릭터에서 몬스터로 쏘는게 아니고 카메라에서 몬스터로 쏜다
            //몬스터의 중심, 상,하,좌,우 이렇게 쏜다.
            //몬스터의 중심 상하좌우의 스크린 포인트

            for (int i = tempViewMonster.Count - 1; i >= 0; i--)
            {
                //for (int point = (int)eSearchPoint.Center; point < (int)eSearchPoint.SMax; point++)
                //{
                //    //카메라에서 몬스터 쪽으로 레이를 쏜다.
                //    GameObject monster = tempViewMonster[i]._monster;
                //    Vector3 dir = (monster.transform.position + tempViewMonster[i]._searchPoint[point]) - GetCamera().transform.position;

                //    //레이캐스트 발사
                //    if (Physics.Raycast(GetCamera().transform.position, dir, out hit, 100.0f, Bosslayer))
                //    {
                //        //if(hit.transform.gameObject.layer)
                //        //if(!hit.transform.CompareTag("Enemy"))
                //        if (hit.collider == null)
                //        {
                //            //Debug.Log("[focus]몬스터 탐색 지워져버림");
                //            tempViewMonster.RemoveAt(i);
                //            //tempViewMonster.
                //            //tempViewMonster[i]._isBlocked = true;
                //            //tempViewMonster[i]._distance = 0;
                //            break;
                //        }
                //        else
                //        {
                //            //Debug.Log("[focus]몬스터 탐색 안지워짐");
                //            tempViewMonster[i]._distance = hit.distance;
                //        }
                //    }
                //}
                GameObject monster = tempViewMonster[i]._monster;
                tempViewMonster[i]._distance = (monster.transform.position - transform.position).magnitude;
            }

            //거리에 따라 정렬
            _monsterObject = tempViewMonster.OrderBy(x => x._distance).ToList();

            //
            if(_monsterObject.Count<=0)
            {
                IsFocusingOn = false;
                CurFocusedIndex = 0;
                CurFocusedMonster = null;
                MonsterSearchCor = null;
                yield break;
            }

            //탕색과 정렬을 끝냈는데 현재 포커싱 중인 몬스터가 사라졌으면 포커싱을 끝내준다.
            if(IsFocusingOn)
            {
                int index = _monsterObject.FindIndex(x => x._monster == CurFocusedMonster);
                //탐색을 완료 했는데 포커싱 중인 몬스터가 없어졌을때
                if (index == -1)
                {
                    //오토 포커싱 중이면 다른 몬스터가 있으면 그 몬스터로 포커싱을 옮겨주고
                    //아무것도 없으면 그때 끝내준다.
                    if(OutoFocus)
                    {
                        if(_monsterObject.Count>0)
                        {
                            CurFocusedIndex = 0;
                            yield return null;
                        }
                        else//오토 포커싱 중인데 탐색 결과 
                        {
                            //Debug.Log("[focus] 오토포서싱 중일때 탐색결과 몬스터 존재 X");
                            IsFocusingOn = false;
                        }
                    }
                    else
                    {
                        //Debug.Log("[focus] 오토포커싱 아닐때 탐색결과 몬스터 존재 X");
                        IsFocusingOn = false;
                        CurFocusedIndex = 0;
                        CurFocusedMonster = null;
                        MonsterSearchCor = null;
                        yield break;
                    }

                }
                else
                {
                    CurFocusedIndex = index;
                }
            }




            yield return new WaitForSeconds(_monsterSearchTime);
        }

        //MonsterSearchCor = null;
          
    }

    public void FocusTab()
    {

        if(MonsterSearchCor==null)
        {
            MonsterSearchCor = MonsterSearchCoroutine();
            StartCoroutine(MonsterSearchCor);
        }


        if(!IsFocusingOn)
        {
            if (_monsterObject.Count > 0)
            {
                
                IsFocusingOn = true;
                CurFocusedIndex = 0;
                CurFocusedMonster = _monsterObject[0]._monster;
                //Debug.Log(CurFocusedMonster._monster.gameObject.name + "포커싱 시작");
            }
        }
        else
        {
            if(!OutoFocus)
            {
                if (CurFocusedIndex == _monsterObject.Count - 1)
                {
                    //Debug.Log("[focus]포커싱 눌러서 꺼짐");
                    IsFocusingOn = false;
                    StopCoroutine(MonsterSearchCor);
                    MonsterSearchCor = null;
                }
            }
            
                
            CurFocusedIndex = (CurFocusedIndex + 1) % _monsterObject.Count;
            CurFocusedMonster = _monsterObject[CurFocusedIndex]._monster;


        }


    }
    

}

2.  Unity 셰이더

더보기

Unity 셰이더랩을 이용해 바다 Stage 구현

 

Unity 에서는 ShaderLab이라는 Unity만의 셰이더 스크립트 언어가 존재 합니다.

이는 Unity가 멀티플랫폼 에 모두 대응가능하기 때문 입니다. 다양한 플랫폼에 모두 대응 가능하게 하기 위해서 공통의 인터페이스가 필요한데 이것이 바로 ShaderLab 입니다. 해당 프로젝트에서는 선상전투 씬에서 바다를 구현하는데 사용 했습니다.

 

    Properties
    {
        //파도 텍스쳐
        _BumpTex("BumpTex", 2D) = "Bump"{}
        //메인 텍스쳐
        _MainTex ("tex", 2D) = "white" {}
        //하늘이 반사되는걸 구현할때 사용할 큐브맵, 현재 스카이 박스와 같은 텍스쳐를 넣어준다.
        _CUBE("Cubemap",CUBE) = ""{}
    }

일단 물셰이더와 파도를 구현하기 위해 프로퍼티는 이렇게 3개를 정의했습니다.

 

Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }

태그는 "RenderType"와 "Queue"를 "Transparent"로 설정해주어서 타입은 반투명이 가능하고 Queue는 뒤에서부터 렌더를 하도록 해줍니다.

 

        //인풋 구조체
        struct Input
        {
            //메인 텍스쳐 UV
            float2 uv_MainTex;
            //월드 반사 벡터
            float3 worldRefl;
            //파도 노말 텍스쳐 UV
            float2 uv_BumpTex;

            INTERNAL_DATA
        };

그러고 인풋 구조체를 정의하고 물의 하늘 반사, 잔물결과 파도를 구현하기 위한 VertexShader, SurfaceShader등등을 정의 해서 물 셰이더를 만들어 줍니다.

void vert(inout appdata_full v)
        {
            v.vertex.z += cos(abs(v.texcoord.x * 2 - 2) * 10/*파도간격*/ + _Time.y/*파도속도*/) * 1.5/*1.5*//*파도높이*/;//하프렘버트 역공식과 삼각함수 적용
        }


        void surf (Input IN, inout /*SurfaceOutputStandard*/SurfaceOutput o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
            
            //잔물결을 구현하기 위해 노말맵의 UV좌표를 시간에따라 양 방향에서 가운데로 모이도록 흘러가게 해준다.
            float3 normal1 = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex + _Time.y * 0.01));
            float3 normal2 = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex - _Time.y * 0.01));
            //0.5를 곱해주는 이유는 합 연산 으로 인해 최대값이 2로 늘었기 때문에 해준것이다.
            o.Normal = (normal1 + normal2) * 0.5;
            
            //WorldReflectionVector 함수를 이용해 픽셀당 노멀을 기반으로 반사 벡터를 얻어온 다음에
            //texCUBE함수를 이용해 반사되어 보이는 최종 색상을 알아와서 적용해준다.
            float4 reflection = texCUBE(_CUBE, WorldReflectionVector(IN, o.Normal));
            o.Emission = reflection;

            o.Alpha = 1;
        }
        

        //커스텀 라이트
        //표면의 노말과 뷰벡터를 내적해서 두개의 벡터가 이루는 각의 cos값을 알아낸다.
        float4 Lightingwater(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten)
        {

            float rim = saturate(dot(s.Normal, viewDir));
            //이렇게 만들어진 rim1값은 이루는각이 0도에 가까울수록 0에 가까워지고 90도에 가까워질수록 1에 가까워진다.
            //따라서 바로 발 아래 부분을 보면 투명하게 보이고 먼곳을 바라볼수록 하늘이 반사된 수면이 보이게 된다. 
            float rim1 = pow(1 - rim, 20);
            //이 값을 알파로 적용함으로써 가까운곳은 Rim이 연하게 들어가고 멀리있는것은 쎄게 들어간다.
            float rim2 = pow(1 - rim, 2);

            float4 final = rim * _LightColor0;

            return float4(final.rgb, rim2);
        }

하지만 해당 셰이더는 물의 Speculer 반사와, 물 아래에 있는 물체의 굴절 효과는 적용되지 않아서 자연스럽지 않았기 때문에 이후에 Biln-Phong Shading을 이용한 Speculer 계산과, GrabTexture 이용해 캡쳐한 캡쳐화면을 외곡시켜 줌으로써 만들어낸 굴절 효과 들을 추가시켜 주면서 물 셰이더를 완성 시켰습니다.

SubShader
    {
        Tags{"RenderType" = "Opaque"}
        LOD 200

        GrabPass{}

        CGPROGRAM
        #pragma surface surf water vertex:vert//surface함수 serf, CustomLight함수 water, vertex함수 vert
        #pragma target 3.0

        sampler2D _GrabTexture;
        sampler2D _MainTex;
        samplerCUBE _CUBE;

        //파도 텍스쳐
        sampler2D _BumpTex;

        struct Input
        {
            //메인 텍스쳐 UV
            float2 uv_MainTex;
            //월드 반사 벡터
            float3 worldRefl;
            //파도 노말 텍스쳐 UV
            float2 uv_BumpTex;
            //림 연산을 위해 사용
            float3 viewDir;
            //반사 또는 스크린 공간 효과를 위한 스크린 UV
            float4 screenPos;


            INTERNAL_DATA//반사벡터 노멀처리, WorldReflectionVector 함수 사용을 위한 메크로 선언
        };

        void vert(inout appdata_full v)
        {
            v.vertex.z += cos(abs(v.texcoord.x * 2 - 2) * 10/*파도간격*/ + _Time.y/*파도속도*/) * 0.1/*1.5*//*파도높이*/;//하프렘버트 역공식과 삼각함수 적용
        }


        void surf (Input IN, inout /*SurfaceOutputStandard*/SurfaceOutput o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex);

            /*잔물결 구현 부분*/
            //잔물결을 구현하기 위해 노말맵의 UV좌표를 시간에따라 양 방향에서 가운데로 모이도록 흘러가게 해준다.
            float3 normal1 = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex + _Time.y * 0.01));
            float3 normal2 = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex - _Time.y * 0.01));
            //0.5를 곱해주는 이유는 합 연산 으로 인해 최대값이 2로 늘었기 때문에 해준것이다.
            o.Normal = (normal1 + normal2) * 0.5;


            /*해수면 반사*/
            //WorldReflectionVector 함수를 이용해 픽셀당 노멀을 기반으로 반사 벡터를 얻어온 다음에
            //texCUBE함수를 이용해 반사되어 보이는 최종 색상을 알아와서 적용해준다.
            float4 reflection = texCUBE(_CUBE, WorldReflectionVector(IN, o.Normal));

 
            /*프레넬 반사 구현 부분*/
            //표면의 노말과 뷰벡터를 내적해서 두개의 벡터가 이루는 각의 cos값을 알아낸다.
            float rim = saturate(dot(o.Normal, IN.viewDir));
            //이렇게 만들어진 rim1값은 이루는각이 0도에 가까울수록 0에 가까워지고 90도에 가까워질수록 1에 가까워진다.
            //따라서 바로 발 아래 부분을 보면 투명하게 보이고 먼곳을 바라볼수록 하늘이 반사된 수면이 보이게 된다. 
            float rim1 = pow(1 - rim, 20);
            //이 값을 알파로 적용함으로써 가까운곳은 Rim이 연하게 들어가고 멀리있는것은 쎄게 들어간다.
            float rim2 = pow(1 - rim, 2);


            /*수면 아래 굴절 구현 부분*/
            //스크린 좌표는 기본적으로 Perspective이기 때문에 Vector의 w성분을 나눠줌으로써 Orthographic으로 바꿔준다.
            float3 ScreenUV = IN.screenPos.rgb / IN.screenPos.a;
            //GrabPass를 이용해서 캡쳐한 화면을 tex2D를 이용해서 뽑아오고
            float4 grabtex = tex2D(_GrabTexture, ScreenUV + o.Normal.xy * 0.03) * 0.5;

            //물에의한 외곡, 림라이트, 잔물결 노말 등을 모두 종합해서 적용시켜 준다.
            o.Emission = lerp(grabtex, reflection, rim2) + (rim1 * _LightColor0);

            o.Alpha = 1;
        }


        //커스텀 라이트
        
        float4 Lightingwater(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten)
        {
            /*Speculer적용부분*/

            //speculer는 Biln-Phong Shading에서의 speculer구하는 공식을 이용한다.
            //빛의 방향과 view방향을 더해서 HalfVector를 구하고
            float3 H = normalize(lightDir + viewDir);
            //해당 벡터와 노말벡터를 내적해서 Speculer값을 구한다.
            float spec = saturate(dot(s.Normal, H));
            //강도조절
            spec = pow(spec, 1050) * 10;

            float4 final;
            final.rgb = spec * _LightColor0;
            final.a = s.Alpha + spec;
            return final;
        }
        
        ENDCG
    }

 

 

 

프로젝트 작업 내용들 모음
https://github.com/quattroro/HadasFiesta_Lette_Jo.git

 

풀 소스 깃 주소
https://github.com/browniesss/Lette-Project.git