[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 43회차 미션 시작합니다.

04. 배틀로얄 - 31, 32 번을 진행합니다.




직교 함수 구하기에 대한 설명입니다.
아래 Rotating() 함수에서 right를 구할 때 사용하는 내용입니다.


저번 시간에 계속하여 MoveBehaviour 작업을 계속 이어나갑니다.


public class MoveBehaviour : GenericBehaviour
{
    // 변수선언과 Start() 함수 구현..
    
    // 3D 캐릭터의 이동은 우선 이동방향으로 회전하고 이동..
    Vector3 Rotating(float horizontal, float vertical) {
        Vector3 forward = behaviourController.playerCamera.TransformDirection(Vector3.forward);
        forward.y = 0.0f;
        forward = forward.normalized; // Normal Vector -> 단일 Vector
        
        Vector3 right = new Vector3(forward.z, 0.0f, -forward.x);
        Vector3 targetDirection = Vector3.zero;
        targetDirection = forward * vertical + right * horizontal;
        
        if (behaviourController.IsMoving() && targetDirection != Vector3.zero) {
            Quaternion targetRotation = Quaternion.LookRotation(targetDirection);
            Quaternion newRotation = Quaternion.Slerp(behaviourController.GetRigidbody.rotation, targetRotation, behaviourController.turnSmoothing);
            behaviourController.GetRigidbody.MoveRotation(newRotation);
            behaviourController.SetLastDirection(targetDirection);
        }
        if (!(Mathf.Abs(horizontal) > 0.9f || Mathf.Abs(vertical) > 0.9f)) {
            behaviourController.Repositioning();
        }
        return targetDirection;
    }
    
    private void RemoveVerticalVelocity() {
        Vector3 horizontalVelocity = behaviourController.GetRigidbody.velocity;
        horizontalVelocity.y = 0.0f;
        behaviourController.GetRigidbody.velocity = horizontalVelocity;
    }
    
    void MovementManagement(float horizontal, float vertical) {
        if (behaviourController.IsGrounded()) {
            behaviourController.GetRigidbody.useGravity = true;
        }
        else if (!behaviourController.GetAnimator.GetBool(jumpBool) && behaviourController.GetRigidbody.velocity.y > 0) {
            RemoveVerticalVelocity();
        }
        
        Rotating(horizontal, vertical);
        Vector2 dir = new Vector2(horizontal, vertical);
        speed = Vector2.ClampMagnitude(dir, 1f).magnitude;
        speedSeeker += Input.GetAxis("Mouse ScrollWheel");
        speedSeeker = Mathf.Clamp(speedSeeker, walkSpeed, runSpeed);
        speed *= speedSeeker;
        if (behaviourController.IsSprinting())
            speed = sprintSpeed;
        behaviourController.GetAnimator.SetFloat(speedFloat, speed, speedDampTime, Time.deltaTime);
    }
    
    private void OnCollisionStay(Collision collision) {
        isColliding = true;
        if (behaviourController.IsCurrentBehaviour(GetBehaviourCode) && collision.GetContact(0).normal.y <= 0.1f) {
            float vel = behaviourController.GetAnimator.velocity.magnitude;
            Vector3 targetMove = Vector3.ProjectOnPlane(myTransform.forward, collision.GetContact(0).normal).normalized * vel;
            behaviourController.GetRigidbody.AddForce(targetMove, ForceMove.VelocityChange);
        }
    }
    
    private void OnCollisionExit(Collision collision) {
        isColliding = false;
    }
    

타이핑 따라하기 ㅜ.,ㅡ;

    private void Update() {
        if (!jump && Input.GetButtonDown(ButtonName.Jump) && behaviourController.IsCurrentBehaviour(behaviourCode) && !behaviourCode.IsOverriding())
            jump = true;
    }
    
    public override void LocalFixedUpdate() {
        MovementManagement(behaviourController.GetH, behaviourController.GetV);
        JumpManagement();
    }
}

따라하다..토..할..뻔.. ^^; 수강타겟층이 애매한것 아닐까 싶어요.. 초중타겟으로는 설명할게 너무 많고 중급 이상 타겟으로 하자니 일일이 설명할 게 없는데 초보자 교육이 아니다보니 어쩔 수 없이 코드 따라하기가 되버리는 듯합니다..
물론 코드다 다 말해준다 하지만... 이럴거면 준비된 코드를 가지고 설명을 자세히 해주시는게 좋을 듯도 ㅠ




Freeze Rotation [v]X [v]Y [v]Z를 하여 여기저기로 이동되는 것을 막아줍니다.
유니티 플레이를 해보면 땅에서 열심히 뛰어가는 캐릭터를 볼 수가 있습니다 ^^~




Aim Behaviour 제작을 시작합니다.



우선 이제 실제로 게임을 플레이할 Scene을 불러와서 여기에서 플레이어 및 카메라를 세팅해 줍니다.
그리고 플레이를 해보면 걷고 뛰도 잘 이동하는 것을 볼 수 있습니다.




AimBehaviour 스크립트를 생성하고 코딩을 ㅠ 시작합니다.


/// <summary>
/// 마우스 오른쪽 버튼으로 조준. 다른 동작을 대체해서 동작하게 됩니다.
/// 마우스 휠 버튼으로 좌우 카메라 변경
/// 벽의 모서리에서 조준할 때 상체를 살짝 기울여주는 기능.
/// </summary>
public class AimBehaviour : MonoBehaviour
{

}



위에서 사용되는 Bone 정보를 볼 수 있는 Unity 화면입니다. (초반에 설명했지만 재확인 ㅎ)





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 42회차 미션 시작합니다.

04. 배틀로얄 - 29, 30 번을 진행합니다.



저번 시간에 계속하여 BehaviourController 작업을 계속 이어나갑니다.

public class BehaviourController : MonoBehaviour
{
    // 지난 시간까지 소스작업들..
    
    private void LateUpdate() {
        if (behaviourLocked > 0 || overrideBehaviours.Count == 0) {
            foreach (GenericBehaviour behaviour in behaviours) {
                if (behaviour.isActiveAndEnabled && currentBehaviour == behaviour.GetBehaviourCode)
                    behaviour.LocalLateUpdate();
            }
        }
        else {
            foreach (GenericBehaviour behaviour in overrideBehaviours)
                behaviour.LocalLateUpdate();
        }
    }
    
    public void SubscribeBehaviour(GenericBehaviour behaviour) {
        behaviours.Add(behaviour);
    }
    
    public void RegisterDefaultBehaviour(int behaviourCode) {
        defaultBehaviour = behaviourCode;
        currentBehaviour = behaviourCode;
    }
    
    public void RegisterBehaviour(int behaviourCode) {
        if (currentBehaviour == defaultBehaviour) {
            currentBehaviour = behaviourCode;
        }
    }
    
    public void UnRegisterBehaviour(int behaviourCode) {
        if (currentBehaviour == behaviourCode) {
            currentBehaviour = defaultBehaviour;
        }
    }
    
    public bool OverrideWithBehaviour(GenericBehaviour behaviour) {
        if (!overrideBehaviours.Contains(behaviour)) {
            if (overrideBehaviours.Count == 0) {
                foreach (GenericBehaviour behaviour1 in behaviours) {
                    if (behaviour1.isActiveAndEnabled && currentBehaviour == behaviour1.GetBehaviourCode) {
                        behaviour1.OnOverride();
                        break;
                    }
                }
            }
            overrideBehaviours.Add(behaviour);
            return true;
        }
        return false;
    }
    
    public bool RevokeOverridingBehaviour(GenericBehaviour behaviour) {
        if (overrideBehaviours.Contains(behaviour)) {
            overrideBehaviours.Remove(behaviour);
            return true;
        }
        return false;
    }
    
    public bool IsOverriding(GenericBehaviour behaviour = null) {
        if (behaviour == null) {
            return overrideBehaviours.Count > 0;
        }
        return overrideBehaviours.Contains(behaviour);
    }
    
    public bool IsCurrentBehaviour(int behaviourCode) {
        return currentBehaviour == behaviourCode;
    }
    
    public bool GetTempLockStatus(int behaviourCode = 0) {
        return (behaviourLocked != 0 && behaviourLocked != behaviourCode);
    }
    
    public void LockTempBehaviour(int behaviourCode) {
        if (behaviourLocked == 0)
            behaviourLocked = behaviourCode;
    }
    
    public void UnLockTempBehaviour(int behaviourCode) {
        if (behaviourLocked == behaviourCode)
            behaviourLocked = 0;
    }
    
    public Vector3 GetLastDirection() {
        return lastDirection;
    }
    
    public void SetLastDirection(Vector3 direction) {
        lastDirection = direction;
    }
}




여기까지 작업하고 Unity에서 컴파일 에러가 나오는지 확인해 봅니다. 네~ 아무 에러도 발생하지 않았네요 ^^~ 굳~~~




이제 이동 동작을 만들기입니다. 모든 게임에서는 이동이 기본이라고 할 수 있겠지요.
그리고 현재 프로젝트에서는 이동에 점프를 포함하는 Behaviour로 구현합니다.

FPS라 이동과 점프를 굳이 구분하여 처리할 필요가 없는 프로젝트이기 때문입니다.


 



해당 위치에 MoveBehaviour 스크립트를 생성하고 재미있는 소스코드 작업을 시작합니다 ^^~


///<summary>
/// 이동과 점프 동작을 담당하는 컴포넌트.
/// 충돌처리에 대한 기능도 포함
/// 기본 동작으로써 작동.
///</summary>
public class MoveBehaviour : GenericBehaviour
{
    public float walkSpeed = 0.15f;
    public float runSpeed = 1.0f;
    public float sprintSpeed = 2.0f;
    public float speedDampTime = 0.1f;
    
    public float jumpHeight = 1.5f;
    public float jumpInertialForce = 10f; // 점프 관성
    public float speed, speedSeeker;
    private int jumpBool;
    private int groundedBool;
    private bool isColliding; // 충돌 체크
    private CapsuleCollider capsuleCollider;
    private Transform myTransform; // 캐싱용
    
    private void Start() {
        myTransform = transform;
        capsuleCollider = GetComponent<CapsuleCollider>();
        jumpBool = Animator.StringToHash(AnimatorKey.Jump);
        groundedBool = Animator.StringToHash(AnimatorKey.Grounded);
        behaviourController.GetAnimator.SetBool(groundedBool, true);
        
        behaviourController.SubscribeBehaviour(this);
        behaviourController.RegisterDefaultBehaviour(this.behaviourCode);
        speedSeeker = runSpeed;
    }
    
    // 3D 캐릭터의 이동은 우선 이동방향으로 회전하고 이동..
}


여기까지 변수 선언 및 초기화 작업이 완료되었습니다 ^^~

다음 시간에는 MoveBehaviour 함수들을 계속 구현합니다~




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 41회차 미션 시작합니다.

04. 배틀로얄 - 27, 28 번을 진행합니다.




플러거블 컨트롤러에 대한 설명을 하였었고, 이제 플러거블 컨트롤러 제작을 시작합니다.

우선 기본이 되는 동작 컨트롤러(Behaviour Controller)부터 제작을 시작합니다.

개념을 이해하면 어렵지 않은 내용이니 어떠한 구성과 개념을 가지고 동작하는지를 염두하고 보는 것이 좋습니다.



우선 PlayerCharacter의 Animator에 미리 다운받아 구성되어진 CharacterController라는 Animation을 연결하여 줍니다.




CharacterController에서 "Open"을 클릭하면 Animator Window가 나타나며 어떤 상태에서 어떤 상태로 Transform되는지가 미리 만들어져 있습니다.



Speed이 0일 때는 걷는 애니메이션이 구동되다가 Speed 값을 계속 올려 보면 조금씩 빨라지다가 빠르게 달려가는 것까지 구동되는 것을 테스트해볼 수 있습니다.

 



Configure Avatar를 클릭하면 캐릭터의 뼈대를 볼 수 있는 Bone 화면이 나타납니다.
게임을 진행하다가 몸을 옆으로 약간 기울이는 기능을 만든다거나 할 때 여기에 있는 뼈대를 가지고 스크립트에서 각도를 주어 기울이는 형태로 제작하기 때문에 어디에 있는지 어떤 이름을 가지고 있는지 알아야겠지요 ^^

여기까지 Unity UI 및 PlayerCharacter의 미리 작성된 구성 및 설정에 대한 설명이 끝났습니다 ^^~ 짝짝짝



 

 


Player 폴더를 만들고 BehaviourController 스크립트를 만들어서 제작을 시작합니다. ^^


/// <summary>
/// 현재 동자, 기본 동작, 오버라이딩 동작, 잠긴 동작, 마우스 이동값, 땅에 서있는지,
/// GenericBehaviour를 상속받은 동작들을 업데이트 시켜줍니다.
/// </summary>
public class BehaviourController : MonoBehaviour
{
    private List<GenericBehaviour> behaviours; // 동작들
    private List<GenericBehaviour> overrideBehaviours; // 우선시 되는 동작
    private int currentBehaviour; // 현재 동작 해시코드
    private int defaultBehaviour; // 기본 동작 해시코드
    private int behaviourLocked; // 잠긴 동작 해시코드
    
    // 캐싱
    public Transform playerCamera;
    private Animator myAnimatoor;
    private Rigidbody myRigidbody;
    private ThirdPersonOrbitCam camScript;
    private Transform myTransform;
    
    private float h; // horizontal axis
    private float v; // vertical axis
    public float turnSmoothing = 0.06f; // 카메라를 향하도록 움직일 때 회전속도
    private bool changedFOV; // 달리기 동작이 카메라 시야각이 변경되었을때 저장되었니
    public float sprintFOV = 100; // 달리기 시야각
    private Vector3 lastDirection; // 마지막 향했던 방향
    private bool sprint; // 달리기 중인가?
    private int hFloat; // 애니메이터 관련 가로축 값
    private int vFloat; // 애니메이터 관련 세로축 값
    private int groundedBool; // 애니메이터 지상에 있는가
    private Vector3 colExtents; // 땅과의 충돌체크를 위한 충돌체 영역.
    
    public float GetH { get => h; }
    public float GetV { get => v; }
    public ThirdPersonOrbitCam GetCamScript { get => camScript; }
    public Rigidbody GetRigidbody { get => myRigidbody; }
    public Animator GetAnimator { get => myAnimatoor; }
    public int GetDefaultBehaviour { get => defaultBehaviour; }
   
    private void Awake() {
        behaviours = new List<GenericBehaviour>();
        overrideBehaviours = new List<GenericBehaviour>();
        myAnimatoor = GetComponent<Animator>();
        hFloat = Animator.StringToHash(AnimatorKey.Horizontal);
        vFloat = Animator.StringToHash(AnimatorKey.Vertical);
        camScript = playerCamera.GetComponent<ThirdPersonOrbitCam>();
        myRigidbody = GetComponent<Rigidbody>();
        myTransform = transform;
        // ground??
        groundedBool = Animator.StringToHash(AnimatorKey.Grounded);
        colExtents = GetComponent<Collider>().bounds.extents;
    }
   
    public bool IsMoving() {
        //return (h != 0) || (v != 0); // 부동소숫점 문제가 발생되는 안좋은 코드!!!
        return Mathf.Abs(h) > Mathf.Epsilon || Mathf.Abs(v) > Mathf.Epsilon;
    }
    
    public bool IsHorizontalMoving() {
        return Mathf.Abs(h) > Mathf.Epsilon;
    }
    
    public bool CanSprint() {
        foreach (GenericBehaviour behaviour in behaviours)
            if (!behaviour.AllowSprint)
                return false;
        foreach (GenericBehaviour genericBehaviour in overrideBehaviours)
            if (!genericBehaviour.AllowSprint)
                return false;
        return true;
    }
    
    public bool IsSprinting() {
        return sprint && IsMoving() && CanSprint();
    }
   
    public bool IsGrounded() {
        // 레이저를 충돌체 크기만큼 아래로 쏴서 걸리는 것이 있으면 땅에 있다고 판단
        Ray ray = new Ray(myTransform.position + Vector3.up * 2 * colExtents.x, Vector3.down);
        return Physics.SphereCast(ray, colExtents.x, colExtents.x + 0.2f);
    }
    
    private void Update() {
        h = Input.GetAxis("Horizontal");
        v = Input.GetAxis("Vertical");
        myAnimatoor.SetFloat(hFloat, h, 0.1f, Time.deltaTime);
        myAnimatoor.SetFloat(vFloat, v, 0.1f, Time.deltaTime);
        
        sprint = Input.GetButton(ButtonName.Sprint);
        if (IsSprinting()) {
            changedFOV = true;
            camScript.SetFOV(sprintFOV);
        }
        else if (changedFOV) {
            camScript.ResetFOV();
            changedFOV = false;
        }
        
        myAnimatoor.SetBool(groundedBool, IsGrounded());
    }
   
    public void Repositioning() {
        if (lastDirection != Vector3.zero) {
            lastDirection.y = 0f; // 3인칭 캐릭터의 y이동을 없앰. 안그러면 하늘을 보고 걸어간다거나하는 이상동작.ㅋㅋ
            Quaternion targetRotation = Quaternion.LookRotation(lastDirection);
            Quaternion newRotation = Quaternion.Slerp(myRigidbody.rotation, targetRotation, turnSmoothing);
            myRigidbody.MoveRotation(newRotation);
        }
    }
    
    // Pluggable Behaviour Pattern의 핵심!!!
    private void FixedUpdate() {
        bool isAnyBehaviourActive = false;
        if (behaviourLocked > 0 || overrideBehaviours.Count == 0) {
            foreach (GenericBehaviour behaviour in behaviours) {
                if (behaviour.isActiveAndEnabled && currentBehaviour == behaviour.GetBehaviourCode) {
                    isAnyBehaviourActive = true;
                    behaviour.LocalFixedUpdate();
                }
            }
        }
        else {
            foreach (GenericBehaviour behaviour in overrideBehaviours) {
                behaviour.LocalFixedUpdate();
            }
        }
        if (!isAnyBehaviourActive && overrideBehaviours.Count == 0) {
            myRigidbody.useGravity = true;
            Repositioning();
        }
    }
    
    private void LateUpdate() {
        if (behaviourLocked > 0 || overrideBehaviours.Count == 0) {
            foreach (GenericBehaviour behaviour in behaviours) {
                if (behaviour.isActiveAndEnabled && currentBehaviour == behaviour.GetBehaviourCode)
                    behaviour.LocalLateUpdate();
            }
        }
    }
}

public abstract class GenericBehaviour : MonoBehaviour
{
    protected int speedFloat;
    protected BehaviourController behaviourController;
    protected int behaviourCode;
    protected bool canSprint; // 뛸 수 있는가 여부. 조준중일때는 못뛴다 등.
    
    private void Awake() {
        behaviourController = GetComponent<BehaviourController>();
        speedFloat = Animator.StringToHash(AnimatorKey.Speed);
        canSprint = true;
        // 동작 타입을 해시코드로 가지고 있다가 추후에 구별용으로 사용
        behaviourCode = this.GetType().GetHashCode();
    }
    
    public int GetBehaviourCode { get => behaviourCode; }
    
    public bool AllowSprint { get => canSprint; }
    
    public virtual void LocalLateUpdate() {
    }
    
    public virtual void LocalFixedUpdate() {
    }
    
    public virtual void OnOverride() {
    }
}


다음 시간에도 이어서 BehaviourController의 코드 작업이 계속됩니다.




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 40회차 미션 시작합니다.

04. 배틀로얄 - 25, 26 번을 진행합니다.




저번 시간에 이어 ThirdPersonOrbitCam 스크립트를 지속 작성합니다.

우선 Unity Script의 이벤트 함수 실행 순서는 반드시 숙지해야 하는 내용입니다.

Unity 매뉴얼 최신 URL은 다음과 같습니다.

docs.unity3d.com/kr/2020.2/Manual/ExecutionOrder.html

 

이벤트 함수의 실행 순서 - Unity 매뉴얼

Unity 스크립트를 실행하면 사전에 지정한 순서대로 여러 개의 이벤트 함수가 실행됩니다. 이 페이지에서는 이러한 이벤트 함수를 소개하고 실행 시퀀스에 어떻게 포함되는지 설명합니다.

docs.unity3d.com

 

그래도 정리된 Flowchart를 첨부합니다. 정리가 아주 죽입니다. ㅎㅎ



일단 Camera의 위치가 어떻게 위치하게 되는지 Update() 함수를 임시로 작성하여 확인해 봅니다.
위와 같이 작성후 플레이를 해보면 다음과 같이 오른쪽 어깨 위쯤에서 약간 위를 보는 듯한 시선으로 처리되는 것을 확인할 수 있습니다.



Update() 내용은 모두 지우고 ThirdPersonOrbitCam 스크립트를 계속 작성합니다.


[RequireComponent(typeof(Camera))] // Camera 컴포넌트 있어야 구동.
public class ThirdPersonOrbitCam : MonoBehaviour
{
    // 저번 시간 작성한 변수들..
    
    private void Awake() {
        // 캐싱
        cameraTransform = transform;
        myCamera = cameraTransform.GetComponent<Camera>();
        // 카메라기본 포지션 세팅
        cameraTransform.position = player.position + Quaternion.identity * pivotOffset + Quaternion.identity * camOffset;
        cameraTransform.rotation = Quaternion.identity;
        
        // 카메라의 플레이어간의 상대 벡터, 충돌체크에 사용하기 위함
        relCameraPos = cameraTransform.position - player.position;
        relCameraPosMag = relCameraPos.magnitue - 0.5f; // 플레이어 충돌을 피하기 위한 Offset
        
        // 기본 세팅
        smoothPivotOffset = pivotOffset;
        smoothCamOffset = camOffset;
        defaultFOV = myCamera.fieldOfView;
        angleH = player.eulerAngles.y;
        
        ResetTargetOffsets();
        ResetFOV();
        ResetMaxVerticalAngle();
    }
    
    public void ResetTargetOffsets() {
        targetPivotOffset = pivotOffset;
        targetCamOffset = camOffset;
    }
    
    public void ResetFOV() {
        targetFOV = defaultFOV;
    }
   
    public void ResetMaxVerticalAngle() {
        targetMaxVerticalAngle = maxVerticalAngle;
    }
    
    public void BounceVertical(float degree) {
        recoilAngle = degree;
    }
    
    public void SetTargetOffset(Vector3 newPivotOffset, Vector3 newCamOffset) {
        targetPivotOffset = newPivotOffset;
        targetCamOffset = newCamOffset;
    }
    
    public void SetFOV(float customFOV) {
        targetFOV = customFOV;
    }
    
    bool ViewingPosCheck(Vector3 checkPos float deltaPlayerHeight) {
        Vector3 target = player.position + (Vector3..up * deltaPlayerHeight);
        if (Physics.SphereCast(checkPos, 0.2f, target - checkPos, out RaycastHit hit, relCameraPosMag)) {
            if (hit.transform != player && !hit.transform.GetComponent<Collider>().isTrigger)
                return false;
        }
        return true;
    }
    
    bool ReverseViewingPosCheck(Vector3 checkPos, float deltaPlayerHeight, float maxDistance) {
        Vector3 origin = player.position + (Vector3.up * deltaPlayerHeight);
        if (Physics.SphereCast(origin, 0.2f, checkPos - origin, out RaycastHit hit, maxDistance) {
            if (hit.transform != player & hit.transform != transform && !hit.transform.GetComponent<Collider>().isTrigger)
                return false;
        }
        return true;
    }
    
    bool DoubleViewingPosCheck(Vector3 checkPos, float offset) {
        float playerFocusHeight = player.GetComponent<CapsuleCollider>().height * 0.75f;
        return ViewingPosCheck(checkPos, playerFocusHeight) && ReverseViewingPosCheck(checkPos, playerFocusHeight, offset);
    }
   
    void Update() {
        // 마우스 이동 값..
        angleH += Mathf.Clamp(Input.GetAxis("Mouse X"), -1f, 1f) * horizontalAimingSpeed;
        angleV += Mathf.Clamp(Input.GetAxis("Mouse Y"), -1f, 1f) * verticalAimingSpeed;
        // 수직 이동 제한
        angleV = Mathf.Clamp(angleV, minVerticalAngle, targetMaxVerticalAngle);
        // 수직 카메라 바운스
        angleV = Mathf.LerpAngle(angleV, angleV + recoilAngle, 10f * Time.deltaTime);
        
        // 카메라 회전
        Quaternion camYRotation = Quaternion.Euler(0.0f, angleH, 0.0f);
        Quaternion aimRotation = Quaternion.Euler(-angleV, angleH, 0.0f);
        cameraTransform.rotation = aimRotation;
        
        // Set FOV
        myCamera.fieldOfView = Mathf.Lerp(myCamera.fieldOfView, targetFOV, Time.deltaTime);
        
        Vector3 baseTempPosition = player.position + camYRotation * targetPivotOffset;
        Vector3 noCollisionOffset = targetCamOffset; // 조준할 때 카메라의 오프셋값, 조준할때와 평소때가 다르다.
        for (float zOffset = targetCamOffset.z; zOffset <= 0f; zOffset += 0.5f) {
            noCollisionOffset.z = zOffset;
            if (DoubleViewingPosCheck(baseTempPosition + aimRotation * noCollisionOffset, Mathf.Abs(zOffset)) || zOffset == 0f) {
                break;
            }
        }
        
        // Reposition Camera
        smoothPivotOffset = Vector3.Lerp(smoothPivotOffset, targetPivotOffset, smooth * Time.deltaTime);
        smoothCamOffset = Vector3.Lerp(smoothCamOffset, noCollisionOffset, smooth * Time.deltaTime);
        
        cameraTransform.position = player.position + camYRotation * smoothPivotOffset + aimRotation * smoothCamOffset;
        
        if (recoilAngle > 0.0f) {
            recoilAngle -= recoilAngleBounce * Time.deltaTime;
        }
        else if (recoilAngle < 0.0f) {
            recoilAngle += recoilAngleBounce * Time.deltaTime;
        }
        
        public float GetCurrentPivotMagnitude(Vector3 finalPivotOffset) {
            return Mathf.Abs((finalPivotOffset - smoothPivotOffset).magnitue);
        }
    }
}




위와 같이 하고 컴파일 에러가 없는 것을 확인 후, 플레이를 해보면 마우스 움직임에 따라 카메라 시점이 변경되는 것을 확인할 수 있습니다.







이제 플러거블 동작 시스템을 제작합니다.
우선 플러거블 동작 시스템에 대한 설명입니다. 우선 플러거블 패턴을 사용하지 않았을 때의 문제점에 대해 간략히 설명합니다.





나쁜 코드에 대한 예입니다. 하나의 Update()에서 특정 키를 누를 때 무얼하고 무엇하고, 그런데 이럴때는 예외처리를 하고 등을 넣고 하다보면 Update()가 엄청나게 길어지고 복잡한 코드로 만들어지는 것을 볼 수 있습니다. 이러한 문제가 발생한다는 것이지요.


[7]



디아블로 시간에 설명되었던 Transition에 대한 설명입니다 ^^~
그리고 이것을 구현하는 디자인 패턴 방법이 Pluggable Pattern인 것입니다.



캐릭터를 하나 가져다 놓고, Rigidbody와 CapsuleCollider를 추가하여 대략적인 세팅을 해줍니다.


다음 시간에는 Pluggable Pattern을 하나씩 만들어 보게 되겠네요 ^^~






<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 39회차 미션 시작합니다.

04. 배틀로얄 - 23, 24 번을 진행합니다.


저번 시간에 이어서 SoundTool 스크립트를 지속 작성합니다 ^^


public void FadeTo(Soundclip clip, float time, Interpolate.EaseType ease) {
    if (currentPlayingType == MusicPlayingType.None) {
        FadeIn(clip, time, ease);
    }
    else if (IsDifferentSound(clip)) {
    


캡쳐 화면을 넣을 것이 부족하여 중간중간 화면 캡쳐라도 해서 넣고 있습니다 ^^ 였는데, 오늘은 뒤에 가니 많네요 ㅎㅎ

        if (currentPlayingType == MusicPlayingType.SourceA) {
            PlayAudioSource(fadeB_audio, currentSound, 0.0f);
            currentPlayingType = MusicPlayingType.AtoB;
        }
        else if (currentPlayingType == MusicPlayingType.SourceB) {
            PlayAudioSource(fadeA_audio, currentSound, 0.0f);
            currentPlayingType = MusicPlayingType.BtoA;
        }
        if (currentSound.HasLoop()) {
            isTicking = true;
            DoCheck();
        }
    }
}

public void FadeTo(int index, float time, Interpolate.EaseType ease) {
    FadeTo(DataManager.SoundData().GetCopy(index), time, ease);
}




public void PlayBGM(int index) {
    SoundClip clip = DataManager.SoundData().GetCopy(index);
    PlayBGM(clip);
}

public void PlayUISound(SoundClip clip) {
    PlayAudioSource(UI_audio, clip, clip.maxVolume);
}

public void PlayEffectSound(SoundClip clip) {
    bool isPlaySuccess = false;
    for (int i = 0; i < EffectChannelCount; i++) {
        if (effect_audios[i].isPlaying == false) {
            PlayAudioSource(effect_audios[i], clip, clip.maxVolume);
            effect_PlayStartTime[i] = Time.realtimeSinceStartup;
            isPlaySuccess = true;
        }
        else if (effect_audios[i].clip == clip.GetClip()) {
            effect_audios[i].Stop();
            PlayAudioSource(effect_audios[i], clip, clip.maxVolume);
            effect_PlayStartTime[i] = Time.realtimeSinceStartup;
            isPlaySuccess = true;
        }
    }
    
    if (isPlaySuccess == false) {
        float maxTime = 0.0f;
        int selectIndex = 0;
        for (int i = 0; i < EffectChannelCount; i++) {
            if (effect_PlayStartTime[i] > maxTime) {
                maxTime = effect_PlayStartTime[i];
                selectIndex = i;
            }
        }
        PlayAudioSource(effect_audios[selectIndex], clip, clip.maxVolume);
    }
}

 


PlayEffectSound()와 거의 유사한데, PlayAudioSource() -> PlayAudioSourceAtPoint()로 변경해주면 됩니다.
대신 isPlaySuccess가 실패시 maxTime 체크하던 부분은 필요없이 바로 함수 호출만 해주면 됩니다.


public void PlayOneShotEffect(int index, Vector3 position, float volume) {
    if (index == (int)SoundList.None) return;
    SoundClip clip = DataManager.SoundData().GetCopy(index);
    if (clip == null) return;
    PlayEffectSound(clip, position, volume);
}



public void Stop(bool allStop = false) {
    if (allStop) {
        fadeA_audio.Stop();
        fadeB_audio.Stop();
    }
    
    FadeOut(0.5f, Interpolate.EaseType.Linear);
    currentPlayingType = MusicPlayingType.None;
    StopAllCoroutines();
}

여기까지가 SoundTool의 구현이 끝입니다 ^^~ 수고하셨습니다.
지금까지는 무언가 싶지만 Player나 Monster 작업을 할 때 지금 만들어놓은 것들이 힘을 발휘하게 됨을 볼 수 있습니다.
자세한 설명들도 직접 적용하는 시점에 하기 위해 코딩에 집중하였다고 하네요.. 어쩐지 너무 타이핑 따라하기 느낌어어서 ㅠ.,ㅜ; 설명도 없고.. ㅎㅎ 무언지 알것은 같은데 사용처를 모르니.. 쩝..
이제부터는 3D GameObject를 가지고 움직이는 기능 구현을 시작합니다.






PluggableBehaviour Pattern을 적용하여 플레이어를 적용할 예정입니다.
우선 카메라 제작을 시작합니다.




배틀그라운드나 바이오해저드와 같은 류의 게임을 보면 어깨 뒤쪽 쯤에서 바라보는 카메라를 배치하게 되는데 이것 "3인칭 Shoulder View"입니다.

숄더뷰의 특징이 여러가지 있습니다. 게임을 해보신 분들은 무슨 느낌인지 바로 압니다. ㅎㅎ 왜냐면 숄더뷰 카메라로 인해 생동감도 넘치는 효과가 있고, 좁은 공간에 있으면 숄더뷰가 캐릭터에 가까이 가게 되어 긴장감도 높여주고 하니까 말이죠..
캐릭터와 카메라 사이에 벽이 있으면 카메라가 캐릭터에 거의 붙도록 다가간다는 특징이 있습니다.

캐릭터와 카메라의 충동 체크는 한쪽에서만 진행을 하게되는 경우 체크가 실패할 수가 있기 때문에 양쪽방향으로 체크를하게 됩니다.

 



"Double Viewing Check"라고 부릅니다.

New Scene을 하나 추가하고, Plane을 추가하여 바닥면을 하나 생성해줍니다.
숄더뷰 카메라는 별도로 생성해도 되지만 이미 존재하는 Main Camera를 숄더뷰로 사용합니다.




ThirdPersonOrbitCam 스크립트를 생성하고 코드를 작성합니다.

// 카메라 속성중 중요 속성 하나는 카메라로부터 위치 오프셋 벡터, 피봇 오프셋 벡터
// 위치 오프셋 벡터는 충돌 처리용으로 사용하고 피봇 오프셋 벡터는 시선이동에 사용하도록
// 충돌체크 : 이중 충돌 체크 기능 (캐릭터->카메라, 카메라->캐릭터)
// 사격 반동을 위한 기능
// FOV 변경 기능.
public class ThirdPersonOrbitCam : MonoBehaviour
{
    public Transform player;
    public Vector3 pivotOffset = new Vector3(0.0f, 1.0f, 0.0f);
    public Vector3 camOffset = new Vector3(0.4f, 0.5f, -2.0f);
    
    public float smooth = 10f; // 카메라 반응속도
    public float horizontalAimingSpeed = 6.0f; // 수평 회전속도
    public float verticalAimingSpeed = 6.0f;
    public float maxVerticalAngle = 30.0f;
    public float minVerticalAngle = -60.0f;
    public float recoilAngleBound = 5.0f; // 사격반동 바운스값
    private float angleH = 0.0f; // 마우스 이동에 따른 카메라 수평이동 수치.
    private float angleV = 0.0f;
    private Transform cameraTransform; // 카메라 트랜스폼 캐싱.
    private Camera myCamera;
    private Vector3 relCameraPos; // 플레이어로부터 카메라까지의 벡터.
    private float relCameraPosMag; // 플레이어로부터 카메라사이의 거리.
    private Vector3 smoothPivotOffset; // 카메라 피봇용 보간 벡터
    private Vector3 smoothCamOffset; // 카메라 위치용 보간 벡터
    private Vector3 targetPivotOffset; // 카메라 피봇용 보간 벡터.
    private Vector3 targetCamOffset; // 카메라 위치용 보간 벡터
    private float defaultFOV; // 기본 시야값
    private float targetFOV; // 타겟 시야값
    private float targetMaxVerticleAngle; // 카메라 수직 최대 각도
    private float recoilAngle = 0f; // 사격 반동 각도
    
    public float GetH { get => angleH; } // return angleH; 같은 코드.
}

위까지가 3인칭 숄더뷰를 위한 기본 변수들의 준비를 마쳤습니다.
다음 시간에는 카메라를 구동하는 함수들을 작성하게 됩니다.

FPS 게임이라는 특성으로 인한 카메라 구현 내용이 있고, FPS 특성으로인해 3D 멀미 등을 유발할 수 있기 때문에 이를 방지하기 위한 변수들과 함수들이 존재하게 되는 것 같습니다 ^^.





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0


 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 38회차 미션 시작합니다.

04. 배틀로얄 - 21, 22 번을 진행합니다.




저번 시간에 이어 SoundManager 스크립트를 지속 작업합니다.

public class SoundManager : SingletonMonobehaviour<SoundManager>
{
    // 저번시간 작성했던 변수들..
    
    void Start() {
        if (mixer == null) {
            mixer = Resources.Load(MixerName) as AudioMixer;
        }
        if (audioRoot == null) {
            audioRoot = new GameObject(ContainerName).transform;
            audioRoot.SetParent(transform);
            audioRoot.localPosition = Vector3.zero;
        }
        if (fadeA_audio == null) {
            GameObject fadeA = new GameObject(FadeA, typeof(AudioSource));
            fadeA.transform.SetParent(audioRoot);
            fadeA_audio = fadeA.GetComponent<AudioSource>();
            fadeA_audio.playOnAwake = false;
        }
        if (fadeB_audio == null) {
            GameObject fadeB = new GameObject(FadeB, typeof(AudioSource));
            fadeB.transform.SetParent(audioRoot);
            fadeB_audio = fadeB.GetComponent<AudioSource>();
            fadeB_audio.playOnAwake = false;
        }
        if (UI_audio == null) {
            GameObject ui = new GameObject(UI, typeof(AudioSource));
            ui.transform.SetParent(audioRoot);
            UI_audio = ui.GetComponent<AudioSource>();
            UI_audio.playOnAwake = false;
        }
        if (effect_audios == null || effect_audios.Length == 0) {
            effect_PlayStartTime = new float[EffectChannelCount];
            effect_audios = new AudioSource[EffectChannelCount];
            for (int i = 0; i < EffectChannelCount; i++) {
                effect_PlayStartTime[i] = 0.0f;
                GameObject effect = new GameObject("Effect" + i.ToString(), typeof(AudioSource));
                effect.transform.SetParent(audioRoot);
                effect_audios[i] = effect.GetComponent<AudioSource>();
                effect_audios[i].playOnAwake = false;
            }
        }
        
        if (mixer != null) {
            fadeA_audio.outputAudioMixerGroup = mixer.FindMatchingGroups(BGMGroupName)[0];
            fadeA_audio.outputAudioMixerGroup = mixer.FindMatchingGroups(BGMGroupName)[0];
            UI_audio.outputAudioMixerGroup = mixer.FindMatchingGroups(UIGroupName)[0];
            for (int i = 0; i < effect_audios.Length; i++) {
                effect_audios[i].outputAudioMixerGroup = mixer.FindMatchingGroups(EffectGroupName)[0];
            }
        }
        
        VolumeInit();
    }
    
    public void SetBGMVolume(float currentRatio) {
        currentRatio = Mathf.Clamp01(currentRatio);
        float volume = Mathf.Lerp(minVolume, maxVolume, currentRatio);
        mixer.SetFloat(BGMVolumeParam, volume);
        PlayerPrefs.SetFloat(BGMVolumeParam, volume);
    }
    
    public float GetBGMVolume() {
        if (PlayerPrefs.HasKey(BGMVolumeParam))
            return Mathf.Lerp(minVolume, maxVolume, PlayerPrefs.GetFloat(BGMVolumeParam));
        else
            return maxVolume;
    }
    
    public void SetEffectVolume(float currentRatio) {
        currentRatio = Mathf.Clamp01(currentRatio);
        float volume = Mathf.Lerp(minVolume, maxVolume, currentRatio);
        mixer.SetFloat(EffectVolumeParam, volume);
        PlayerPrefs.SetFloat(EffectVolumeParam, volume);
    }
    
    public float GetEffectVolume() {
        if (PlayerPrefs.HasKey(EffectVolumeParam))
            return Mathf.Lerp(minVolume, maxVolume, PlayerPrefs.GetFloat(EffectVolumeParam));
        else
            return maxVolume;
    }
    
    public void SetUIVolume(float currentRatio) {
        currentRatio = Mathf.Clamp01(currentRatio);
        float volume = Mathf.Lerp(minVolume, maxVolume, currentRatio);
        mixer.SetFloat(UIVolumeParam, volume);
        PlayerPrefs.SetFloat(UIVolumeParam, volume);
    }
    
    public float GetUIVolume() {
        if (PlayerPrefs.HasKey(UIVolumeParam))
            return Mathf.Lerp(minVolume, maxVolume, PlayerPrefs.GetFloat(UIVolumeParam));
        else
            return maxVolume;
    }
    

 



여기까지가 기본적인 사운드 초기화 및 초기 설정을 하는 코드입니다. 이어서 소리 켜고 끄기 및 Fade In/Out 등을 작성합니다.



    void PlayAudioSource(AudioSource source, SoundClip clip, float volume) {  // 소리 재생
        if (source == null || clip == null) return;
        source.Stop();
        source.clip = clip.GetClip();
        source.volume = volume;
        source.loop = clip.isLoop;
        source.pitch = clip.pitch;
        source.dopplerLevel = clip.dopplerLevel;
        source.rolloffMode = clip.rolloffMode;
        source.minDistance = clip.minDistance;
        source.maxDistance = clip.maxDistance;
        source.spartialBlend = clip.spartialBlend;
        source.Play();
    }
    
    void PlayAudioSourceAtPoint(SoundClip clip, Vector3 position, float volume) { // 특정 위치에서 재생
        AudioSource.PlayClipAtPoint(clip.GetClip(), position, volume);
    }
    
    public bool IsPlaying() {
        return (int)currentPlayingType > 0;
    }
    
    public bool IsDifferentSound(SoundClip clip) {
        if (clip == null) return false;
        if (currentSound != null && currentSound.realId == clip.realId && IsPlaying() && currentSound.isFadeOut == false) return false;
        else return true;
    }
   
    private IEnumerator CheckProcess() {
        while (isTicking == true && IsPlaying() == true) {
            yield return new WaitForSeconds(0.05f);
            if (currentSound.HasLoop()) {
                if (currentPlayingType == MusicPlayingType.SourceA) {
                    currentSound.CheckLoop(fadeA_audio);
                }
                else if (currentPlayingType == MusicPlayingType.SourceB) {
                    currentSound.CheckLoop(fadeB_audio);
                }
                else if (currentPlayingType == MusicPlayingType.AtoB) {
                    lastSound.CheckLoop(fadeA_audio);
                    currentSound.CheckLoop(fadeB_audio);
                }
                else if (currentPlayingType == MusicPlayingType.BtoA) {
                    lastSound.CheckLoop(fadeB_audio);
                    currentSound.CheckLoop(fadeA_audio);
                }
            }
        }
    }

 


DoCheck, FadeIn 함수도 위와 같이 구현해 줍니다.

    public void FadeIn(int index, float time, Interpolate.EaseType ease) {
        FadeIn(DataManager.SoundData().GetCopy(index), time, ease);
    }
    
    public void FadeOut(float time, Interpolate.EaseType ease) {
        if (currentSound != null)
            currentSound.FadeOut(time, ease);
    }
   
    void Update() {
        if (currentSound == null) return;
        if (currentPlayingType == MusicPlayingType.SourceA) {
            currentSound.DoFade(Time.deltaTime, fadeA_audio);
        }
        else if (currentPlayingType == MusicPlayingType.SourceB) {
            currentSound.DoFade(Time.deltaTime, fadeB_audio);
        }
        else if (currentPlayingType == MusicPlayingType.AtoB) {
            lastSound.DoFade(Time.deltaTime, fadeA_audio);
            currentSound.DoFade(Time.deltaTime, fadeB_audio);
        }
        else if (currentPlayingType == MusicPlayingType.BtoA) {
            lastSound.DoFade(Time.deltaTime, fadeB_audio);
            currentSound.DoFade(Time.deltaTime, fadeA_audio);
        }
        
        if (fadeA_audio.isPlaying && fadeB_audio.isPlaying == false) {
            currentPlayingType = MusicPlayingType.SourceA;
        }
        else if (fadeB_audio.isPlaying && fadeA_audio.isPlaying == false) {
            currentPlayingType = MusicPlayingType.SourceB;
        }
        else if (fadeA_audio.isPlaying == false && fadeB_audio.isPlaying == false) {
            currentPlayingType = MusicPlayingType.None;
        }
    }
}


사실 이런 구현은 말이 많을 것 같다는 느낌도 들긴 하네요. 분명 위와 같은 기능을 하는 툴이 있어야 함은 분명한데..
매번 툴을 만들때마다 많은 시간이 들어가야 하며 코드 관리도 힘들다면 오히려 짐이 될 것도 같기 때문입니다.

다음 시간에는 사운드 매니저를 마무리하고, 3인칭 카메라 제작에 들어갑니다 ^^~




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 37회차 미션 시작합니다.

04. 배틀로얄 - 19, 20 번을 진행합니다.




드디어 SoundTool입니다 ^^~

EffectTool과 비교한다면 속성 및 설정값이 많기에 조금 복잡해 보이는 코드가 되기 하겠지만 핵심적인 내용은 비슷하므로 천천히 따라가면 충분히 이해할 수 있습니다.




해당 위치에 SoundTool Script를 생성하고 코드 작업을 시작합니다.


public class SoundTool : EditorWindow
{
    public int uiWidthLarge = 450;
    public int uiWidthMiddle = 300;
    public int uiWidthSmall = 200;
    private int selection = 0;
    private Vector2 SP1 = Vector2.zero;
    private Vector2 SP2 = Vector2.zero;
    private AudioClip soundSource;
    private static SoundData soundData;
    
    [MenuItem("Tools/Sound Tool")]
    static void Init() {
        soundData = CreateInstance<SoundData>();
        soundData.LoadData();
        
        SoundTool window = GetWindow<SoundTool>(false, "Sound Tool");
        window.Show();
    }
    
    private void OnGUI() {
        if (soundData == null) return;
        EditorGUILayout.BeingVertical();
        {
            UnityObject source = soundSource;
            SoundClip sound = soundData.soundClips[selection];
            EditorHelper.EditorToolTopLayer(soundData, ref selection, ref source, uiWidthMiddle);
            soundSource = (AudioClip)source;
           
            EditorGUILayout.BeingVertical();
            {
                EditorHelper.EditorToolListLayer(ref SP1, soundData, ref selection, ref source, uiWidthMiddle);
                soundSource = (AudioClip)source;
                
                EditorGUILayout.BeingVertical();
                {
                    SP2 = EditorGUILayout.BeginScrollView(SP2);
                    {
                        if (soundData.GetDataCount() > 0) {
                            EditorGUILayout.BeginVertical();
                            {
                                EditorGUILayout.Separator();
                                SoundClip sound = soundData.soundClips[selection];
                                EditorGUILayout.LabelField("ID", selection.ToString(), GUILayout.Width(uiWidthLarge));
                                soundData.names[selection] = EditorGUILayout.TextField("Name", soundData.names[selection]), GUILayout.Width(uiWidthLarge));
                                sound.playType = (SoundPlayType)EditorGUILayout.EnumPopup("PlayType", sound.playType, GUILayout.Width(uiWidthLarge));
                                sound.maxVolume = EditorGUILayout.FloatField("Max Volume", sound.maxVolume, GUILayout.Width(uiWidthLarge));
                                sound.isLoop = EditorGUILayout.Toggle("LoopClip", sound.isLoop, GUILayout.Width(uiWidthLarge));
                                EditorGUILayout.Separator();
                                if (soundSource == null && sound.clipName != string.Empty) {
                                    soundSource = Resources.Load(sound.clipPath + sound.clipName) as AudioClip;
                                }
                                soundSource = (AudioClip)EditorGUILayout.ObjectField("Audio Clip", soundSource, typeof(AudioClip), false, GUILayout.Width(uiWidthLarge));
                                if (soundSource != null) {
                                    sound.clipPath = EditoHelper.GetPath(soundSource);
                                    sound.clipName = soundSource.name;
                                    sound.pitch = EditorGUILayout.Slider("Pitch", sound.pitch, -3.0f, 3.0f, GUILayout.Width(uiWidthLarge));
                                    
                                    // OMG



                                }
                                else {
                                    sound.clipName = string.Empty;
                                    sound.clipPath = string.Empty;
                                }
                                
                                EditorGUILayout.Separator();
                                if (GUILayout.Button("Add Loop", GUILayout.Width(uiWidthMiddle)) {
                                    soundData.soundClips[selection].AddLoop();
                                }
                                
                                // 더이상 따라가기가 벅차네요 ^^;


                            }
                            EditorGUILayout.EndVertical();
                        }
                    }
                    EditorGUILayout.EndScrollView();
                }
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndVertical();
        }
        EditorGUILayout.EndVertical();
        
        EditorGUILayout.Separator();
       
        EditorGUILayout.BeginHorizontal();
        {
            if (GUILayout.Button("Reload")) {
                soundData = CreateInstance<SoundData>();
                soundData.LoadData();
                selection = 0;
                soundSource = null;
            }
            if (GUILayout.Button("Save")) {
                soundData.SaveData();
                CreateEnumStructure();
                AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            }
        }
        EditorGUILayout.EndHorizontal();
    }
    
    public void CreateEnumStructure() {
        string enumName = "SoundList";
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < soundData.ames.Length; i++) {
            if (!soundData.names[i].ToLower().Contains("none")) {
                builder.AppendLine("    " + soundData.names[i] + " = " + i.ToString() + ",");
            }
            EditorHelper.CreateEnumStructure(enumName, builder);
        }
    }
}




이렇게하여 SoundTool 창이 잘 떠서 동작함을 확인할 수 있습니다.
상세한 사용 설명은 게임을 개발할 때 설정해 보도록 할 예정입니다.





Audio Mixer에 대한 Unity 화면입니다.





SoundManager의 변수들입니다.

public class SoundManager : SingletonMonobehaviour<SoundManager>
{
    // 위의 변수들..
    
    public enum MusicPlayingType {
        None = 0,
        SourceA = 1,
        SourceB = 2,
        AtoB = 3,
        BtoA = 4
    }
    public AudioMixer mixer = null;
    public Transform audioRoot = null;
    public AudioSource fadeA_audio = null;
    public AudioSource fadeB_audio = null;
    public AudioSource[] effect_audios = null; // 예전 5채널 소리째짐을 회피하기 위한 채널갯수 제한.
    public AudioSource UI_audio = null;
    
    public float[] effect_PlayStartTime = null;
    private int EffectChannelCount = 5;
    private MusicPlayingType currentPlayingType = MusicPlayingType.None;
    private bool isTicking = false;
    private SoundClip currentSound = null;
    private SoundClip lastSound = null;
    private float minVolume = -80.0f;
    private float maxVolume = 0.0f;
}


여기까지가 SoundManager의 필요한 변수들을 설정한 내용입니다 ^^;;;
미리 말씀은 해주셨지만 진짜 멘붕이 오네요. ㅋㅋㅋ 나름 괘않을거라 생각하며 열심히 따라하였으나 중간에 왔다갔다 및 버그 수정 등 따라갈 수가 없는 ㅎㅎㅎㅎ

역시나 옵션이 많다보니 타이핑 신공으로 정신없이 쳐야한다는 점이 문제였던것같아요..
다음 시간에도 이어서 SoundTool을 진행합니다.



<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 36회차 미션 시작합니다.

04. 배틀로얄 - 17, 18 번을 진행합니다.




이번 시간에는 저번시간에 제작한 SoundClip을 사용하는 SoundData 모델을 제작합니다.
내용 난이도는 EffectData와 크게 다르지 않지만, 다양한 옵션 등으로 인해 코드가 좀 깁니다.





위의 위치에 SoundData Script를 생성하고 작업을 시작합니다.


/// <summary>
/// 사운드 클립을 배열로 소지, 사운드 데이터를 저장하고 로드하고, 프리로딩을 가짐.
/// </summary>
public class SoundData : BaseData
{
    public SoundClip[] soundClips = new SoundClip[0];
    
    private string clipPath = "Sound/";
    private string xmlFilePath = "";
    private string xmlFileName = "soundData.xml";
    private string dataPath = "Data/soundData";
    private static string SOUND = "sound";
    private static string CLIP = "clip";
    
    public SoundData() {}
    
    public void SaveData() {
        using (XmlTextWriter xml = new XmlTextWriter(xmlFilePath + xmlFileName, System.Text.Encoding.Unicode)) {
            xml.WriteStartDocument();
            xml.WriteStartElement(SOUND);
            xml.WriteElementString("length", GetDataCount().ToString());
            xml.WriteWhitespace("\n");
            
            for (int i = 0; i < names.Length; i++) {
                SoundClip clip = soundClips[i];
                xml.WriteStartElement(CLIP);
                xml.WriteElementString("id", i.ToString());
                xml.WriteElementString("name", names[i]);
                xml.WriteElementString("loops", clip.checkTime.Length.ToString());
                xml.WriteElementString("maxvol", clip.maxVolume.ToString());
                xml.WriteElementString("pitch", clip.pitch.ToString());
                xml.WriteElementString("dopplerlevel", clip.dopplerlevel.ToString());
                xml.WriteElementString("rolloffmode", clip.rolloffMode.ToString());
                xml.WriteElementString("mindistance", clip.minDistance.ToString());
                xml.WriteElementString("maxdistance", clip.maxDistance.ToString());
                xml.WriteElementString("spartialblend", clip.spartialBlend.ToString());
                if (clip.isLoop == true) {
                    xml.WriteElementString("loop", "true");
                }
                xml.WriteElementString("clippath", clip.clipPath);
                xml.WriteElementString("clipname", clip.clipName);
                xml.WriteElementString("checktimecount", clip.checkTime.Length.ToString());
                string str = "";
                foreach (float t in clip.checkTime) {
                    str += t.ToString() + "/";
                }
                xml.WriteElementString("checktime", str);
                str = "";
                xml.WriteElementString("settimecount", clip.setTime.Length.ToString());
                foreach (float t in clip.setTime) {
                    str += t.ToString() + "/";
                }
                xml.WriteElementString("settime", str);
                xml.WriteElementString("type", clip.playType.ToString());
                
                xml.WriteEndElement(); // CLIP
            }
            
            xml.WriteEndElement(); // SOUND
            xml.WriteEndDocument();
        }
    }
   
    public void LoadData() {
        xmlFilePath = Application.dataPath + dataDirectory;
        TextAsset asset = (TextAsset)Resources.Load(dataPath, typeof(TextAsset));
        if (asset == null || asset.text == null) {
            AddData("NewSound");
            return;
        }
        
        using (XmlTextReader reader = new XmlTextReader(new StringReader(asset.text))) {
            int currentID = 0;
            while (reader.Read()) {
                if (reader.IsStartElement()) {
                    switch (reader.Name) {
                        case "length":
                            int length = int.Parse(reader.ReadString());
                            names = new string[length];
                            soundClips = new SoundClip[length];
                            break;
                        case "clip":
                        case "id":
                            currentID = int.Parse(reader.ReadString());
                            soundClips[currentID] = new SoundClip();
                            soundClips[currentID].realId = currentID;
                            break;
                        case "name":
                            names[currentID] = reader.ReadString();
                            break;
                        case "loops":
                            int count = int.Parse(reader.ReadString());
                            soundClips[currentID].checkTime = new float[count];
                            soundClips[currentID].setTime = new float[count];
                            break;
                        case "maxvol":
                            soundClips[currentID].maxVolume = float.Parse(reader.ReadString());
                            break;
                        case "pitch":
                            soundClips[currentID].pitch = float.Parse(reader.ReadString()));
                            break;
                        case "dopplerlevel":
                            soundClips[currentID].dopplerLevel = float.Parse(reader.ReadString());
                            break;
                        case "rolloffmode":
                            soundClips[currentID].rolloffMode = (AudioRolloffMode)Enum.Parse(typeof(AudioRolloffMode), reader.ReadString()));
                        case "mindistance":
                            soundClips[currentID].minDistance = float.Parse(reader.ReadString());
                            break;
                        case "maxdistance":
                            soundClips[currentID].maxDistance = float.Parse(reader.ReadString());
                            break;
                        case "spartialblend":
                            soundClips[currentID].spartialBlend = float.Parse(reader.ReadString());
                            break;
                        case "loop":
                            soundClips[currentID].isLoop = true;
                            break;
                        case "clippath":
                            soundClips[currentID].clipPath = reader.ReadString();
                            break;
                        case "clipname":
                            soundClips[currentID].clipName = reader.ReadString();
                            break;
                        case "checktimecount":
                            break;
                        case "checktime":
                            SetLoopTime(true, soundClips[currentID], reader.ReadString());
                            break;
                        case "settime":
                            SetLoopTime(false, soundClips[currentID], reader.ReadString());
                            break;
                        case "type":
                            soundClips[currentID].playType = (SoundPlayType)Enum.Parse(typeof(SoundPlayType), reader.ReadString()));
                    }
                }
            }
        }
        
        // 미리 불러와 놓기. 리소스가 많아 로딩이 느리다면 제거하라.
        foreach (SoundClip clip in soundClips) {
            clip.PreLoad();
        }
    }
    
    void SetLoopTime(bool isCheck, SoundClip clip, string timeString) {
        string[] time = timeString.Split('/');
        for (int i = 0; i < time.Length; i++) {
            if (time[i] != string.Empty) {
                if (isCheck == true)
                    clip.checkTime[i] = float.Parse(time[i]);
                else
                    clip.setTime[i] = float.Parse(time[i]);
            }
        }
    }
    
    public override int AddData(string newName) {
        if (names == null) {
            names = new string[] { newName };
            soundClips = new SoundClip[] { new SoundClip() };
        }
        else {
            names = ArrayHelper.Add(newName, names);
            soundClips = ArrayHelper.Add(new SoundClip(), soundClips);
        }
    }
    
    public override void RemoveData(int index) {
        names = ArrayHelper.Remove(index, names);
        if (names.Length == 0) names = null;
        soundClips = ArrayHelper.Remove(index, soundClips);
    }
    
    public SoundClip GetCopy(int index) {
        if (index < 0 || index >= soundClips.Length) return null;
        SoundClip clip = new SoundClip();
        SoundClip original = soundClips[index];
        clip.realId = index;
        clip.clipPath = original.clipPath;
        clip.clipName = original.clipName;
        clip.maxVolume = original.maxVolume;
        clip.pitch = original.pitch;
        clip.dopplerLevel = original.dopplerLevel;
        clip.rolloffMode = original.rolloffMode;
        clip.minDistance = original.minDistance;
        clip.maxDistance = original.maxDistance;
        clip.spartialBlend = original.spartialBlend;
        clip.isLoop = original.isLoop;
        clip.checkTime = new float[original.checkTime.Length];
        clip.setTime = new float[original.setTime.Length];
        clip.playType = original.playType;
        for (int i=0; i < clip.checkTime.Length; i++) {
            clip.checkTime[i] = original.checkTime[i];
            clip.setTime[i] = original.setTime[i];
        }
        clip.PreLoad();
        return clip;
    }
    
    public override void Copy(int index) {
        names = ArrayHelper.Add(names[index], names);
        soundClips = ArrayHelper.Add(GetCopy(index), soundClips);
    }
}




