Chapter 5. FPS Game

개발하는 운동인·2024년 10월 8일

3D 좌표

  • Y축 - 높이
  • X,Z - 정면

즉, Y축을 회전 시키면 바라보고 있는 방향이 달라질 것이다.

Light 오브젝트

Directional Light 작동 방식


  • X축을 회전 시키면 해가 진 상태로 변경할 수 있다.

PointLight

  • Intensity(강도)를 조절하여 빛의 세기를 설정할 수 있다.

SpotLight

  • PointLight 와 유사하다.

🖌️ 단축키 꿀팁

  • Shift+Ctrl 누른 상태에서 해당 오브젝트를 움직이면 바닥에 붙은 상태에서 움직일 수 있다.

2D 에서는 좌표 1칸을 픽셀 1칸이라고 불렀다면 3D에서는 1M라고 부른다.

원근감을 없애는 방법

  • 다음 빨간색 박스를 누르면 Iso로 변경되어 원근감을 없앨 수 있다.

🖌️ 에셋 다운로드

Unity AssetStore

✅ 유니티 에셋 스토어에서 HDRP,SRP를 지원하는지 안하는지 아래 사진을 통해 알 수 있다.

Itch.io

SRP

  • URP, HDRP로 나뉘어지며 스크립터블 렌더링 파이프 라인을 말한다.

쉐이더가 깨졌을 경우

  • Builtin으로 만들어진 URP를 읽었을 때 Shader을 바꿔야 함
  • Built - in 에서 URP로 변경하는 방법이다.

  • 모두 체크하고 Converters 버튼을 클릭한다.

  • HDRP로 변경할 수 없는 이유는 애초에 프로젝트를 URP로 만들었기 때문이다.

  • 텍스처를 입혀진 것을 볼 수 있다.

레벨 디자인용 배치 툴 (에셋)

  • 유료이다.

⭐ 3D 구현 시작

    1. 기존 캡슐의 이름을 Player 이름으로 짓고, 메인 카메라를 자식으로 넣는다.
    1. Mesh Renderer를 잠시 비활성화 하고, Character Controller 컴포넌트, PlayerInput 컴포넌트를 추가한다.

Character Controller 컴포넌트

  • Min Move Distance: 어느정도 각도까지 올라갈 수 있는 정도를 말함
  • Stem Offset : 어느정도 계단을 올라갈 수 있는가

유니티에서 제공하는 입력 방식

  • 다양한 Action 기능의 키들이 존재한다.
  • Button은 눌렀다 뗐다의 방식을 말함.
  • 추가로, UI용도 존재한다.
    1. PlayerController 스크립트를 생성하고 추가한다.

솔직히 에셋 스토어에서 character controller 검색 후 다운 받으면 이동 구현은 굳이 안해도 된다. 하지만, 실력을 기르기 위해 직접 코딩하는 것이 좋다.

⭐ Mesh Collider 컴포넌트를 오브젝트의 Mesh 모양을 기준으로 추가하는 것이 좋다.

  • Mesh Collider를 부착한 오브젝트는 오브젝트의 Mesh별로 계산을 하기 때문에 과부하가 올 수 있다. 그럴 경우 Convex를 체크하면 된다.

  • 체크할 경우 오목한 곳에는 들어갈 수 없다. 오브젝트의 모양별로 Convex를 체크할지 말지 정하면 될 것 같다.

  • Box의 경우 BoxCollider을 사용하면 된다.

  • Convex를 체크할 경우, 바깥 라인만 콜라이더가 적용되었다.

  • 체크하지 않을 경우, Mesh 그 자체. 즉, Mesh의 모양 별로 콜라이더가 적용되었다.

  • 만약, 아래 처럼 Mesh 가 들어간 부분이 있다면 Mesh Collider의 Convex를 체크하는 것이 좋을 것이다. (그래야 들어간 부분을 플레이어가 들어갈 수 있음)

⭐ Player 이동

    1. PlayerController 스크립트에 코드를 작성한다.
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    public float WalkingSpeed = 7;

    InputAction moveAction; //InputAction 컴포넌트를 말함.

    CharacterController charController;
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked; //마우스 커서가 들어가면, 마우스 커서가 보이지 않는다. (⭐ 3D게임은 대부분 마우스 커서가 보이지 않으므로)
        Cursor.visible = false; //혹시 모르니 커서를 보이지 않게 한다.

        InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.

        moveAction = inputActions.FindAction("Move"); //Move에 해당하는 Action 값을 moveAction에 저장.

        charController = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update()
    {
        Vector2 moveVector = moveAction.ReadValue<Vector2>(); //moveAction의 Vector2를 moveVector에 저장.(x,y값을 가져옴) 
        //하지만 3D는 움직일 때 X,Z로 움직이므로 x,z로 바꿔줘야 함
        Vector3 move = new Vector3(moveVector.x, 0,moveVector.y);

        if(moveVector.magnitude > 1) // 이동벡터. 이동벡터가 1보다 커야 하는 이유
        {
            move.Normalize();
        }

        move = move * WalkingSpeed * Time.deltaTime; //이 코드로 움직일 수는 있지만, 방향 벡터가 필요함. 
        move = transform.TransformDirection(move); //이 벡터를 내가 쳐다보고 있는 방향벡터로 바꿔준다.

        charController.Move(move); //최종적으로 캐릭터 컨트롤러의 Move 함수를 이용해서 움직일 수 있게 함.
        

    }
}

✅ 코드 설명 1 : actions을 가져와야 한다.

- InputActionAsset inputActions = GetComponent< PlayerInput >().actions; 
  • actions는 PlayterInput 컴포넌트에 Defalut Map을 의미한다.
  • Defalut Map은 Player로 설정되어 있다. Defalut Map에 대한 의미는 아래 사진과 같다.

✅ 코드 설명 2 : Move Action 찾기

moveAction = inputActions.FindAction("Move");

✅ 코드 설명 3: 결국 최종적으로 움직이게 하려면 CharacterController 컴포넌트를 이용해야 함

charController = GetComponent<CharacterController>();

✅ 코드 설명 4: Move Action 에 대한 값을 가져오기

Vector2 moveVector = moveAction.ReadValue<Vector2>(); //moveAction의 Vector2를 moveVector에 저장.(x,y값을 가져옴) 

//하지만 3D는 움직일 때 X,Z로 움직이므로 x,z로 바꿔줘야 함
Vector3 move = new Vector3(moveVector.x, 0,moveVector.y);

✅ 코드 설명 5 : 대각선으로 이동할 때 벡터의 크기가 1.1414로 되기 때문에 1이 넘어간다. 따라서, Nomarlize() 하여 벡터의 크기를 1로 고정시켜야 한다.

 if(move.magnitude > 1) // 이동벡터. 이동벡터가 1보다 커야 하는 이유
 {
    move.Normalize();
}
  • 아래의 이유때문에 normalized 하여 1.1414가 아닌 1로 맞춰준다.

  • 코드 설명 6 : move라는 벡터에 거리와 시간을 곱해주고, move 벡터를 방향벡터로 바꾸기 위해 TransformDirection(move)로 해서 내가 쳐다 보고 있는 방향을 방향벡터로 바꿔주고, 최종적으로 CharacterController 컴포넌트에 Move()을 이용해서 이동하게 한다.

    move = move * WalkingSpeed * Time.deltaTime; //이 코드로 움직일 수는 있지만, 방향 벡터가 필요함. 
    move = transform.TransformDirection(move); //이 벡터를 내가 쳐다보고 있는 방향벡터로 바꿔준다.

    charController.Move(move); //최종적으로 캐릭터 컨트롤러의 Move 함수를 이용해서 움직일 수 있게 함.

⭐ Player 마우스 좌,우 이동

✅ 3D에서 상,하 이동과 좌,우 이동이 다르다. 좌,우 이동은 몸이 회전되어야 한다. 상,하 이동은 마우스 커서만 올리거나 내리거나 하면 된다.

    1. 코드를 작성한다.
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    //...생략
    public float mouseSens = 1; //마우스 감도
    float horizontalAngle; //마우스 각도
    InputAction lookAction;

    void Start()
    {
  
  		//...생략
        
        InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.

		//..생략
        
        lookAction = inputActions.FindAction("Look"); //Look에 해당하는 Action 값을 moveAction에 저장.
        horizontalAngle = transform.localEulerAngles.y; //y축을 회전 시켜야 좌,우 회전이 가능하다.
        
    }

    void Update()
    {
        
        

        //좌,우 이동
        Vector2 look = lookAction.ReadValue<Vector2>();

        float turnPlayer = look.x * mouseSens; // 마우스 감도를 적용
        horizontalAngle += turnPlayer; //적용한 것을 현재 각도에 더한다.
        if(turnPlayer >= 360)
        {
            turnPlayer -= 360;
        }
        else if(horizontalAngle < 0)
        {
            turnPlayer += 360;
        }
        //transform.localEulerAngles.y = turnPlayer; //이게 안되므로 아래 3줄 코드를 작성해야함
        Vector3 currentAngle = transform.localEulerAngles; // 현재 플레이어의 
        회전 값(x,y,z)를 currentAngle에 넣는다.
        currentAngle.y = horizontalAngle; //y축 회전 각도를 업데이트
        transform.localEulerAngles = currentAngle; //변경된 회전 각도를 오브젝트에 반영
        //=============================================

    }
}

✅ 코드 설명 1: 마찬가지로, Look에 해당하는 Action 값을 moveAction에 저장. 마우스 상,하 각도를 계산하기 위해 y축을 float 변수인 horizontalAngle 에다 저장한다. 로컬 좌표 계산시 플레이어가 바라보고 있는 방향으로 계산이 된다.

InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.
lookAction = inputActions.FindAction("Look"); //Look에 해당하는 Action 값을 moveAction에 저장.

horizontalAngle = transform.localEulerAngles.y; //로컬 좌표 y축 각도를 회전 시켜야 좌,우 회전이 가능하다. 

✅ 코드 설명 2: Look에 해당하는 Action 의 타입(Vector2)을 읽어와서 look에 저장.

Vector2 look = lookAction.ReadValue<Vector2>();

✅ 코드 설명 3: 마우스 감도를 읽어온 look의 x좌표를 곱해서 float 변수 turnPlayer에 저장한다. 저장한 값을 마우스 각도와 더해준다.

float turnPlayer = look.x * mouseSens; // 마우스 감도를 적용 
horizontalAngle += turnPlayer; //적용한 것을 현재 각도에 더한다.

✅ 코드 설명 4: 마우스 각도가 360도 이상 되거나 0도 이하가 되면 안되므로 조건을 추가한다.

 if(turnPlayer >= 360)
{
    turnPlayer -= 360;
}
else if(horizontalAngle < 0)
{
 	turnPlayer += 360;
}

✅ 아래 코드는 문법 오류가 발생하므로 , 아래 코드를 작성하기 위해 3줄 정도 코드가 필요하다.

 transform.localEulerAngles.y = turnPlayer; //이게 안되므로 아래 3줄 코드를 작성해야함

✅ 코드 설명 5 : 최종 마우스 좌,우 이동을 계산한다.

Vector3 currentAngle = transform.localEulerAngles; //현재 회전 값(x,y,z) 가져오고 currentAngle에 저장
currentAngle.y = horizontalAngle; //y축 회전 각도를 업데이트
transform.localEulerAngles = currentAngle; // 변경된 회전 각도를 오브젝트에 반영

⭐ Player 마우스 상,하 이동

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{

	public float mouseSens = 1; //마우스 감도
    public Transform cameraTransform; //카메라 할당
    float verticalAngle;

	float horizontalAngle;
    
	InputAction lookAction;
    
    void Start()
    {
       
        InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.

        lookAction = inputActions.FindAction("Look"); //Look에 해당하는 Action 값을 moveAction에 저장.
		 horizontalAngle = transform.localEulerAngles.y; //y축을 회전 시켜야 좌,우 회전이 가능하다.
        verticalAngle = 0;
        
    }

    // Update is called once per frame
    void Update()
    {
		Vector2 look = lookAction.ReadValue<Vector2>();
        
       Vector3 currentAngle = transform.localEulerAngles; //변화된 현재 각도를 넣는다.
       currentAngle.y = horizontalAngle;
       transform.localEulerAngles = currentAngle;
       
        //상,하 이동
        float turnCam = look.y * mouseSens;
        verticalAngle -= turnCam;
        verticalAngle = Mathf.Clamp(verticalAngle, -89f, 89f); //verticalAngle에 값이 -89~99사이라면 
        currentAngle = cameraTransform.localEulerAngles;
        currentAngle.x = verticalAngle; //x축 중심으로 회전해야 위 아래 회전 가능
        cameraTransform.localEulerAngles = currentAngle;

        


    }
}