여기까지가 SoundData의 완료입니다.. ^^;;;

xml로 저장하고 불러와서 파싱하기가 주요코드인데 옵션들이 많다보니 반복적인 코드가 많고 눈이 빙글빙글 ㅎㅎ..

다음 시간에는 SoundTool을 제작합니다...





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 35회차 미션 시작합니다.

04. 배틀로얄 - 15, 16 번을 진행합니다.




BaseData::GetDataCount() 함수의 오류가 있어서 버그를 수정한 내용입니다.
위와 같이 수정하고 저번시간의 EffectTool Window를 실행해 보면 기본적인 구동이 되는 것을 확인할 수 있습니다.




ADD를 클릭하여 Item을 하나 더 추가하고.. 이름을 설정하고 Prefabs/Effects에 준비된 Effect GameObject를 마우스클릭으로 드래그하여 이팩트에 가져다 놓으면 연결되어 동작하는 것도 확인할 수 있습니다.




추가한 이후 [SAVE]를 클릭하면 effectData와 EffectList에 영향을 주게 됩니다.

우선 effectData 파일을 더블클릭하여 열어봅니다.
다음과 같이 한 줄로 구성된 xml 파일의 내용을 볼 수 있습니다.
이렇게 보면 좀 불편한대요..


 

 


이렇게 Excel 문서에서 열어서 보면 좀더 구성이 잘된 모양의 동일한 effectData xml 파일의 내용을 볼 수가 있습니다.

 



또한 EffectList 파일을 더블클릭하여 열어보면 EffectList라는 enum도 Tool에서 추가한 아이템 내용대로 업데이트된 것을 확인할 수가 있습니다.
이렇게 수행되도록 만든 코드가 CreateEnumStructure()라는 함수였고 이것이 수행된 결과인 것입니다.




이제 Manager 폴더 아래에 DataManager와 EffectManager 2개의 스크립트를 준비합니다.

우선 DataManager부터 작성을 시작합니다.

public class DataManager : MonoBehaviour
{
    private static EffectData effectData = null;
    
    void Start() {
        if (effectData == null) {
            effectData = ScriptableObject.CreateInstance<EffectData>();
            effectData.LoadData();
        }
    }
    
    public static EffectData EffectData() {
        if (effectData == null) {
            effectData = ScriptableObject.CreateInstance<EffectData>();
            effectData.LoadData();
        }
        return effectData;
    }
}


이제 EffectManager를 작성합니다.

public class EffectManager : SingletonMonoBehaviour<EffectManager>
{
    private Transform effectRoot = null;
    
    void Start() {
        effectRoot = new GameObject("EffectRoot").transform;
        effectRoot.SetParent(transform);
    }
    
    //## 핵심 코드
    public GameObject EffectOneShot(int index, Vector3 position) {
        EffectClip clip = DataManager.EffectData().GetClip(index);
        GameObject effectInstance = clip.Instantiate(position);
        effectInstance.SetActive(true);
        return effectInstance;
    }
}