✅ 코드 설명 1: 마우스 상,하 각도에 대한 변수 선언하고, 카메라를 할당하기 위해 Transform 을 선언한다.

public Transform cameraTransform; //카메라 할당
float verticalAngle;

void Start()
{
	  verticalAngle = 0; //0으로 초기화 한다.
}

✅ 코드 설명 2: 카메라의 상하를 회전 시키기 위해 float 변수 turnCame을 선언하고, Look Action의 Y값과 마우스 감도를 곱해준다.

float turnCam = look.y * mouseSens; //Look이라는 Action 특성 상
//y축의 값을 움직여야 상,하 회전이 가능
verticalAngle -= turnCam;

플레이어가 아닌 카메라의 회전 축을 기준으로 할 때는 Y축이 일반적으로 수직 방향을 나타낸다. 또한, Look Action은 마우스를 올리면 올릴 수록 양수의 값을 갖고, 마우스를 내리면 내릴 수록 음수의 값을 가진다. 그러므로 verticalAngle -= turnCam; 해주게 되면 카메라의 x축의 회전값이 마우스를 내릴 때 양수, 올릴 때 음수가 된다. 즉, 반대가 되는 개념이다.

✅ 코드 설명 3: 각도를 최소 -89 , 최대 89도로 제한하였다.

verticalAngle = Mathf.Clamp(verticalAngle, -89f, 89f); //verticalAngle에 값이 -89~99사이라면 

✅ 코드 설명 4: currentAngle(현재 각도)에 카메라의 현재 회전값을 저장한다.

currentAngle = cameraTransform.localEulerAngles;

✅ 코드 설명 5: currentAngle의 x축에다 최종 수직 각도를 저장한다.

 currentAngle.x = verticalAngle; //currentAngle.x에 변경된 회전 각도를 업데이트한다.

✅ 코드 설명 6: 메인카메라의 각도에다 회전한 마우스 각도를 저장한다.

 cameraTransform.localEulerAngles = currentAngle; // 변경된 회전 각도를 오브젝트에 반영

⭐ 실행 시, 플레이어 이동 , 마우스 상,하 회전 , 마우스 좌,우 회전이 정상적으로 작동한다.

⭐ 중력 적용

  • 코드를 수정한다.
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
	 CharacterController charController;
     //중력 부분
     public float gravity = 10; //중력속도
     public float terminalSpeed = 20;// 떨어지는 물체가 초당 20보다 떨어지지않게 할듯
     public float verticalSpeed = 0; //낙하속도
     
     //....생략
     void Update()
     {
        //....생략
     	
        verticalSpeed -= gravity * Time.deltaTime; //가속도 * 시간 = 속도의 변화

        if(verticalSpeed < -terminalSpeed) //2. 떨어지는 속도를 제한(verticalSpeed 가 -20보다 작다면)
        {
            verticalSpeed = terminalSpeed; //verticalSpeed을 20(terminalSpeed)으로 설정
        }

        Vector3 verticalMove = new Vector3(0, verticalSpeed, 0); //verticalSpeed을 통해 떨어지는 벡터 생성
        verticalMove *= Time.deltaTime; //벡터에 시간단위로 계산하게 적용

       CollisionFlags flag = charController.Move(verticalMove); //4. 캐릭터 중력속도 적용.CollisionFlags : 움직였는데 , 어디에서 부딪혔는지 검출 할 수 있음  
       //Move함수를 통해 움직였을 때 CollisionFlags로 통해 어떤 콜라이더와 부딪혔는지 알 수 있음.
        //비트연산을 함
        //땅을 밟고 있지 않을 때 None , 땅을 밟고 있을 때 Bleow

	
        if((flag & CollisionFlags.Below) != 0) //flag 중에 CollisionFlags.Below가 있는지,떨어지는 상태가 아니라면
        {
            verticalSpeed = 0; //떨어지는 속도를 0으로
        }
    }

Debug.Log(flag); 을 해보면 Below와 None이 여러번 호출되고 있다. 즉, 땅에 붙어있는지 붙어있지 않는지 확인하기 어렵다.

  • 실행 모습

Debug.Log(charController.isGrounded); 을 하면 땅과 붙어있는지 , 붙어있지 않는지에 대한 여부를 True False로 확인할 수 있다.

  • 실행 모습
  • 하지만 여전히 정확히 판별하지 못하고 있다.

⭐ charController.isGrounded을 통해 땅에 붙었는지, 안 붙었는지 여부에 대한 판별

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
   CharacterController charController;
   bool isGrounded = true; //땅 충돌 여부
   float groundedTimer; //땅에 붙어있는 시간
   
   void Update()
   {
   		  if(!charController.isGrounded) //땅에 안 붙었다면
 		  {
    		    if(isGrounded) //그런데도 땅에 붙었다면?
    	  		{
          				groundedTimer += Time.deltaTime; //안 붙어있다고 주장한 시간이 0.5초를 넘억ㅆ음
         				
                        if (groundedTimer > 0.5f) 
         				 {
           					   isGrounded = false; //진짜로 땅에 붙어있지 않구나
         				 }
     			 }
	     }
 		 else //땅에 붙어있다면
 		 {
    		  isGrounded = true; //땅에 붙어있다
     		 groundedTimer = 0;

 		 }
         Debug.Log(isGrounded);
  }

Debug.Log(isGrounded); 통해 판별 가능해졌다.

  • 실행 시

⭐ 플레이어 점프

  • PlayerInput 컴포넌트에 Jump키를 통해 OnJump 이벤트를 호출 할 수 있다.

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    //...생략

    //중력 부분
    public float gravity = 10; //중력속도
    public float terminalSpeed = 20;// 떨어지는 물체가 초당 20보다 떨어지지않게 할듯
    public float verticalSpeed = 0; //낙하속도


    //땅 여부
    bool isGrounded = true; //땅 충돌 여부
    float groundedTimer; //땅에 붙어있는 시간

    public float JumpSpeed = 10;
    void Start()
    {
      	//...생략
        charController = GetComponent<CharacterController>();
      
        
    }

    // Update is called once per frame
    void Update()
    {
     	//...생략

        verticalSpeed -= gravity * Time.deltaTime; //가속도 * 시간 = 속도의 변화

        if(verticalSpeed < -terminalSpeed)
        {
            verticalSpeed = terminalSpeed;
        }

        Vector3 verticalMove = new Vector3(0, verticalSpeed, 0);
        verticalMove *= Time.deltaTime;

       CollisionFlags flag = charController.Move(verticalMove); //CollisionFlags : 움직였는데 , 어디에서 부딪혔는지 검출 할 수 있음
                                                                //비트연산을 함, Move함수를 통해 움직였을 때 CollisionFlags로 통해 어떤 콜라이더와 부딪혔는지 알 수 있음.
        if ((flag & (CollisionFlags.Below | CollisionFlags.Above) != 0) //땅에 떨어졌다면 속도를 0으로
        {
            verticalSpeed = 0;
        }
       
        Debug.Log(charController.isGrounded); //땅에 부딪혔는지 물어볼 수 있음.

        if (!charController.isGrounded) //땅에 안 붙었다면
        {
            if(isGrounded) //그런데도 땅에 붙었다면?
            {
                groundedTimer += Time.deltaTime; //안 붙어있다고 주장한 시간이 0.5초를 넘억ㅆ음
                if (groundedTimer > 0.5f) 
                {
                    isGrounded = false; //진짜로 땅에 붙어있지 않구나
                }
            }
        }
        else //땅에 붙어있다면
        {
            isGrounded = true; //땅에 붙어있다
            groundedTimer = 0;

        }
        //Debug.Log(isGrounded);
    }

    void OnJump() //⭐ 점프 액션을 통해 함수 호출
    {
        if(isGrounded)
        {
            verticalSpeed = JumpSpeed; //verticalSpeed가 Update문에서 1초 간격으로 줄어들고 있지만, 점프 키를 누르면 JumpSpeed로 설정. -> 그럼 다시 Update문에 의해 verticalSpeed가 1초 간격으로 줄어듦.
            isGrounded = false; //점프 했기 때문에 땅에 붙어 있지 않음
        }
    }
}
  • 실행 시, 땅에 붙어있다면 True, 점프로인해 붙어있지 않다면 False 처리가 되어 정상적으로 작동한다.

⭐ 플레이어 총 위치 및 애니메이션 설정

  • 다운로드 받았던 에셋(총)을 메인카메라의 자식으로 넣는다. 그리고, 45ACP와 45ACP_Shell을 비활성화한다.
  • 단, 자식으로 넣을 때 총의 뼈대가 분리되어있는 소스를 넣는다.
  • 총의 위치를 조절해서 게임뷰에 보여질 수 있게 설정한다.
  • 프리펩화 한다.
  • 탄창이 게임뷰에 보이기 때문에 프리펩에서 탄창의 위치를 조절한다.
  • Animator 컴포넌트를 추가하고, 새로운 컨트롤러를 생성해서 추가한다.
  • Idle 클립을 생성한다. Idle 클립은 아무런 애니메이션을 적용하지 않을 것이다.
  • Fire 클립을 생성한다.
  • Fire 클립의 녹화버튼을 누른 뒤 발사하는 애니메이션을 만들어본다.
  • Reload 클립을 생성하고 장전하는 애니메이션을 만들어본다.

  • 폴더정리

중요한 점은 키 프레임 사이의 보간이 작용된다. 0에서 1까지 값을 추정하여 결과값을 도출하는 것을 맗마.

⭐ 플레이어 총 애니메이션 설정

  • Has Exit Time : 1번 애니메이션을 Play하던중 다른 애니메이션을 틀 일이 없다 라는 말이다. 반대로 말하면 촬영한 시간 동안 다른 애니메이션은 틀 수 없다 라는 말이 된다.

✅ Has Exit Time을 체크하면, Exit Time 비율만큼 애니메이션 재생 후 다음 상태로 전이된다 . 체크 해제 하면 전이 조건을 만족해야만 즉시 전이가 이루어진다. 전이 조건이 만족될 때 즉각 재생되게 하고 싶으면 이게 체크 해제되어야 한다.

  • Exit Time : 다른 애니메이션으로 전환 할 때 걸리는 시간
  • Transition Duration(s): 애니메이션을 전환할 때 걸리는 시간을 의미
  • Fixed Duration : Fixed Duration이 체크되어 있다면 Exit Time 값이 “시간”으로 해석된다. Exit Time 값이 0.7 이고 Has Exit TIme이 체크되어 있다면 절대적인 “0.7초”의 시간 후에 다음 전이가 일어난다. Fixed Duration이 체크되어 있지 않다면 Exit Time 값이 “% 비율”로 해석된다. Exit Time 값이 0.7 이고 Has Exit TIme이 체크되어 있다면 “해당 애니메이션의 전체 재생 길이의 70 %”까지만 재생한 후에 다음 전이가 일어난다.

⭐ Idle <-> Fire 상태로 전환

✅ Idle -> Fire

  • Idle -> Fire 전환 할 때 Has Exit Time 체크 해제한다. (Idle 애니메이션을 Play 하던 중 다른 애니메이션을 틀어야 하므로). 또한, Transition Duration을 0으로 해서 애니메이션을 전환할 때 걸리는 시간을 0으로 설정한다. 즉, 조건을 통해 조건이 만족된다면 곧 바로 연결된 애니메이션을 실행한다.
  • 조건으로 Fire Trigger로 세팅한다.

✅ Fire -> Idle

  • Has Exit Time을 체크하고, 조건은 따로 설정하지 않는다. 즉, 아래 사진과 같이 Transition Duration 1로 되어 있으므로, 정해진 Fire 애니메이션 시간(1초)을 반드시 다 재생시키고, Idle 애니메이션으로 진행한다.

장전 애니메이션도 똑같이 적용한다.