SingletonMonoBehaviour의 소스는 위와 같으며 프로젝트에 기본 포함하여 배포되었습니다.

위와같이 하여 EffectTool이 끝난거라고 합니다.. 무언가 Unity에서 확인해보고 구동하는 것이 있을거라 생각했는데 아니었네요 ㅎㅎ
바로 SoundTool 제작으로 넘어갑니다..




SoundClip 스크립트를 만들고 작성을 시작합니다 ^^; SoundTool은 EffectTool과 핵심적인 기능은 동일한데 Sound의 특성상 Loop 기능이나 CheckPoint 및 FadeOut등의 효과 등의 처리가 많아서 데이터가 많기 때문에 조금 복잡하다는 것만 다르다고 보면 됩니다.


// 루프, 페이드 관련 속성, 오디오 클립 속성들.
public class SoundClip
{
    public SoundPlayType playTpe = SoundPlayType.None;
    public string clipName = string.Empty;
    public string clipPath = string.Empty;
    public float maxVolume = 1.0f;
    public bool isLoop = false;
    public float[] checkTime = new float[0];
    public float[] setTime = new float[0];
    public int realId = 0;
    
    private AudioClip clip = null;
    public int currentLoop = 0;
    public float pitch = 1.0f;
    public float dopplerLevel = 1.0f;
    public AudioRolloffMode rolloffMode = AudioRolloffMode.Logarithmic;
    public float minDistance = 10000.0f;
    public float maxDistance = 50000.0f;
    public float sparialBlend = 1.0f;
    
    public float fadeTime1 = 0.0f;
    public float fadeTime2 = 0.0f;
    public Interpolate.Function interpolate_Func;
    public bool isFadeIn = false;
    public bool isFadeOut = false;
    
    public SoundClip() { }
    public SoundClip(string clipPath, string clipName) {
        this.clipPath = clipPath;
        this.clipName = clipName;
    }
    public void PreLoad() {
        if (this.clip == null) {
            string fullPath = this.clipPath + this.clipName;
            this.clip = ResourceManager.Load(fullPath) as AudioClip;
        }
    }
    
    public void AddLoop() {
        checkTime = ArrayHelper.Add(0.0f, this.checkTime);
        setTime = ArrayHelper.Add(0.0f, this.setTime);
    }
    public void RemoveLoop(int index) {
        checkTime = ArrayHelper.Remove(index, this.checkTime);
        setTime = ArrayHelper.Remove(index, this.setTime);
    }
    public AudioClip GetClip() {
        if (clip == null) PreLoad();
        if (clip == null && clipName != string.Empty) {
            Debug.LogWarning($"Can not load audio clip Resource {clipName}");
            return null;
        }
        return clip;
    }
    public void ReleaseClip() {
        if (clip != null) clip = null;
    }
    public bool HasLoop() {
        return checkTime.Length > 0;
    }
    public void NextLoop() {
        currentLoop++;
        if (currentLoop >= checkTime.Length) currentLoop = 0;
    }
    public void CheckLoop(AudioSource source) {
        if (HasLoop() && source.time >= checkTime[currentLoop]) {
            source.time = setTime[currentLoop];
            NextLoop();
        }
    }
    