⭐ 플레이어 총 구현 코드 작성

    1. Gun 오브젝트에 GunWeapon 스크립트를 생성하고 추가한다.
    1. GunWeapon 스크립트에 코드를 작성한다. 총이 발사되는 기능과 장전하는 기능이 담긴 함수 틀을 만든다.
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
    //플레이어가 총을 쏘고, 장전해야 하므로 public으로 총쏘는 함수와 장전하는 함수를 만든다.
    public void FireWeapon() 
    {
        Debug.Log("Fire !");
    }
    public void ReloadWeapon()
    {

    }
}
    1. 결국 총 발사, 총 장전은 플레이어이가 하는 것이므로, 플레이어 컨트롤러에서 GunWeapon 스크립트를 접근해서 함수를 호출해야 한다.
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
  	//...생략
    
    //총 쏘기
    InputAction fireAction;
    public GunWeapon gunWeapon;
    void Start()
    {
       	//...생략
        
        InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.
		
        //...생략

        fireAction = inputActions.FindAction("Fire");
    }

    void Update()
    {
     	//...생략

        //총 발사
        if(fireAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 Fire Acition이 있었나요? (즉 , 키가 눌렸나요?)
        {
            gunWeapon.FireWeapon(); //FireWeapon() 호출
        }
    }

	//...생략
}
  • Fire에 의미는 아래 InputAction에 담겨져있다. 원래는 Attack이였지만 Rename하여 Fire으로 변경하였다.

    1. 플레이어 컨트롤러에서 public으로 했던 것들을 할당한다.
  • 실행 시, 마우스 좌클릭(Fire 키)을 통해 로그가 정상적으로 출력된다.

틀을 짰으니, 발사 및 장전 애니메이션을 실행 시켜보자.

    1. GunWeapon 스크립트에 코드를 작성한다.
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
    Animator animator;

    private void Start()
    {
        animator = GetComponent<Animator>();
    }
    public void FireWeapon() 
    {
        Debug.Log(animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim")); //Idle이라는 애니메이션의 상태를 알 수 있다. (Idle 상태일 때만 총을 발사하고 싶다면 사용)

        if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim")) //현재 GunIdle_Anim 애니메이션이 재생중이라면. 
        {
            animator.SetTrigger("Fire");
        }
    }
    public void ReloadWeapon()
    {
        if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim")) //현재 GunIdle_Anim 애니메이션이 재생중이라면. 
        {
            animator.SetTrigger("Reload");
        }
    }
}
    1. PlayerController에 스크립트에 ReloadWeapon() 을 호출하는 부분을 작성한다.
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
  	//...생략
    
    //총 쏘기
    InputAction fireAction;
    public GunWeapon gunWeapon;
    
     InputAction reloadAction;
    void Start()
    {
       	//...생략
        
        InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.
		
        //...생략

        fireAction = inputActions.FindAction("Fire");
        reloadAction = inputActions.FindAction("Reload");
    }

    void Update()
    {
     	//...생략

        //총 발사
        if(fireAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 Fire Acition이 있었나요? (즉 , 키가 눌렸나요?)
        {
            gunWeapon.FireWeapon(); //FireWeapon() 호출
        }
        if(reloadAction.WasCompletedThisFrame()) 
        {
            gunWeapon.ReloadWeapon(); //FireWeapon() 호출
        }
    }

	//...생략
}

⭐ 하지만 Reload Action이 Input Action에 없으므로 아래 순서에 맞게 추가해준다.

  • +버튼을 누르고 Reload라는 이름으로 짓는다.
  • No Binding 버튼을 누르고 Path을 누른다.(입력해야 하는 키를 찾아야 한다)
  • By Location of Key를 누른다.
  • R 키를 선택한다.
  • 그 결과 PlayerInput 컴포넌트에 OnReload 이벤트가 추가 되었다.

OnReload 이벤트를 통해 호출 할 수 있지만, 편의상 Update문에서 WasCompletedThisFrame()을 이용하였다.

  • 실행 시, 정상적으로 애니메이션이 재생된다.

⭐ 크로스 헤어 준비 및 설정

    1. 소스를 준비한다.
    1. cross hair을 아래와 같이 설정한다. 텍스처 타입을 Sprite로 Sprite Mode을 Single로 변경한다.
    1. 이미지를 만들고 소스를 집어 넣는다.

  • 실행 시, 정상적으로 크로스헤어가 나온다.

⭐ 총알 궤적 구현

    1. Gun의 자식으로 빈 객체를 만든다. 이름은 FirePos.(FirePos : 총알이 나가는 것처럼 보이는 오브젝트 )

  • Gun의 자식으로 Line을 추가한다.

    1. 프리펩화 시키고, 이름을 BulletLine으로 짓는다.
  • ⭐ 이때, BulletLine의 위치를 0,0,0으로 세팅해야 한다.
    1. GunWeapon 스크립트에 총알 궤적을 그리는 코드를 작성한다.
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
    Animator animator;
    public GameObject BulletLine; //총알 선(궤적)
    public Transform FirePos;
    private void Start()
    {
        animator = GetComponent<Animator>();
    }
    public void FireWeapon() 
    {
        Debug.Log(animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim")); //Idle이라는 애니메이션의 상태를 알 수 있다. (Idle 상태일 때만 총을 발사하고 싶다면 사용)

        if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
        {
            animator.SetTrigger("Fire");
            RayCastFire();
        }

    }
    public void ReloadWeapon()
    {
        if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
        {
            animator.SetTrigger("Reload");
        }
    }

    void RayCastFire()
    {
        Camera cam = Camera.main;

        RaycastHit hit; //빛을 발사한 결과를 hit에 저장
        Ray r = cam.ViewportPointToRay(Vector3.one / 2); //카메라 정 가운데를 쏘는 빛을 r에 저장한다.(1/2이므로 0.5 크기임)

        Vector3 hitPos = r.origin + r.direction * 200; //카메라 방향으로부터 200m 떨어진 지점을 Vector의 hitPos에 저장

        if(Physics.Raycast(r, out hit, 1000)) //빛이 부딪혔다면 true
        {
            hitPos = hit.point; //빛에 충돌한 지점을 hitPos에 저장.
        }

        GameObject GO = Instantiate(BulletLine);
        Vector3[] pos = new Vector3[]
        {
            FirePos.position,hitPos
        };
        GO.GetComponent<LineRenderer>().SetPositions(pos); //꼭짓점 좌표를 넣어줌.
    }
}
    1. GunWeaon 스크립트에 public으로 선언한 것들을 할당 시킨다.
  • 실행 시, 정상적으로 작동한다.

✅ 코드 리뷰 1: 총알 궤적을 그리기 위해 오브젝트 선언

public class GunWeapon : MonoBehaviour
{
    Animator animator;
    public GameObject BulletLine; //총알 선(궤적)
    public Transform FirePos;
}
  • 프리펩으로 만들었던 총알 선(BulletLine), 총알 초기 위치(FirePos) 을 선언해야 한다.

코드 리뷰 2: 총이 오른쪽에 있어도, 총알 궤적은 메인카메라 중앙에 있어야 한다. 따라서, MainCamera의 정보를 받아야한다.

 void RayCastFire()
 {
     Camera cam = Camera.main;
 }

✅ 코드 리뷰 3: 빛을 발사한 결과를 RaycastHit에 저장, 빛 자체를 Ray에 저장

  void RayCastFire()
  {
        RaycastHit hit; //빛을 발사한 결과를 hit에 저장
        Ray r = cam.ViewportPointToRay(Vector3.one / 2); //카메라 정 가운데를 쏘는 빛을 r에 저장한다.(1/2이므로 0.5 크기임)
   }
   - Camera 컴포넌트에 ViewportPointToRay()을 이용한다. 
   - ViewportPointToRay(Vector3.one / 2)을 함으로써, 카메라 정중앙에 쏘게끔 한다.

코드 리뷰 4: 총알 궤적이 콜라이더 컴포넌트가 없는 오브젝트에 부딪혔을 때를 생각해야 한다.

 void RayCastFire()
 {
        Vector3 hitPos = r.origin + r.direction * 200; //카메라 방향으로부터 200m 떨어진 지점을 Vector의 hitPos에 저장
 }
   - 빛을 발사한 시작 위치 + 빛을 발사했던 방향 * 200 을 하는 것이다.

✅ 코드 리뷰 5: 총알 궤적에 충돌한 지점을 저장해야 한다. (콜라이더 컴포넌트가 있는 오브젝트에 부딪혔을 때)

 void RayCastFire()
 {
	    Vector3 hitPos = r.origin + r.direction * 200; //카메라 방향으로부터 200m 떨어진 지점을 Vector의 hitPos에 저장
        if(Physics.Raycast(r, out hit, 1000)) //빛(r)에 충돌했다면 그 정보를 out 매개변수 hit에 저장. -> if문의 조건이 true가 된다
        {
            hitPos = hit.point; //빛에 충돌한 지점을 hitPos에 저장.
        }
 }

코드 리뷰 6: 총알 궤적 프리펩을 생성하고, 총알 궤적(라인의 시작 부분과 라인의 끝 부분(충돌체 지점))을 벡터 배열로 저장한다.

  void RayCastFire()
  {
        if(Physics.Raycast(r, out hit, 1000)) //빛이 부딪혔다면 true
        {
            hitPos = hit.point; //빛에 충돌한 지점을 hitPos에 저장.
        }
        GameObject GO = Instantiate(BulletLine); //총알 궤적 생성
        Vector3[] pos = new Vector3[]
        {
            FirePos.position,hitPos //FirePos.position : 라인의 시작 부분을 의미, hitPos는 충돌 지점을 의미한다.
        }
  }
  • 벡터 배열로 저장한 이유는 선이 여러개가 될 수 있으므로 벡터 배열로 저장한다.

✅ 코드 리뷰 7: LineRenderer 컴포넌트의 SetPositions()을 통해 총알 궤적의 최종 라인의 좌표를 저장한다.

void RayCastFire()
{
	 GO.GetComponent<LineRenderer>().SetPositions(pos); //꼭짓점 좌표를 넣어줌.
}
  • SetPositions()는 꼭짓점(x,y,z) 좌표를 설정하는 함수이다.

⭐ 총알 궤적을 보여주고, 총알 궤적 사라지게 하기. (코루틴)

    1. 다음 빨간색 박스의 코드를 작성한다.
  • Invoke()을 사용하면 함수를 호출할 때 인자를 넘길 수 없다. 그래서, 코루틴을 사용해서 인자를 넘겨준다.
  • 코루틴은 싱글쓰레드이고, StartCoroutine()을 해서 호출할 수 있다. 그 밑의 바로 코드를 작성해도, 호출 할 수 있다. (병렬처리 되므로)

⭐ 총알이 박힌 지점에서 이펙트

  • 파티클 시스템을 이용할 것이므로 이펙트를 만들어준다.

  • GunWeapon 스크립트에 코드를 작성한다.

 using System.Collections;
using Unity.VisualScripting;
using UnityEngine;
public class GunWeapon : MonoBehaviour
{
    //...생략
  
    //파티클
    public GameObject particlePrefab;

    //...생략

    void RayCastFire()
    {
        //...생략

        if(Physics.Raycast(r, out hit, 1000)) //빛이 부딪혔다면 true
        {
            hitPos = hit.point; //빛에 충돌한 지점을 hitPos에 저장.

            GameObject particle = Instantiate(particlePrefab);
            particle.transform.position = hitPos;
            particle.transform.forward = hit.normal; //파티클의 정면 방향은 수직
        }

   		  //...생략
    }
  
    IEnumerator DestroyTrail(GameObject go)
    {
        yield return new WaitForSeconds(0.1f);
        Destroy(go);
    }
} 

⭐ 수류탼 던지기

    1. 에셋을 준비한다.
  • 하지만 셰이더가 빠진 것을 볼 수 있다.

    1. Render Pipeline Converter창을 열고 모두 체크 한 뒤 Converter을 진행한다.
    1. 그 결과 셰이더가 입혀진 것을 볼 수 있다.
    1. 플레이어의 메인카메라 자식으로 둔다.
    1. 위치 조절과 사이즈 조절을 한다.

    1. MK2에 ProjectileWeapon 스크립트를 생성하고 추가한다.
    1. ProjectileWeapon 스크립트는 기존 GunWeapon 스크립트를 상속받게 한다.
    1. 코드 작성한다.
public class ProjectileWeapon : GunWeapon
{

    public GameObject projecttilePrefab; //수류탄
 
    public float projecttileAngle = 30; // 수류탄 던질 때 포물선
    public float projecttileForce = 30; //속도
    public float projecttileTime = 5; //폭파시간
}
  • 필요한 변수 선언을 한다

    1. GunWeapon 스크립트를 수정