    public void FadeIn(float time, Interpolate.EaseType easeType) {
        isFadeOut = false;
        fadeTime1 = 0.0f;
        fadeTime2 = time;
        interpolate_Func = Interpolate.Ease(easeType);
        isFadeIn = true;
    }
    public void FadeOut(float time, Interpolate.EaseType easeType) {
        isFadeIn = false;
        fadeTime1 = 0.0f;
        fadeTime2 = time;
        interpolate_Func = Interpolate.Ease(easeType);
        isFadeOut = true;
    }
    /// <summary>
    /// 페이드인,아웃 효과 프로세스
    /// </summary>
    public void DoFade(float time, AudioSource audio) {
        if (isFadeIn == true) {
            fadeTime1 += time;
            audio.volume = Interpolate.Ease(interpolate_Func, 0, maxVolume, fadeTime1, fadeTime2);
            if (fadeTime1 >= fadeTime2) isFadeIn = false;
        }
        else if (isFadeOut == true) {
            fadeTime1 += time;
            audio.volume = Interpolate.Ease(interpolate_Func, maxVolume, 0 - maxVolume, fadeTime1, fadeTime2);
            if (fadeTime1 >= fadeTime2) {
                isFadeOut = false;
                audio.Stop();
            }
        }
    }
}

여기까지가 SoundClip 완성입니다. 다음시간부터는 SoundManager를 통하여 Clip을 관리하는 기능을 구현할 것입니다.
결국 SoundClip은 소리 재생인데 게임 상황상 여러가지 옵션이 많다보니 함수를 많이 구현하게 된 것입니다.