public class GunWeapon : MonoBehaviour
{
    public void FireWeapon() 
    {
         if(animator != null)
		 {
   			  if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
     		  {
        			animator.SetTrigger("Fire");
         			Fire();
     		   }
		 }
		 else
		 {
    		 Fire();
		 }
	}
  
   protected virtual void Fire() //protected을 하여 자식 클래스가 사용할 수 있게하고, virtual로 하여 구현할지 말지 자식 클래스가 정하도록 한다.
   {
       RayCastFire(); //총알 궤적을 그리는 함수 호출
   }
}
    1. ProjectileWeapon 스크립트에 부모 클래스의 virtual로 된 함수를 정의하고, 자식클래스만 가지고 있는 멤버 함수 ProjecttileFire()를 호출한다.
using UnityEngine;

public class ProjectileWeapon : GunWeapon
{

    public GameObject projecttilePrefab; //수류탄
 
    public float projecttileAngle = 30; // 수류탄 던질 때 포물선
    public float projecttileForce = 30; //속도
    public float projecttileTime = 5; //폭파시간

    protected override void Fire()
    {
        ProjecttileFire(); //ProjectileFire함수 호출

    }

    void ProjecttileFire()
    {

    }
}
    1. 기존 MK2 오브젝트를 복사 한 뒤 이름은 Granade로 짓는다. 그리고, Rigidbody와 CapsuleCollider 컴포넌트를 추가한다.
    1. ProjectileWeapon 스크립트에 코드를 작성한다.
using UnityEngine;

public class ProjectileWeapon : GunWeapon
{

    public GameObject projecttilePrefab; //수류탄
 
    public float projecttileAngle = 30; // 수류탄 던질 때 포물선
    public float projecttileForce = 30; //속도
    public float projecttileTime = 5; //폭파시간

    protected override void Fire()
    {
        ProjecttileFire(); //ProjecttileFire함수 호출

    }

    void ProjecttileFire()
    {
        Camera cam = Camera.main; //카메라 정보를 담는다

        Vector3 forward = cam.transform.forward; //카메라의 정면 방향을 벡터로 저장
        Vector3 up = cam.transform.up; // 카메라의 위쪽 방향을 벡터로 저장

        Vector3 direction = forward + up * Mathf.Tan(projecttileAngle * Mathf.Deg2Rad);

        direction.Normalize();
        direction *= projecttileForce;

        GameObject go = Instantiate(projecttilePrefab); //수류탄 생성
        go.transform.position = FirePos.position; //수류탄의 위치는 수류탄의 자식 중 FiringPos의 위치로 
        go.GetComponent<Rigidbody>().AddForce(direction,ForceMode.Impulse); //수류탄을 AddForce함수를 이용해서 direction방향으로 이동.
    }
}
    1. public으로 만든 변수들을 할당한다.
  • 부모 클래스에서 만든 변수는 할당하지 않는다.

  • 또한, 중요한 점은 플레이어 컨트롤러에서 Weapon을 GranageLancher로 변경해야 던질 수 있다.

  • 실행 시, 정상적으로 작동한다.

⭐ 수류탄 터트리기

    1. Granade 프리펩 창에서 자식으로 Sphere 오브젝트를 추가한다.

    1. Granade 프리펩에 Rigidbody,Animator 컴포넌트를 추가하고, Bomb 스크립트를 생성해서 추가한다.
    1. 1번에서 추가했던 Sphere 오브젝트에 콜라이더 컴포넌트를 추가하고, 자리 차지하지 않기 위해 IsTrigger을 활성화한다.
    1. GranadeExplode라는 애니메이션 클립을 만들고, Sphere 오브젝트의 크기를 늘려서 터진다는 것을 시각화 한다.
  • 또한 애니메이터 창에서 Explode 라는 Trigger를 만들고, Idle(빈 애니메이션)에서 Explode(터지는 애니메이션)으로 전이 될때 조건을 Explode로 추가한다.
    1. Bomb 스크립트를 작성한다.
using UnityEngine;

public class Bomb : MonoBehaviour
{
    public float time; //인스펙터창에서 5초로 설정함

    private void Update()
    {
        time -= Time.deltaTime;

        if(time < 0)
        {
            GetComponent<Animator>().SetTrigger("Explode"); //터지는 애니메이션 실행
            Destroy(gameObject, 1);
        }
    }
}
  • 위 코드는 Granade (수류탄)이 생성 될 때 5초 뒤에 터지는 애니메이션이 실행 된 후 삭제되는 코드이다.
  • 실행 시, 정상적으로 작동한다.

⭐ 무기 교체(수류탄,권총)

    1. Change Weapon이라는 InputAction을 추가하고, 마우스 Middle 버튼으로 설정한다.
  • 이로써 OnChangeWeapon 이벤트를 호출할 수 있을 것이다.
    1. 플레이어 컨트롤러에서 코드를 수정하고 추가한다.
using NUnit.Framework;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
   	//..생략

 	//public GunWeapon gunWeapon; // 이 코드는 없애고 아래 List로 관리한다.
  
    public List<GunWeapon> weapons;
    int currentWeaponIndex;
    void Start()
    {
    	//..생략
    }

    // Update is called once per frame
    void Update()
    {
   		//...생략

        //총 발사
        if(fireAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 눌렸나요?
        {
            //gunWeapon.FireWeapon();
            weapons[currentWeaponIndex].FireWeapon(); 
        }
        if (reloadAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 눌렸나요?
        {
            //gunWeapon.ReloadWeapon();
            weapons[currentWeaponIndex].ReloadWeapon();
        }
    }

    public void OnChangeWeapon()
    {
        weapons[currentWeaponIndex].gameObject.SetActive(false); //현재 무기를 비활성화
        currentWeaponIndex++; //무기 번호 증가
        if(currentWeaponIndex > weapons.Count - 1) //무기 번호가 무기 개수보다 많을 경우
        {
            currentWeaponIndex = 0; //무기 번호를 0으로 만듦
        }

        weapons[currentWeaponIndex].gameObject.SetActive(true); //교체된 무기 활성화
    }
}
    1. 인스펙터창에서 public으로 했던 부분을 할당한다.
    • 실행시, 정상적으로 작동한다.

⭐ 총알 갯수

    1. TMP UI를 만든다.
    1. 무기들을 관리하는 GunWeapon에 코드를 추가한다.
using System.Collections;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
    //..생략

    public TMP_Text bulletText; //텍스트
  
    public int currentBullet = 8; //현재 총알 개수
    public int totalBullet = 32; //탄창 개수
    public int maxBullet = 8; //총알 최대 개수
	
   private void Update()
   {
     bulletText.text = currentBullet.ToString() + "/" + totalBullet.ToString();
   }

  
	//..생략
    
}
    1. 무기마다 변수 값을 다르게 하고, 텍스트를 할당한다.
  • 권총 부분
  • 수류탄 부분
  • 실행 시, 교체할 때 텍스트가 바뀔 것이다.

    1. 이제, 총을 쏘거나 수류탄을 던질 때마다 개수 및 탄창을 텍스트 형태로 줄여야 한다. 그러기 위해 GunWeapon 스크립트를 수정한다.
using System.Collections;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
	//...생략

    public TMP_Text bulletText;
    public int currentBullet = 8;
    public int totalBullet = 32;
    public int maxBullet = 8;
	
  	//...생략
  
    private void Update()
    {
        bulletText.text = currentBullet.ToString() + "/" + totalBullet.ToString();
    }
    public void FireWeapon() 
    {
        if(currentBullet > 0 ) //남은 탄약이 0 보다 커야함
        {
            if (animator != null)
            {
                if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
                {
                    currentBullet--; //탄약 감소
                    animator.SetTrigger("Fire");
                    Fire();
                }
            }
            else
            {
                currentBullet--; //탄약 감소
                Fire();
            }
        }
       
    }
    public void ReloadWeapon()
    {
        if(totalBullet > 0) //남은 탄창이 0보다 커야함
        {
            if (animator != null)
            {
                if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
                {
                    animator.SetTrigger("Reload");
                    Reload();
                }
            }
            else
            {
                Reload();
            }
        }
       
       
    }


    void Reload() // ⭐ 복습 중요
    {
        if(totalBullet >= maxBullet - currentBullet) // 32 >= 8- 현재 남은 탄피 의 조건을 만족하면 장전됨
        {
            totalBullet -= maxBullet - currentBullet; // 32 = 32 - 8 - 현재 남은 탄피
            currentBullet = maxBullet; //탄피를 최대 탄피로 설정. 즉, 5발 남았을 때 장전하면 8발로 세팅됨
        }
        else
        {
            currentBullet += totalBullet;
            totalBullet = 0;
        }
    }
  
	//..생략
    
}
  • 실행 시 , 정상적으로 작동한다.

⭐ 적 설정

    1. 적을 하이라키창에 올려놓는다.
  • 이 소스는 Rigging이 된 것을 볼 수 있고,
  • Animation Type이 Humanoid이므로, 다른 리깅된 애니메이션의 소스를 이용할 수 있다.
    1. Idle 애니메이션을 다운로드해서, 적용해본다.
  • 애니메이션 컨트롤러를 만들어주고 할당한다.
    1. 애니메이터창에다 다운로드 받은 애니메이션을 드래그하여 실행 시켜본다.

⭐ 단, Avatar을 할당 시켜야 재생된다. 그 이유는 애유니티의 휴머노이드 모델은 리타게팅, 머슬 정의 와 같은 특수한 기능을 가지는데요, 이런 특수한 기능은 휴머노이드 캐릭터를 공통 내부 포맷(common internal format)으로 매핑시키는 Unity의 Avatar 시스템이 있기 때문에 가능한것 이다.

  • 실행 시, 애니메이션이 재생된다.

    1. PunchRight 애니메이션을 복사하고, 다른 폴더로 옮긴다.
  • 이렇게 하게 되면 기존 PuchRight 애니메이션 클립을 제어할 수 없었지만, 복사를 통해 제어할 수 있게 되었다.

  • 폴더로 옮겨진 애니메이션 모습

    1. 애니메이터 창에 드래그한다.
    1. Idle <-> PuhchRight의 전이 부분에서 Has Exit Time을 체크하고, Exit Time을 1로 맞춘다.
    1. 추가로, Run애니메이션도 추가한다.
    1. Enemy에 Rigidbody,CapsuleCollider 컴포넌트를 추가한다.
    1. 빈 객체를 만들고, 이름은 NavyMeshSurface로 짓는다. 그리고, NavyMeshSurface 컴포넌트를 추가한다.
    • 그리고, Bake 버튼을 누른다.그 결과 하늘색으로 변경된 부분이 있다. 이 부분은 적이 움직일 수 있는 발판과 같은 개념이다.
    • 또한, 레이어 별로 Bake를 해서 원하는 오브젝트만 이동할 수 있게 할 수 있다.
    • 그리고, Agent 세팅에서도 값을 조절하여 Bake 할 수 있다.

    1. NavyMeshAgent 컴포넌트를 Enemy에 추가한다.
    • NavyMeshAgent 컴포넌트는 NavyMeshSurface 위로 돌아다닐 수 있다.
    1. isKinematic은 활성화한다. 그 이유는 NavyMeshAgent 컴포넌트가 Rigidbody를 컨트롤 할 것이기 때문이다. 추가로, EnemyController 스크립트를 생성하고 추가한다.

⭐ 적 추격

    1. 코드를 작성한다.
using UnityEngine;
using UnityEngine.AI;

public class EnemyController : MonoBehaviour
{
    NavMeshAgent agent;
    GameObject player;
  
    void Start()
    {
        player = GameObject.FindWithTag("Player"); //태그로 플레이어 찾기
        agent = GetComponent<NavMeshAgent>();
    }

    // Update is called once per frame
    void Update()
    {
        agent.destination = player.transform.position; //적의 목적지는 플레이어 위치야. -> 추격
    }
}

⭐ 적에게 발자취를 남겨서 적이 플레이어를 추격

    1. 코드를 작성한다.
using UnityEngine;
using UnityEngine.AI;

public class EnemyController : MonoBehaviour
{
    NavMeshAgent agent;
    GameObject player;
    void Start()
    {
        player = GameObject.FindWithTag("Player");
        agent = GetComponent<NavMeshAgent>();
  		agent.destination = player.transform.position;
    }

    void Update()
    {   
        if(agent.remainingDistance < 1.0f) //목적지 지점까지 1m보다 작을 경우.즉, 목적지와 가까워질 수록
        {
            agent.destination = player.transform.position; //적의 목적지는 플레이어 위치야. -> 추격
        }
    }
}
  • 실행 시 , 정상적으로 작동한다.