Easing functions에 대해서는 구글에서 검색한 이미지를 하나 추가합니다.






<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 34회차 미션 시작합니다.

04. 배틀로얄 - 13, 14 번을 진행합니다.




EffectTool Script 생성하여 진행합니다 ^^~


using UnityEngine;
using UnityEditor; // Editor Tool을 만들기 위해 필수
using System.Text;
using UnityObject = UnityEngine.Object;

// Clip Property를 수정하고, 읽고, 저장하는 기능.
public class EffectTool : EditorWindow
{
    //UI 그리는데 필요한 변수들.
    pubic int uiWidthXLarge = 450; // pixel 크기.
    pubic int uiWidthLarge = 300;
    pubic int uiWidthMiddle = 200;
    private int selection = 0; // 선택 index
    private Vector2 SP1 = Vector2.zero;
    private Vector2 SP2 = Vector2.zero;
}

여기까지가 기본적인 처리를 위한 준비 과정을 마친 상태이고, 이제 본격적으로 이팩트 툴 작성에 들어갑니다 ^^


 



public class EffectTool : EditorWindow
{
    // 위의 변수들..
    
    // 이팩트 클립
    private GameObject effectSource = null;
    // 이팩트 데이터
    private static EffectData effectData;
    
    [MenuItem("Tools/Effect Tool")]  // 유니티에 메뉴 생성
    static void Init() {
        effectData = ScriptableObject.CreateInstance<EffectData>();
        effectData.LoadData();
        
        EffectTool window = GetWindow<EffectTool>(false, "Effect Tool");
        window.Show();
    }
    