⭐ 적 공격,추격 상태를 FSM으로 관리

    1. 애니메이터창에서 Idle,Run,Attack 애니메이션을 전이 한다.

  • Idle -> Run

  • Idle -> Attack

    1. 코드를 작성한다.
using UnityEngine;
using UnityEngine.AI;
using static Health;

public class EnemyController : MonoBehaviour, Health.IHealthListner
{
    enum State
    {
        Idle,
        Follow,
        Attack,
    }
    State state;
    float currentStateTime;
    public float timeForNextState = 2; //2초간 가만히 머문다.

    NavMeshAgent agent;
    GameObject player;
    Animator animator;
  
    void Start()
    {
        player = GameObject.FindWithTag("Player");
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();

        state = State.Idle;
        currentStateTime = timeForNextState;
    }

    void Update()
    {       
        switch(state)//FSM
        {
            case State.Idle:
                currentStateTime -= Time.deltaTime;
                if(currentStateTime < 0)
                {
                    float distance = (player.transform.position - transform.position).magnitude; //방향벡터의 크기를 구한다. 
              
                    if(distance < 1.5f)
                    {
                        StartAttack();
                    }
                    else
                    {
                        StartFollow();
                    }
                }
                break;
            case State.Follow:
                if(agent.remainingDistance < 1.0f || !agent.hasPath) //목적지 1M이내이거나
                                                                     //갈 수 있는 경로가 없을 때
                {
                    StartIdle();
                }
                break;

            case State.Attack:
                currentStateTime -= timeForNextState;
                if(currentStateTime < 0)
                {
                    StartIdle();
                }
                break;
        }
    }

    //가만히 있는 상태에서ㅏ 가까울 경우 attack, 멀어질 경우 follow
    void StartAttack()
    {
        state = State.Attack;
        currentStateTime = timeForNextState;
        animator.SetTrigger("Attack");
    }

    void StartFollow()
    {
        state = State.Follow;
        agent.destination = player.transform.position;
        agent.isStopped = false;
        animator.SetTrigger("Run");
    }



    void StartIdle()
    {
        state = State.Idle;
        currentStateTime = timeForNextState;
        agent.isStopped = true;
        animator.SetTrigger("Idle");
    }

}
  • 추가로, public으로 선언한 변수에 값을 2로 설정한다.

⭐ 적에게 총 쏘는 것, 그리고 수류탄을 던져서 적 HP 깎기

    1. Health 스크립트를 만들어서 Enemy에 추가한다.
    1. Health 스크립트를 작성한다.
using UnityEngine;

public class Health : MonoBehaviour
{
    public float invincibleTime;
    float lastDamageTime;


    public float hp = 10;
    public float maxHp = 10;

     IHealthListner healthListner;

    private void Start()
    {
        healthListner = GetComponent<IHealthListner>();
    }
    public void Damage(float damage)
    {
        if (hp > 0 && lastDamageTime + invincibleTime < Time.time)
        {
            hp -= damage;

            lastDamageTime = Time.time;

            if (hp <= 0)
            {
 
                if(healthListner != null)
                {
                    healthListner.OnDie();
                }
            }
            else
            {
                // ekcla
                Debug.Log("다침!");
            }
        }
    }
    public interface IHealthListner
    {
        void OnDie();



    }


}
  • 주목해야 될 부분은 인터페이스이다. 인터페이스를 상속 받을 시, 인터페이스에 들어간 함수를 자식 클래스가 구현해야 한다.

    1. 추가로, healthListner가 있을 때 호출 되는 OnDie() 메서드를 EnemyController 스크립트에 작성한다.
using UnityEngine;
using UnityEngine.AI;
using static Health;

public class EnemyController : MonoBehaviour, Health.IHealthListner
{
    public void OnDie()
    {
        state = State.Die;
        agent.isStopped = true;
        animator.SetTrigger("Die");
        Invoke("DestroyThis", 2.0f);
    }
    void DestroyThis()
    {
        Destroy(gameObject);
    }
    enum State
    {
        Idle,
        Follow,
        Attack,
        Die
    }
    State state;
    float currentStateTime;
    public float timeForNextState = 2; //2초간 가만히 머문다.

    NavMeshAgent agent;
    GameObject player;
    Animator animator;
    void Start()
    {
        player = GameObject.FindWithTag("Player");
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();

        state = State.Idle;
        currentStateTime = timeForNextState;
    }

	//...생략

  
}
    1. 애니메이션창에서 AnyState로 죽는 애니메이션을 재생한다. Die라는 트리거를 Condition한다.
  • 그리고, 총 쏘는 코드가 담겨져 있는 GunWeapon 스크립트에서 Health 스크립트에 Damage()를 호출해야 한다.

using System.Collections;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
   	//...생략
  
    void RayCastFire()
    {
        //...생략
        if(Physics.Raycast(r, out hit, 1000)) //빛이 부딪혔다면 true
        {
            hitPos = hit.point; //빛에 충돌한 지점을 hitPos에 저장.

            GameObject particle = Instantiate(particlePrefab);
            particle.transform.position = hitPos;
            particle.transform.right = hit.normal; //파티클의 정면 방향은 수직


            if(hit.collider.tag == "Enemy") //Enemy 태그와 충돌 시
            {
                hit.collider.GetComponent<Health>().Damage(2.0f); //호출한다.
            }
        }

       	//...생략
    }
 	//...생략
    
}
    1. 또한, Granade 프리펩에 들어간 Bomb 스크립트에서 Health 스크립트에 Damage()를 호출해야 한다.
using UnityEngine;

public class Bomb : MonoBehaviour
{
    public float time;
    public float damage;


    private void Update()
    {
        time -= Time.deltaTime;

        if(time < 0)
        {
            GetComponent<Animator>().SetTrigger("Explode");
            Destroy(gameObject, 1);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Enemy") //Enemy 태그와 충돌 시 
        {
            other.GetComponent<Health>().Damage(damage); //호출한다.
        }
    }
}
  • 실행 시, 정상적으로 작동한다.

⭐ 적이 플레이어에게 공격했을 때만 콜라이더 켜기

    1. Enemy의 rig된 부모 객체에서 자식 객체 중 왼쪽 손에 빈 객체를 하나 만든다. 이름은 PunchCollider로 짓는다.
    1. 콜라이더 크기 설정을 해준다.

    1. 첫 실행 부분에서 콜라이더를 끄고, 중간 부분에서 콜라이더를 킨다. 중간 부분은 펀치를 날리는 모션이다.

    1. 플레이어에게도 Helath 스크립트 부착.
    1. Health 스크립트를 플레이어가 갖고 있기 때문에 EnemyConTroller 스크립트에서 Player에게 Damage 주는 함수를 호출할 수 있다.
using UnityEngine;
using UnityEngine.AI;
using static Health;

public class EnemyController : MonoBehaviour, Health.IHealthListner
{
    //...생략

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            other.GetComponent<Health>().Damage(1); //호출한다. 
        }
    }
   public void OnDie()
   {
     state = State.Die;
     agent.isStopped = true;
     animator.SetTrigger("Die");
     Invoke("DestroyThis", 2.0f);
   }
  void DestroyThis()
  {
     Destroy(gameObject);
  }
}
  • 실행 시, 정상적으로 작동한다.

⭐ 플레이어 체력 UI 설정

    1. Canvas에 Image를 만들고, HPBackGround라는 이름으로 짓는다.
    1. 자식으로 이미지를 또 만든다.
    1. Image의 Image 컴포넌트에서 아래와 같이 설정한다.

  • 현재 체력 UI 모습

    1. 플레이어가 들고 있는 Health 스크립트에서 코드를 추가한다.
using UnityEngine;
using UnityEngine.UI;

public class Health : MonoBehaviour
{
    public float invincibleTime;
    float lastDamageTime;


    public float hp = 10;
    public float maxHp = 10;

     IHealthListner healthListner;


    //체력 HP UI
    public Image hpGauge;
    private void Start()
    {
        healthListner = GetComponent<IHealthListner>();
    }
    public void Damage(float damage)
    {
        if (hp > 0 && lastDamageTime + invincibleTime < Time.time)
        {
            hp -= damage;

            //체력 HP UI
            if (hpGauge != null)
            {
                hpGauge.fillAmount = hp / maxHp; //이미지 컴포넌트의 fillAmount 속성을 이용한다. 계산식은 현재 hp를 maxHP로 나누는 방식이다. 
            }
            lastDamageTime = Time.time;

         	//..생략
        }
    }
    public interface IHealthListner
    {
        void OnDie();
    }


}
    1. public으로 한 hpGuage를 할당한다.
    1. GaugeColor 스크립트를 생성 하고 이미지에 추가한다.
    1. GaugeColor 스크립트에 HP_Image에 fillAmout 값에 맞춰 색깔을 변경하는 코드를 작성한다.
using UnityEngine;
using UnityEngine.UI;

public class GaugeColor : MonoBehaviour
{
    Image image;
    private void Start()
    {
        image = GetComponent<Image>();
    }
    void Update()
    {      
        GetComponent<Image>().color = Color.HSVToRGB(image.fillAmount / 3, 1.0f, 1.0f);
    }
}
  • 실행 시, 정상적으로 작동한다.

⭐ 적 체력 UI 설정

    1. 적의 자식으로 Canvas을 만들고, Canvas 컴포넌트의 Render Mode에서 WorldSpace로 변경한다. 그 이유는 3D UI를 만들기 위함이다.
    1. 플레이어 체력 UI 만드는 방식과 똑같이 적 체력 이미지와 게이지를 만든다.
    1. HPGauge도 똑같다.
  • 현재 적 체력 UI 사진이다.

하지만, 적이 돌아보고 있을 때 체력 UI도 같이 회전하고 있다. 이렇게 하는 것이 아닌 카메라와 평행이 되게 해야한다.

    1. HPGauge에 LookCamera 스크립트를 생성하고, Enemy에 추가한다.
    1. LookCamera 스크립트를 작성한다.
using UnityEngine;

public class LookCamera : MonoBehaviour
{

    void Update()
    {
        transform.LookAt(transform.position + Camera.main.transform.forward);
    }
}
  • 실행 시, 플레이어가 바라보는 화면, 즉, 메인카메라와 체력 ui가 평행이 되었다.

⭐ 플레이어 죽음 UI 설정

    1. 플레이어에 Animator 컴포넌트를 추가하고, Animator Controller 을 추가한다.
    1. Idle(빈 애니메이션) -> Die 구조로 만든다.

      - Has Exit Time은 체크하고, Conditions을 Die 라는 이름으로 짓는다. 타입은 Trigger이다.
    1. Die 애니메이션은 Loop Time 체크 해제하고, Animator에 Apply Loot Motion 은 체크한다.

    1. 플레이어에 Canvas을 자식으로 추가하고, Canvas의 자식으로 이미지를 추가한다.

      - Canvas의 SortOrder는 1로 설정한다.
    1. Image의 투명도를 0으로 낮춘다.
    1. Die 애니메이션을 녹화 버튼을 눌러 플레이어가 쓰러지면서 이미지가 어두워지게 페이드 인 하는 애니메이션을 만든다.
  • 실행 시, 아래와 같다.

    1. 빈 객체를 만들고, GameManager라는 이름으로 스크립트를 만든다.
    1. GameManager 스크립트를 작성한다. (싱글톤 구현)
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    private static GameManager instance = null;

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

    private void Awake()
    {
        instance = this;
    }

    public bool isPlaying;

    private void Start()
    {
        isPlaying = true;
    }

    public void PlayerDie()
    {
        isPlaying = false;
        Cursor.visible = true;
        Cursor.lockState = CursorLockMode.None;
    }
}  
    1. PlayerController 스크립트에서 GameManager 스크립트의 PlayerDie()을 호출한다. 호출 할 때는 PlayerController 스크립트의 OnDie 함수에서 호출하도록 한다. OnDie함수는 플레이어의 체력이 0 미만일 때 호출된다.
using NUnit.Framework;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour,Health.IHealthListner
{
    //...생략

    // Update is called once per frame
    void Update()
    {
        if(!GameManager.Instance.isPlaying) //isPlaying 변수가 false일 때는 return하여 플레이어의 입력을 방지 한다.
        {
            return;
        }
		//...생략
  	}

    public void OnDie()
    {
        GetComponent<Animator>().SetTrigger("Die");
        GameManager.Instance.PlayerDie();
    }
}
  • 실행 시, 애니메이션이 정상적으로 작동되고, 죽었을 때 입력도 하지 못한다.

⭐ 플레이어가 죽었을 때 UI 나타나게 하기

    1. 부모가 없는 캔버스를 만들고, 타이틀 제목 그리고 버튼 2개를 생성한다.
    1. 캔버스의 Sort Order을 2로 맞춘다. (2로 맞추는 이유는 1로 맞췄었던 플레이어의 페이드 인 ui가 있기 때문에 검은 화면 위에 ui가 나타나게 해야 된다.)
    1. GameManager에서 코드를 추가한다.
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    //..생략
	  public GameObject GameOverCanvas;
  
    public void PlayerDie()
    {
        isPlaying = false;
        Cursor.visible = true; //마우스 보이게
        Cursor.lockState = CursorLockMode.None; //마우스 락 걸었던거 풀기 
        GameOverCanvas.SetActive(true); //GameOverCanvas을 활성화
    }

  
    public void AgainPressed()
    {
        SceneManager.LoadScene("GameScene"); //GameScene 게임씬 이동
    }

    public void QuitPressed()
    {
        Application.Quit(); //종료
    }
}
    1. 각 버튼의 OnClick에서 이벤트를 추가한다.

    1. 그리고 비활성화 상태로 시작한다.
    1. 캔버스를 할당한다.
  • 실행 시, 정상적으로 작동한다.

기타 설정(Enemy 배치)

    1. 기존 하이라키창에 있는 Enemy를 프리펩화 한다.
    1. Enemy를 배치한다.

⭐ 게임 클리어( 플레이어가 죽었을 때 텍스트 변경, 적을 모두 죽였을 때 텍스트 변경)

    1. GameManager 스크립트에서 코드를 수정한다.
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
   //..생략
  
    public GameObject GameOverCanvas; //게임오버 ui
  
    public int enemyNumber = 3; //남아있는 적 
    public TMP_Text title; // 타이틀 텍스트
  
    public bool isPlaying; //플레이어가 게임을 진행중인지 여부

    private void Start()
    {
        isPlaying = true; //게임 진행 중
    }

    public void PlayerDie()
    {
        isPlaying = false; //게임 진행 x 
        Cursor.visible = true; //마우스 보이게
        Cursor.lockState = CursorLockMode.None; //마우스 Lock 걸었던 것을 다시 원래대로
        GameOverCanvas.SetActive(true); //게임오버 ui활성화
        title.text = "You DIed"; //You DIed로 텍스트 변경
    }

    void GameEnd()
    {
        isPlaying = false; //게임 진행 x
        Cursor.visible = true; //마우스 보이게
        Cursor.lockState = CursorLockMode.Confined; //
        GameOverCanvas.SetActive(true);
    }

    public void EnemyDie() //Enemy가 죽을때마다 이 메서드 호출
    {
        enemyNumber--; //감소
        if (enemyNumber <= 0) //적이 0보다 작을경우
        {
            title.text = "They DIed"; //They Died로 텍스트 변경
            GameEnd();
        }
    }
    public void AgainPressed()
    {
        SceneManager.LoadScene("GameScene");
    }

    public void QuitPressed()
    {
        Application.Quit();
    }
}
    1. Enemy에서 EnemyDie 호출부분 작성
using UnityEngine;
using UnityEngine.AI;
using static Health;

public class EnemyController : MonoBehaviour, Health.IHealthListner
{
    public void OnDie()
    {
        state = State.Die;
        agent.isStopped = true;
        animator.SetTrigger("Die");
        Invoke("DestroyThis", 2.0f);
    }
    void DestroyThis()
    {
        GameManager.Instance.EnemyDie(); //호출
        Destroy(gameObject);
    }
	//..생략
}
    1. 인스펙터 할당
  • 실행시, 플레이어가 죽었을 때의 모습과 적을 모두 물리쳤을 때 ui가 달라진 것을 볼 수 있다.

⭐ 사운드

    1. Enemy에 AudioSource 컴포넌트를 추가하고, 발소리 사운드를 넣는다.
  • Loop 체크와 Play On Awake 체크 해제한다.
    1. EnemyController 스크립트에서 코드를 추가한다.
using UnityEngine;
using UnityEngine.AI;
using static Health;

public class EnemyController : MonoBehaviour, Health.IHealthListner
{
	  //..생략

    //오디오
    AudioSource audio;
    void Start()
    {
        //..생략
        //오디오
        audio = GetComponent<AudioSource>();

        //..생략
    }

 		 //..생략
 


    void StartFollow()
    {
        audio.Play();
        //..생략
    }



    void StartIdle()
    {
        audio.Stop();
        //..생략
    }

}
    1. 총기에도 AudioSource 컴포넌트를 추가한다.
  • 따로 소스는 넣지 않는다.

    1. GunWeapon 스크립트에서 코드를 추가한다.
using System.Collections;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;

public class GunWeapon : MonoBehaviour
{
    //..생략
  
    public AudioClip gunShotSound;
  
	//..생략
    void RayCastFire()
    {
        //..생략

        GetComponent<AudioSource>().PlayOneShot(gunShotSound);

    
    }
    protected virtual void Fire()
	{
      	RayCastFire();
	}
	//..생략
    
}
 public void FireWeapon() 
 {
     if(currentBullet > 0 )
     {
         if (animator != null)
         {
             if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
             {
                 currentBullet--;
                 animator.SetTrigger("Fire");
                 Fire();
             }
         }
         else
         {
             currentBullet--;
             Fire();
         }
     }
    
     //RayCastFire();
 }
  • 총을 쐈을 때 GetComponent().PlayOneShot(gunShotSound); 하여 오디오를 재생 시킨다.

    1. 수류탄도 마찬가지로 AudioSource 컴포넌트를 추가한다.
    1. Bomb 스크립트에 코드를 추가한다.
using UnityEngine;

public class Bomb : MonoBehaviour
{
 	 //..생략

    public AudioClip fireInTheHole;
  	
  	//..생략

    public void PlaySound()
    {
        GetComponent<AudioSource>().PlayOneShot(fireInTheHole);
    }
	
  //..생략
}
    1. 수류탄 터지는 애니메이션에 이벤트를 추가하여 PlaySound를 이벤트형식으로 호출한다.
    1. Player와 Enemy 모두 Health 스크립트를 갖고 있으므로, Health 스크립트에서 오디오를 재생 시킬 수 있도록한다.
using UnityEngine;
using UnityEngine.UI;

public class Health : MonoBehaviour
{
 	 //..생략

    public AudioClip dieSound;
    public AudioClip hurtSound;

  	 //..생략


	  //..생략
    public void Damage(float damage)
    {
        if (hp > 0 && lastDamageTime + invincibleTime < Time.time)
        {
            hp -= damage;

            //체력 HP UI
            if (hpGauge != null)
            {
                hpGauge.fillAmount = hp / maxHp;
            }
            lastDamageTime = Time.time;

            if (hp <= 0)
            {
                if(dieSound != null)
                {
                    GetComponent<AudioSource>().PlayOneShot(dieSound);
                }
                if(healthListner != null)
                {
                    healthListner.OnDie();
                }
            }
            else
            {
                if (hurtSound != null)
                {
                    GetComponent<AudioSource>().PlayOneShot(hurtSound);
                }
                Debug.Log("다침!");
            }
        }
    }
	//..생략


}
    1. 인스펙터 할당한다.

⭐ 📄 꿀팁 기능 1. TMP 한국어로 변경하기

    1. Window - TMP - Font Asset Creator
    1. TMP 전용 한글 소스와, 다운로드 받은 폰트를 넣는다.(Charcters from File로 변경)
  • Generate Font Asset 버튼을 누른다.
    1. 폰트 에셋이 만들어졌다.
    1. TMP에 적용시키면, 안되던 한글이 정상적으로 작동한다.

⭐ 📄 꿀팁 기능 2. Newtonsoft Json

JsonUtility 유니티에서 제공하는 jSON 유틸리니. 단점은 딕셔너리 직렬화가 안됨. 즉, 데이터를 덩어리로 만들어주는 것이 안됨. -> 안되는 것을 되게 해주는 Newtonsoft Json을 사용한다.

    1. Install package by name을 누르고, com.unity.nuget.newtonsoft-json 을 복사하면 Newtonsoft Json을 Install한다.
    1. 코드를 작성한다
using System.Collections.Generic;
using UnityEngine;
using Unity.Plastic.Newtonsoft.Json; //Newtonsoft.Json 라이브러리를 사용하여 JSON 관련 작업을 처리합니다.

public class JsonTest : MonoBehaviour
{

    Dictionary<string, string> dict = new Dictionary<string, string>();

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        dict.Add("티모", "나쁜놈");
        dict.Add("요네", "개사기");

        string json1 = JsonConvert.SerializeObject(dict); //직렬화

        Debug.Log(json1); //직렬화 한 것 출력

        Dictionary<string, string> dict2 = JsonConvert.DeserializeObject<Dictionary<string, string>>(json1);
		//JSON 문자열을 다시 Dictionary 객체로 변환합니다. 이 과정을 **역직렬화(Deserialization)**라고 한다.
        Debug.Log(dict2["요네"]); //dict2에서 "요네"라는 키에 해당하는 값을 콘솔에 출력합니다. 결과는 **"개사기"**이다.
    }
}
    1. 출력 시, json1 부분에서 각 키와 키에 들어간 값이 출력되었다. -> 이 과정은 딕셔너리에 데이터를 추가하여 JSON 형식으로 직렬화 한 것이다.
    • 아래 코드를 보면 json1을 JsonConvert.DeserializeObject< T >(json1을)을 해서 역 직렬화를 하였다. 역 직렬화는 JSON 형식의 문자열을 다시 원래의 객체로 변환하는 작업이다. 즉, Dictionary와 동일한 구조와 데이터를 가진다.
 Dictionary<string, string> dict2 = JsonConvert.DeserializeObject<Dictionary<string, string>>(json1);
  • dict2는 역직렬화 된 원래의 딕셔너리 방식이다. 따라서 요네 라는 키를 이용하여 요네에 들어있는 "개사기"가 출력되는 것이다.
Debug.Log(dict2["요네"]);

⭐ Newtonsoft Json 직렬화 역직렬화 예제

⭐ 직렬화

using Newtonsoft.Json;
using UnityEngine;

public class Data
{
    public string Name;
    public float Height;
    private string secret;

    public Data(string name, float height, string secret) //생성자
    {
        Name = name;
        Height = height;
        this.secret = secret;
    }

    public override string ToString()
    {
        return Name + " " + Height + " " + secret;
    }
}


public class JsonTest : MonoBehaviour
{
    private void Start()
    {
        Data charles = new Data("철수", 198, "발바닥 점 둘"); //Data 클래스의 인스턴스화 (변수는 charles ) -> Data 생성자 호출
        string json1 = JsonConvert.SerializeObject(charles);  //JsonConvert.SerializeObject(charles): charles 객체를 Json 문자열로 직렬화
        Debug.Log(json1);
    }
}
  • 출력 시, 문자열이 출력 된다.

중요한 점은 public으로 선언되지 않은 secret 변수는 Json에 포함되지 않는다. 즉, private 변수는 Json에 포함되지 않음

⭐ 역 직렬화

using Newtonsoft.Json;
using UnityEngine;

public class Data
{
    public string Name;
    public float Height;
    private string secret;

    public Data(string name, float height, string secret)
    {
        Name = name;
        Height = height;
        this.secret = secret;
    }

    public override string ToString()
    {
        return Name + " " + Height + " " + secret;
    }
}


public class JsonTest : MonoBehaviour
{
    private void Start()
    {
        Data charles = new Data("철수", 198, "발바닥 점 둘");
        string json1 = JsonConvert.SerializeObject(charles);
        Debug.Log(json1);

        Data aMan = JsonConvert.DeserializeObject<Data>(json1); ;
        Debug.Log(aMan);
    }
}
  • aMan 출력 시, 철수 198이 출력 된다. 그 이유는 직렬화 할 때 secret 변수는 Json에 포함되지 않았으므로, 포함된 Name과 Height만 출력 된다.

private도 Json을 이용한 직렬화 할 때 포함 시키는 방법

using Newtonsoft.Json;
using UnityEngine;

public class Data
{
    //..생략
    [JsonProperty] //⭐ 
    private string secret;

	//..생략
}
  • 출력 시, secret 변수도 포함되었다.