    private void OnGUI() {
        if (effectData == null) return;
        
        EditorGUILayout.BeginVertical();
        {
            // 상단 - add, remove, copy
            UnityObject source = effectSource;
            EditorHelper.EditorToolTopLayer(effectData, ref selection, ref source, this.uiWidthMiddle);
            effectSource = (GameObject)source;
            
            EditorGUILayout.BeginHorizontal();
            {
                // 중단 - data list
                EditorHelper.EditorToolListLayer(ref SP1, effectData, ref selection, ref source, this.uiWidthLarge);
                effectSource = (GameObject)source;
                
                // 설정 부분
                EditorGUILayout.BeginVertical();
                {
                    SP2 = EditorGUILayout.BeginScrollView(this.SP2);
                    {
                        if (effectData.GetDataCount() > 0) {
                            EditorGUILayout.BeginVertical();
                            {
                                EditorGUILayout.Separator();
                                EditorGUILayout.LabelField("ID", selection.ToString(), GUILayout.Width(uiWidthLarge));
                                effectData.names[selection] = EditorGUILayout.TextField("이름.", effectData.names[selection], GUILayout.Width(uiWidthXLarge));
                                effectData.effectClips[selection].effectType = (EffectType)EditorGUILayout.EnumPopup("이팩트 타입.", effectData.effectClips[selection].effectType, GUILayout.Width(uiWidthLarge));
                                EditorGUILayout.Separator();
                                if (effectSource == null && effectData.effectClips[selection].effectName != string.Empty) {
                                    effectData.effectClips[selection].PreLoad();
                                    effectSource = Resources.Load(effectData.effectClips[selection].effectPath + effectData.effectClips[selection].effectName) as GameObject;
                                }
                                effectSource = (GameObject)EditorGUILayout.ObjectField("이팩트", this.effectSource, typeof(GameObject), false, GUILayout.Width(uiWidthXLarge));
                                if (effectSource != null) {
                                    effectData.effectClips[selection].effectPath = EditorHelper.GetPath(this.effectSource);
                                    effectData.effectClips[selection].effectName = effectSource.name;
                                }
                                else {
                                    effectData.effectClips[selection].effectPath = string.Empty;
                                    effectData.effectClips[selection].effectName = string.Empty;
                                    effectSource = null;
                                }
                                EditorGUILayout.Separator();
                            }
                            EditorGUILayout.EndVertical();
                        }
                    }
                    EditorGUILayout.EndScrollView();
                }
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndHorizontal();
        }
        EditorGUILayout.EndVertical();
        
        EditorGUILayout.Separator();
       
        // 하단
        EditorGUILayout.BeginHorizontal();
        {
            if (GUILayout.Button("Reload Settings")) {
                effectData = CreateInstance<EffectData>();
                effectData.LoadData();
                selection = 0;
                this.effectSource = null;
            }
            if (GUILayout.Button("Save")) {
                EffectTool.effectData.SaveData();
                CreateEnumStructure();
                AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            }
        }
        EditorGUILayout.EndHorizontal();
    }
    
    public void CreateEnumStructure() {
        string enumName = "EffectList";
        StringBuilder builder = new StringBuilder();
        builder.AppendLine();
        for (int i = 0; i < effectData.names.Length; i++) {
            if (effectData.names[i] != string.Empty) {
                builder.AppendLine("   " + effectData.names[i] + " = " + i + ",");
            }
        }
        EditorHelper.CreateEnumStructure(enumName, builder);
    }
}


또 EffectData가 VisualStudio에서 오류를 발생하네요.. 전체 컴파일이라거나 namespace 등의 문제가 아닐까 싶긴합니다만..
관련 변수, 함수들 인텔리전스가 지원되지 않으니 강의하면서 답답하실 듯 하네요.. 미리 확인이 되지 않았던건가 하는 아쉬움은 남네요.
어여 해결이 되어서 뒤의 내용들에서는 수월하게 진행될 수 있기를 바랍니다.




드디어 위에까지 작업을 하면 위와 같이 Tools - Effect Tool 이라는 메뉴가 생성된 것을 볼 수가 있습니다..


 



그리고 클릭을 해보면 와우~ Vertical, Horizontal 함수를 열심히 사용하여 만든 "Effect Tool" 창이 나타나고, 지금까지 작업한대로 버튼들과 리스트컨트롤이 하나 붙어서 보이는 것을 확인할 수 있습니다.

Unity UI 제작을 일일이 Typing으로 하니 소스보기가 좀 복잡하네요 ㅎㅎ. 좀더 복잡한 UI를 가지는 사운드 툴은 확실히 정신이 없을 듯하네요..

Unity UI 제작을 지원하는 Addon 등이 없나 궁금해지네요.

 

궁금해서 검색해보니 Unity UI Addons를 누가 만들어 배포하고 있긴 하네요 ^^. 물론 안정성이나 검증되지 않아 사용하지 않으셨는지는 모르겠습니다. 나중에 시간이 되면 사용해 봐야겠습니다.

 


아직 버그들이 있기에 좀더 수정 작업을 진행하게 되고, 다음 시간에는 데이터 매니저와 이팩트 매니저를 만들어서 전체 EffectTool이 구동되는 것을 확인하고, 바로 이어서 사운드툴 제작으로 들어가겠습니다 ^^~





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

+ Recent posts