📄 꿀팁 기능 3. Newtonsoft Json을 이용한 Save & Load

⭐ Save

public class Data
{
    public string Name;
    public float Height;
    [JsonProperty]
    private string secret;

    public Data(string name, float height, string secret)
    {
        Name = name;
        Height = height;
        this.secret = secret;
    }

    public override string ToString()
    {
        return Name + " " + Height + " " + secret;
    }
}
public class JsonTest : MonoBehaviour
{
    private void Start()
    {
        Data charles = new Data("철수", 198, "발바닥 점 둘");
        string json1 = JsonConvert.SerializeObject(charles);
        Debug.Log(json1);

        Data aMan = JsonConvert.DeserializeObject<Data>(json1); ;
        Debug.Log (aMan);

        Save(aMan, "save.txt"); //역 직렬화 된 것과 문자열을 Save함수로 넘김
    }

    void Save(Data data, string filename)
    {
        string path = Path.Combine(Application.persistentDataPath, filename); // persistentDataPath 세이브 파일이 저장 될 위치
        Debug.Log(path);
    }
}
  • 출력 시, 아래 경로로 save.txt파일에 저장되었다.

    - 경로를 따라가면

  • save.text 파일에 역직렬화 된 내용들이 담겨져있다.

⭐ 저장했을 때 혹시 모를 버그에 대비하기.

  void Save(Data data, string filename)
    {
		 string path = Path.Combine(Application.persistentDataPath, filename); // persistentDataPath 세이브 파일이 저장 될 위치
		 Debug.Log(path);

		 try
		 {
 		    string json = JsonConvert.SerializeObject(data); //data를 다시 직렬화 하여 문자열로 저장.
		     File.WriteAllText(path, json); //지정된 경로에 있는 파일에 직렬화된 JSON 문자열을 쓰기 작업을 합니다.
		 }//이 작업은 파일이 없으면 새로 파일을 생성하고, 파일이 이미 있으면 그 내용을 덮어씁니다.
		 catch (Exception e)
		 {
    		 Debug.Log(e.ToString()); //파일 쓰기 작업은 다양한 예외(권한 문제, 경로 문제 등)를 발생시킬 수 있기 때문에 안전하게 처리하기 위해 catch문으로 ㅈ바고, e.ToString하여 버그 나타나면, 문자열로 변환시킬 수 있게끔
  
		 }
    }

⭐ 불러오기

using Newtonsoft.Json;
using System;
using System.IO;
using UnityEngine;

public class Data
{
    public string Name;
    public float Height;
    [JsonProperty]
    private string secret;

    public Data(string name, float height, string secret)
    {
        Name = name;
        Height = height;
        this.secret = secret;
    }

    public override string ToString()
    {
        return Name + " " + Height + " " + secret;
    }
}
public class JsonTest : MonoBehaviour
{
    private void Start()
    {
        Data charles = new Data("철수", 198, "발바닥 점 둘");
        string json1 = JsonConvert.SerializeObject(charles);
        Debug.Log(json1);

        Data aMan = JsonConvert.DeserializeObject<Data>(json1); ;
        Debug.Log(aMan);

        Save(aMan, "save.txt");

        Data secondMan = Load("save.txt"); //save.txt 라는 문자열을 넘김 <- 이게 파일 이름이 될 것이고, 이 파일의 경로를 찾게 될것
        Debug.Log(secondMan); //
    }

    void Save(Data data, string filename)
    {
      //..생략
    }
    Data Load(string filename)
    {
        string path = Path.Combine(Application.persistentDataPath, filename); //filename의 경로를 생성하여 path에 저장

        try
        {
            if (File.Exists(path)) //해당 경로에 파일이 존재한다면,
            {
                string json = File.ReadAllText(path); //파일 내용을 텍스트로 읽어오고 json 변수에 저장.
                return JsonConvert.DeserializeObject<Data>(json); //파일 내용이 담겨진 json을 역 직렬화하여 반환
            }
            else
            {
                return null;
            }
        }
        catch (Exception e)
        {
            Debug.LogError(e.ToString());
            return null;
        }
    }
}
  • 출력 시, 파일에 들어있는 내용이 출력된다.

⭐ OnCollider 이벤트와, OnTrigger 이벤트 호출 조건

📄 몰랐던 내용

✅ 1. 유니티에서 지원하는 입력 방식에 접근하기

- InputActionAsset inputActions = GetComponent< PlayerInput >().actions; 
  • actions는 PlayterInput 컴포넌트에 Defalut Map을 의미한다.
  • Defalut Map은 Player로 설정되어 있다. Defalut Map에 대한 의미는 아래 사진과 같다.

✅ 2. 입력 방식에 접근하고 내가 원하는 입력 방식 찾기

  • 코드 설명 2 : Move Action 찾기
InputAction moveAction; //InputAction 컴포넌트를 말함.
void Start()
{

moveAction = inputActions.FindAction("Move");
}

✅ 3. 내가 원하는 입력 방식에 대한 값 가져오기

  • 코드 설명 4: Move Action 에 대한 값을 가져오기
Vector2 moveVector = moveAction.ReadValue<Vector2>(); //moveAction의 Vector2를 moveVector에 저장.(x,y값을 가져옴) 

//하지만 3D는 움직일 때 X,Z로 움직이므로 x,z로 바꿔줘야 함
Vector3 move = new Vector3(moveVector.x, 0,moveVector.y);

✅ 4. 대각선의 방향으로 이동할 때 방향 벡터의 크기는 1이 넘어간다. Normalize() 을 이용해야 한다.

if(move.magnitude > 1) // 이동벡터. 이동벡터가 1보다 커야 하는 이유
 {
    move.Normalize();
}

✅ 5. 내가 쳐다보고 있는 방향으로 벡터 계산하기 transform.,TransformDirection(벡터)을 이용

 move = transform.TransformDirection(move); //이 벡터를 내가 쳐다보고 있는 방향벡터로 바꿔준다.

✅ 6. 좌,우로 마우스 회전하기 위해서는 Y축을 회전 시켜야 한다는 점. localEulerAngles을 할 경우 로컬좌표로 계산(즉, 플레이어 기준으로 계산)

  float horizontalAngle; //마우스 각도
  void Start()
  {
   		horizontalAngle = transform.localEulerAngles.y; //y축을 회전 시켜야 좌,우 회전이 가능하다.
  }

✅ 7. 상,하로 마우스 회전하기 위해서는 카메라 객체를 이용한다는 점

public Transform cameraTransform; //카메라 할당

✅ 8. 마우스를 위쪽으로 올리면 Look 입력의 y값은 양수가 되고, 아래쪽으로 내리면 Look 입력의 y값은 음수가 됨. 그러므로 verticalAngle -= turnCam; 해주게 되면 카메라의 x축의 회전값이 마우스를 내릴 때 양수, 올릴 때 음수가 된다. 즉, 반대가 되는 개념이다.

float turnCam = look.y * mouseSens;
verticalAngle -= turnCam;

✅ 9. 마우스 회전 각도 제한 하기위해 Mathf.Clamp 사용 (최솟값과 최대값을 정해주는 함수)

verticalAngle = Mathf.Clamp(verticalAngle, -89f, 89f); //verticalAngle에 값이 -89~99사이라면 

✅ 10. look.x가 좌,우 회전을 담당하고, look.y가 상,하 회전을 담당하는 이유

  • Look이라는 Action 특성 상 Vector2를 갖고 있다. 즉, Vector2의 특성 상 x,y 좌표밖에 없으며 x좌표를 움직이면 좌,우 회전이 가능한 것이었고, y좌표를 움직이면 상,하 회전이 가능한 것이였다.

강사님께 드린 질문 내용

🤔 Vector3 moveVector = moveAction.ReadValue< Vector3 >(); 을 사용해도 무방한지?

  • Move는 애초에 Control Type이 Vector2로 되어 있다.
  • 또한, WASD 키의 CompositeType 2D Vector로 이루어져있으므로 , Vector3 moveVector = moveAction.ReadValue< Vector3 >(); 에 대한 코드는 성립될 수 없다. 즉, ReadValue을 통해 읽을 수 없다는 이야기다.

✅ 11. InputAction 클래스를 통해 WasCompletedThisFrame() 을 이용하는 것. 주로, Update()에서 사용되며, 기능은 현재 프레임에 액션이 있었는지 여부에 대한 bool 타입이다.

 InputAction fireAction;
 void Start()
 {
 		 fireAction = inputActions.FindAction("Fire");
 }
 void Update()
 {    
	 if(fireAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 눌렸나요?
	 {
   		  gunWeapon.FireWeapon();
	 }
 }
  

✅ 12. LineRenderer 컴포넌트에 SetPositions(pos)에 의미

    GameObject GO = Instantiate(BulletLine);
    Vector3[] pos = new Vector3[]
    {
        FirePos.position,hitPos
    };
    GO.GetComponent<LineRenderer>().SetPositions(pos);

  • 즉, 라인 렌더러가 시작점과 끝점을 지정된 위치로 연결하는 선을 그리는 함수이다.

✅ if ((flag & (CollisionFlags.Below | CollisionFlags.Above))!= 0) 가 왜 땅에 떨어졌다는 조건일까?

  • 먼저, CollisionFlags는 CharacterController가 다른 콜라이더와 충돌했을 때 발생하는 정보를 비트 플래그로 저장한다.
  • CollisionFlags 플래그는 각각 고유한 비트 값을 가지고 있다.

⭐ CollisionFlags.Below | CollisionFlags.Above 을 계산 해보자.

  • CollisionFlags.Below = 001
  • CollisionFlags.Above = 010
  • 결과 : 011이다. 2진수로 하면 3이다. 즉, 아래와 위 충돌을 모두 포함하는 비트 플래그가 된다. OR 연산자(|)을 사용하면 여러 상태를 결합할 수 있다.

⭐ flag & (CollisionFlags.Below | CollisionFlags.Above) 을 계산 해보자.

  • 위 식을 flag & 011 로 생각 할 수 있다. 011는 위에서 계산 과정을 거쳤다.
  • 만약 flag가 001이나 011 이라면, 011과 비트 연산( & )을 하게 되면 0이 결과로 나오지 않아 위 조건은 true가 된다.
  • 하지만, flag가 000이라면 000 & 011 이므로, 0이 결과로 나오게 되어 위 조건은 false가 된다.

⭐ 즉, if ((flag & (CollisionFlags.Below | CollisionFlags.Above)) != 0) 는 조건식에서 비트 연산을 한 뒤 결과값이 0인지 아닌지를 작성한 것이다.

  • 0인지 아닌지 확인하는 이유는, 0이면 어떤 충돌도 발생하지 않았다는 의미이기 때문이다. 즉, != 0 조건을 통해 실제로 아래나 위에서 충돌이 발생했을 때만 코드를 실행하도록 한다.
  • ✅ 예제 1 : CollisionFlags 설명
verticalSpeed -= gravity * Time.deltaTime; //가속도 * 시간 = 속도의 변화
  
if(verticalSpeed < -terminalSpeed)
{
 	 verticalSpeed = terminalSpeed;
}

Vector3 verticalMove = new Vector3(0, verticalSpeed, 0);
verticalMove *= Time.deltaTime;
CollisionFlags flag = charController.Move(verticalMove); //CollisionFlags : 움직였는데 , 어디에서 부딪혔는지 검출 할 수 있음
                                                        //비트연산을 함, Move함수를 통해 움직였을 때 CollisionFlags로 통해 어떤 콜라이더와 부딪혔는지 알 수 있음.
if ((flag & (CollisionFlags.Below | CollisionFlags.Above))!= 0) //땅에 떨어졌다면 속도를 0으로
{
    verticalSpeed = 0;
}
    1. Move함수를 통해 verticalMove 방향으로 움직이게 되는데, 움직였을 때 어느지점(밑,위,옆)에 부딪혔는지 CollisionFlags을 통해 flag에 저장한다.
    1. 만약 flag & 밑 부분 또는 천장 부분 을 비트 단위로 계산해서 0이 아니라면, 즉, 땅이나 천장에 부딪혔다면, verticalSpeed을 0으로 설정한다.

✅ 13. Input Actio 키 추가 방법

  • +버튼을 누르고 Reload라는 이름으로 짓는다.
  • No Binding 버튼을 누르고 Path을 누른다.(입력해야 하는 키를 찾아야 한다)
  • By Location of Key를 누른다.
  • R 키를 선택한다.
  • 그 결과 PlayerInput 컴포넌트에 OnReload 이벤트가 추가 되었다.

OnReload 이벤트를 통해 호출 할 수 있지만, 편의상 Update문에서 WasCompletedThisFrame()을 이용하였다.

✅ 14. 파티클(이펙트)의 방향을 충돌지점의 수직 방향으로 맞추기

  • why? 총알이 충돌된 지점에서 이펙트가 재생될 때 위로 튀는 것보다 아래로 튀는 것이 좋다. 즉, 수직으로 맞춰야 한다.
  • How?
		public GameObject particlePrefab;

        if(Physics.Raycast(r, out hit, 1000)) //빛이 부딪혔다면 true
        {
            hitPos = hit.point; //빛에 충돌한 지점을 hitPos에 저장.

            GameObject particle = Instantiate(particlePrefab);
            particle.transform.position = hitPos;
            particle.transform.forward = hit.normal; //파티클의 정면 방향은 수직
        }
  • hit.normal을 할 경우, 충돌 지점을 기준으로 수직으로 나오는 벡터를 가져온다. 이 벡터를 파티클의 정면 방향 벡터에다 저장한다.
  • 만약, particle.transform.right = hit.normal; 일 경우 수직으로 나오는 벡터를 오른쪽 방향에다 저장하게 된다.
  • 위 사진처럼 오른쪽 방향으로 이펙트가 재생되고 있으므로, 3d에서 정면 방향인 z축. 즉, particle.transform.forward 을 사용해야 한다.

✅ 15. 현재 재생되고 있는 애니메이션 클립에 대한 코드

  • when? 가만히 있는 상태에서만 총을 발사하는 애니메이션을 재생하고 싶을 때
  • How?
  if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
   {
        animator.SetTrigger("Fire");
        Fire();
   }
  • animator.GetCurrentAnimatorStateInfo(0).IsName("애니메이션 클립 이름") 을 이용한다.

✅ 16. 수류탄 포물선

  • 포물선이란? 수류탄을 던질 때, 일정한 각도로 발사하면 중력의 영향으로 포물선 궤적을 그리며 날아갑니다. 이 코드는 수류탄이 그리는 포물선을 결정하는 요소들을 설정하고, 물리 엔진을 통해 그 궤적을 구현합니다.
  • why? 수류탄은 투사체이긴 하지만 포물선이 존재해야 수류탄 답다. 따라서, 포물선을 구현해야 한다.
  • when? 내가 던진 투사체가 포물선의 기능이 필요할 때
  • How
    public float projecttileAngle = 30; // 수류탄 던질 때 포물선
    public float projecttileForce = 30; //속도
    public float projecttileTime = 5; //폭파시간
                                  
    void ProjecttileFire()
    {
        Camera cam = Camera.main;

        Vector3 forward = cam.transform.forward;
        Vector3 up = cam.transform.up;

        Vector3 direction = forward + up * Mathf.Tan(projecttileAngle * Mathf.Deg2Rad);

        direction.Normalize();
        direction *= projecttileForce;

        GameObject go = Instantiate(projecttilePrefab);
        go.transform.position = FirePos.position;
        go.GetComponent<Rigidbody>().AddForce(direction,ForceMode.Impulse);
        go.GetComponent<Bomb>().time = projecttileTime;
    }
    1. projecttileAngle은 30이지만, Mathf.Deg2Rad 을 곱함으로써 30이 아닌 30라디안이 된다. <- 30 각도가 되었다.
    1. 각도르 구했으니, 어느 방향으로 포물선을 그릴지 정해야 하는데 이때, Vector3 up = cam.transform.up; 을 함으로써
      up x Mathf.Tan(projecttileAngle * Mathf.Deg2Rad)을 한 것이고,
  • 나머지 절반은 중력 작용이라, Rigidbody를 이용하면 된다.
    1. 수류탄이 앞으로 나아갈 수 있게 하기 위해 Vector3 forward = cam.transform.forward; 을 해서 forward를 + 해주는 것이다. - 4. 최종 포물선의 방향을 구했지만, 정규화 해야한다. 즉, Normalize()을 해야 한다.

Nomalize() 하는 이유는?

✅ 17. 무기 교체시 , List 컬렉션으로 관리

public class PlayerController : MonoBehaviour
{
  public List<GunWeapon> weapons; //무기들의 스크립트(컴포넌트)를 List로 관리
  int currentWeaponIndex; //무기 자체는 currentWeaponIndex 의 int로 관리
                                                                        
	void Update()
	{
       if(fireAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 눌렸나요?
        {
            //gunWeapon.FireWeapon();
            weapons[currentWeaponIndex].FireWeapon(); //무기 번호가 다르므로, 무기 번호에 따라 무기에 부착된 스크립트에 접근해서 각각 다른 함수를 호출 할 수 있다.
  // Projecttile 스크립트는 GunWeapon 스크립트로부터 상속받고 있기 때문에 접근 가능
        }
        if (reloadAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 눌렸나요?
        {
            //gunWeapon.ReloadWeapon();
            weapons[currentWeaponIndex].ReloadWeapon();
        }
   }
  
   public void OnChangeWeapon() // 위 개념과 동일하다.
   {
        weapons[currentWeaponIndex].gameObject.SetActive(false);
        currentWeaponIndex++;
        if(currentWeaponIndex > weapons.Count - 1)
        {
            currentWeaponIndex = 0;
        }

        weapons[currentWeaponIndex].gameObject.SetActive(true);
   }
  

✅ 18. 재장전하여 남은 탄피나,탄창 등 코드 구현

    1. 탄창에 남은 공간 계산(현재 총알(currentBullet)이 얼마나 부족한지를 계산하는)
if(totalBullet >= maxBullet - currentBullet)
  • 예를 들어, 최대 탄약(maxBullet)이 8발이고, 현재 남은 총알이 3발이면, 이때 필요한 탄약은 5발이다. 즉, 3/8 상태에서 장전시 5발이 필요하므로 8/8
    1. 남은 총알(totalBullet)이 충분하면, 그 필요한 양만큼 totalBullet에서 빼고, 현재 총알을 최대치(maxBullet)로 채웁니다.
totalBullet -= maxBullet - currentBullet; // maxBullet은 최대 탄피, 8/28 로 시작해서 1발쏘고 7/28이 되었을 때 장전하면 8/27로 설정
//즉, 8(최대탄피,maxBullet) - 3(현재 남은 탄피,currentBullet) = 5 -> 탄창(totalBullet)에서 뺄 값
currentBullet = maxBullet; //최대 탄피를 현재 탄피에 저장 , 장전하면 8/(28-5) 이므로, 8/23이 될 것이다.
    1. 남은 탄약이 부족할 때 (즉, 필요한 탄약이 5발인데 남은 탄약이 3발밖에 없다면, 남은 모든 탄약을 현재 총알(currentBullet)에 채우고, totalBullet을 0으로 만듭니다.)
else // 현재 남은 탄피가 6발 있고, 최대 탄피는 11발이다. 또한, 총 탄피는 5발 있다. 즉, 6/5 일 경우 -> 11/0 으로 한다.
{
    currentBullet += totalBullet; //현재 탄피 + 최대 탄피 = 11
    totalBullet = 0; //최대 탄피를 0으로 설정
}

✅ 19. NavyMeshAgent 컴포넌트를 이용하여 목적지 설정(agent.destination 사용), 목적지 남은지점(agent.remainingDistance)

NavMeshAgent agent;
void Start()
{
      player = GameObject.FindWithTag("Player");
      agent = GetComponent<NavMeshAgent>();
      agent.destination = player.transform.position;
 }  
 void Update()
 {       
     if(agent.remainingDistance < 1.0f) //목적지 지점까지 1m보다 작을 경우.즉, 목적지와 가까워질 수록
     {
         agent.destination = player.transform.position; //적의 목적지는 플레이어 위치야. -> 추격
     }
 }

✅ 20. 무적시간

using UnityEngine;

public class Health : MonoBehaviour
{
    public float invincibleTime;
    float lastDamageTime;


    public float hp = 10;
    public float maxHp = 10;

    //..생략
  
    public void Damage(float damage)
    {
        if (hp > 0 && lastDamageTime + invincibleTime < Time.time)
        {
            hp -= damage;

            lastDamageTime = Time.time;

            if (hp <= 0)
            {
 
                if(healthListner != null)
                {
                    healthListner.OnDie();
                }
            }
            else
            {
                // ekcla
                Debug.Log("다침!");
            }
        }
    }
	//..생략
}
    1. 변수 선언
public float invincibleTime = 2f; //무적 상태가 지속되는 시간
float lastDamageTime; //마지막으로 피해를 입은 **시점(시간)**을 기록하는 변수
    1. 무적 시간 체크 로직
if (hp > 0 && lastDamageTime + invincibleTime < Time.time)
{
    hp -= damage;
    lastDamageTime = Time.time;
}
  • 첫번째 조건은 hp가 0보다 커야, Damage를 받을 수 있다라는 조건
  • 두 번째 조건은 마지막으로 피해를 입은 시간(lastDamageTime)에 무적 시간을 더한 값이 현재 시간(Time.time)보다 작을 때만, 즉 무적 시간이 지나야 다시 피해를 받을 수 있다.
  • Time.time: 게임이 시작된 후부터 흐른 시간을 나타내며, 현재 시점을 의미한다.
  1. 게임 시작 후 5초에 피해를 입었다면 lastDamageTime = 5, 그리고, invincibleTime = 2로 설정되어 있다.
  2. lastDamageTime + invincibleTime < Time.time 에 의해 7 < Time.time이므로, 7초 후에 적이 플레이어를 때렸을 때 플레이어가 피해를 입을 수 있다. 즉, 2초동안 무적시간인 개념
                                                    

✅ 21. 이미지(Image)의 색상을 게이지의 채워진 양에 따라 변화 시키기

public class GaugeColor : MonoBehaviour
{
    Image image;
    private void Start()
    {
        image = GetComponent<Image>();
    }
    void Update()
    {      
        GetComponent<Image>().color = Color.HSVToRGB(image.fillAmount / 3, 1.0f, 1.0f);
    }
}
  • Color.HSVToRGB(h, s, v): 이 함수는 HSV 색상 값을 RGB 색상으로 변환해줍니다. HSV는 색상(Hue), 채도(Saturation), 명도(Value)를 의미합니다.
    • image.fillAmount / 3: fillAmount는 0에서 1까지의 값으로, 게이지의 채워진 양을 나타낸다.. 이 값을 3으로 나누어, 더 부드럽게 색상이 변하도록 설정한 것
    • s : 채도,코드에서는 항상 1.0f로 설정하여, 채도가 100%
    • v : 명도, 이 역시 1.0f로 설정하여 항상 밝은 색상

✅ 22. 게임 오브젝트가 카메라와 항상 평행하게 바라보도록 만들기

    1. 적의 자식인 Canvas - HPBackGround 에다 LookCamera 스크립트 추가한다.

    1. 코드 작성한다.
using UnityEngine;

public class LookCamera : MonoBehaviour
{

    void Update()
    {
        transform.LookAt(transform.position + Camera.main.transform.forward);
    }
}
  • transform.LookAt(Vector3 targetPosition): 이 함수는 오브젝트를 특정 위치(targetPosition) 쪽을 바라보도록 만든다. 즉, 오브젝트의 앞면이 주어진 위치를 향하게 한다.
  • Camera.main.transform.forward: 현재 활성화된 메인 카메라의 "앞" 방향을 나타내는 벡터이다. 이 벡터는 카메라가 바라보는 방향을 의미한다.
  • transform.position + Camera.main.transform.forward: 오브젝트 자신의 위치에서 카메라의 앞 방향으로 벡터를 더한 값을 가리킨다.
  • 실행 시, 매 프레임마다 카메라의 앞 방향 + HPBackGround 위치 => HPBackGround 오브젝트가 플레이어를 바라보고 있다.

✅ 23. Canvas

화면 맨 위에 그리는 것(덮어버림) - 스크린 스페이스 오버레이

  • 화면 자체 좌표를 사용

카메라에 캔버스가 맞춰진다. - 스크린 스페이스 카메라

  • 카메라에 비치는 캔버스가 100m 밖에 있다. 100m는 Plane Distance의 값
  • 캔버스가 직접 플레이어의 화면 앞에 카메라가 배치 된다.

세계 안에 캔버스를 그리는 것 ,월드 안에다 평면을 만드는 것 - 월드 스페이스

0개의 댓글