













Built - in 에서 URP로 변경하는 방법이다.

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

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

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







Mesh Collider를 부착한 오브젝트는 오브젝트의 Mesh별로 계산을 하기 때문에 과부하가 올 수 있다. 그럴 경우 Convex를 체크하면 된다.
체크할 경우 오목한 곳에는 들어갈 수 없다. 오브젝트의 모양별로 Convex를 체크할지 말지 정하면 될 것 같다.
Box의 경우 BoxCollider을 사용하면 된다.


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

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

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

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 함수를 이용해서 움직일 수 있게 함.
}
}
- InputActionAsset inputActions = GetComponent< PlayerInput >().actions;


moveAction = inputActions.FindAction("Move");

charController = GetComponent<CharacterController>();

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(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 함수를 이용해서 움직일 수 있게 함.
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; //변경된 회전 각도를 오브젝트에 반영
//=============================================
}
}
InputActionAsset inputActions = GetComponent<PlayerInput>().actions; //Defalut Map에 할당된 Player을 가져오는 개념.
lookAction = inputActions.FindAction("Look"); //Look에 해당하는 Action 값을 moveAction에 저장.
horizontalAngle = transform.localEulerAngles.y; //로컬 좌표 y축 각도를 회전 시켜야 좌,우 회전이 가능하다.
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; // 변경된 회전 각도를 오브젝트에 반영
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;
}
}
public Transform cameraTransform; //카메라 할당
float verticalAngle;
void Start()
{
verticalAngle = 0; //0으로 초기화 한다.
}
float turnCam = look.y * mouseSens; //Look이라는 Action 특성 상
//y축의 값을 움직여야 상,하 회전이 가능
verticalAngle -= turnCam;
플레이어가 아닌 카메라의 회전 축을 기준으로 할 때는 Y축이 일반적으로 수직 방향을 나타낸다. 또한, Look Action은 마우스를 올리면 올릴 수록 양수의 값을 갖고, 마우스를 내리면 내릴 수록 음수의 값을 가진다. 그러므로 verticalAngle -= turnCam; 해주게 되면 카메라의 x축의 회전값이 마우스를 내릴 때 양수, 올릴 때 음수가 된다. 즉, 반대가 되는 개념이다.
verticalAngle = Mathf.Clamp(verticalAngle, -89f, 89f); //verticalAngle에 값이 -89~99사이라면
currentAngle = cameraTransform.localEulerAngles;
currentAngle.x = verticalAngle; //currentAngle.x에 변경된 회전 각도를 업데이트한다.
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으로
}
}


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);
}



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; //점프 했기 때문에 땅에 붙어 있지 않음
}
}
}





















using UnityEngine;
public class GunWeapon : MonoBehaviour
{
//플레이어가 총을 쏘고, 장전해야 하므로 public으로 총쏘는 함수와 장전하는 함수를 만든다.
public void FireWeapon()
{
Debug.Log("Fire !");
}
public void ReloadWeapon()
{
}
}
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으로 변경하였다.


실행 시, 마우스 좌클릭(Fire 키)을 통해 로그가 정상적으로 출력된다.

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");
}
}
}
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() 호출
}
}
//...생략
}

















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); //꼭짓점 좌표를 넣어줌.
}
}

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

public class GunWeapon : MonoBehaviour { Animator animator; public GameObject BulletLine; //총알 선(궤적) public Transform FirePos; }
- 프리펩으로 만들었던 총알 선(BulletLine), 총알 초기 위치(FirePos) 을 선언해야 한다.
void RayCastFire() { Camera cam = Camera.main; }
void RayCastFire() { RaycastHit hit; //빛을 발사한 결과를 hit에 저장 Ray r = cam.ViewportPointToRay(Vector3.one / 2); //카메라 정 가운데를 쏘는 빛을 r에 저장한다.(1/2이므로 0.5 크기임) } - Camera 컴포넌트에 ViewportPointToRay()을 이용한다. - ViewportPointToRay(Vector3.one / 2)을 함으로써, 카메라 정중앙에 쏘게끔 한다.
void RayCastFire() { Vector3 hitPos = r.origin + r.direction * 200; //카메라 방향으로부터 200m 떨어진 지점을 Vector의 hitPos에 저장 } - 빛을 발사한 시작 위치 + 빛을 발사했던 방향 * 200 을 하는 것이다.
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에 저장. } }
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는 충돌 지점을 의미한다. } }
- 벡터 배열로 저장한 이유는 선이 여러개가 될 수 있으므로 벡터 배열로 저장한다.
void RayCastFire() { GO.GetComponent<LineRenderer>().SetPositions(pos); //꼭짓점 좌표를 넣어줌. }
- SetPositions()는 꼭짓점(x,y,z) 좌표를 설정하는 함수이다.

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


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);
}
}

하지만 셰이더가 빠진 것을 볼 수 있다.








public class ProjectileWeapon : GunWeapon
{
public GameObject projecttilePrefab; //수류탄
public float projecttileAngle = 30; // 수류탄 던질 때 포물선
public float projecttileForce = 30; //속도
public float projecttileTime = 5; //폭파시간
}
필요한 변수 선언을 한다
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(); //총알 궤적을 그리는 함수 호출
}
}
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()
{
}
}

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방향으로 이동.
}
}

부모 클래스에서 만든 변수는 할당하지 않는다.
또한, 중요한 점은 플레이어 컨트롤러에서 Weapon을 GranageLancher로 변경해야 던질 수 있다.

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






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);
}
}
}


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); //교체된 무기 활성화
}
}



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();
}
//..생략
}


실행 시, 교체할 때 텍스트가 바뀔 것이다.

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;
}
}
//..생략
}







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


이렇게 하게 되면 기존 PuchRight 애니메이션 클립을 제어할 수 없었지만, 복사를 통해 제어할 수 있게 되었다.


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











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; //적의 목적지는 플레이어 위치야. -> 추격
}
}
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; //적의 목적지는 플레이어 위치야. -> 추격
}
}
}



Idle -> Run

Idle -> Attack

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");
}
}


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();
}
}
주목해야 될 부분은 인터페이스이다. 인터페이스를 상속 받을 시, 인터페이스에 들어간 함수를 자식 클래스가 구현해야 한다.
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;
}
//...생략
}

그리고, 총 쏘는 코드가 담겨져 있는 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); //호출한다.
}
}
//...생략
}
//...생략
}
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); //호출한다.
}
}
}






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 모습

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();
}
}


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 사진이다.



using UnityEngine;
public class LookCamera : MonoBehaviour
{
void Update()
{
transform.LookAt(transform.position + Camera.main.transform.forward);
}
}










실행 시, 아래와 같다.



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;
}
}
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();
}
}



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(); //종료
}
}




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



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();
}
}
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);
}
//..생략
}

실행시, 플레이어가 죽었을 때의 모습과 적을 모두 물리쳤을 때 ui가 달라진 것을 볼 수 있다.


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();
//..생략
}
}

따로 소스는 넣지 않는다.
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); 하여 오디오를 재생 시킨다.

using UnityEngine;
public class Bomb : MonoBehaviour
{
//..생략
public AudioClip fireInTheHole;
//..생략
public void PlaySound()
{
GetComponent<AudioSource>().PlayOneShot(fireInTheHole);
}
//..생략
}

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("다침!");
}
}
}
//..생략
}








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에서 "요네"라는 키에 해당하는 값을 콘솔에 출력합니다. 결과는 **"개사기"**이다.
}
}

Dictionary<string, string> dict2 = JsonConvert.DeserializeObject<Dictionary<string, string>>(json1);
Debug.Log(dict2["요네"]);
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);
}
}

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);
}
}

using Newtonsoft.Json;
using UnityEngine;
public class Data
{
//..생략
[JsonProperty] //⭐
private string secret;
//..생략
}

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);
}
}



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;
}
}
}



- InputActionAsset inputActions = GetComponent< PlayerInput >().actions;
- actions는 PlayterInput 컴포넌트에 Defalut Map을 의미한다.
- Defalut Map은 Player로 설정되어 있다. Defalut Map에 대한 의미는 아래 사진과 같다.
InputAction moveAction; //InputAction 컴포넌트를 말함.
void Start()
{
moveAction = inputActions.FindAction("Move");
}
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(move.magnitude > 1) // 이동벡터. 이동벡터가 1보다 커야 하는 이유
{
move.Normalize();
}
move = transform.TransformDirection(move); //이 벡터를 내가 쳐다보고 있는 방향벡터로 바꿔준다.
float horizontalAngle; //마우스 각도
void Start()
{
horizontalAngle = transform.localEulerAngles.y; //y축을 회전 시켜야 좌,우 회전이 가능하다.
}
public Transform cameraTransform; //카메라 할당
float turnCam = look.y * mouseSens;
verticalAngle -= turnCam;
verticalAngle = Mathf.Clamp(verticalAngle, -89f, 89f); //verticalAngle에 값이 -89~99사이라면


InputAction fireAction;
void Start()
{
fireAction = inputActions.FindAction("Fire");
}
void Update()
{
if(fireAction.WasCompletedThisFrame()) //WasCompletedThisFrame(): 혹시 이번 프레임에 눌렸나요?
{
gunWeapon.FireWeapon();
}
}
GameObject GO = Instantiate(BulletLine);
Vector3[] pos = new Vector3[]
{
FirePos.position,hitPos
};
GO.GetComponent<LineRenderer>().SetPositions(pos);



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;
}






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; //파티클의 정면 방향은 수직
}
if (animator.GetCurrentAnimatorStateInfo(0).IsName("GunIdle_Anim"))
{
animator.SetTrigger("Fire");
Fire();
}
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;
}



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);
}
if(totalBullet >= maxBullet - currentBullet)
totalBullet -= maxBullet - currentBullet; // maxBullet은 최대 탄피, 8/28 로 시작해서 1발쏘고 7/28이 되었을 때 장전하면 8/27로 설정
//즉, 8(최대탄피,maxBullet) - 3(현재 남은 탄피,currentBullet) = 5 -> 탄창(totalBullet)에서 뺄 값
currentBullet = maxBullet; //최대 탄피를 현재 탄피에 저장 , 장전하면 8/(28-5) 이므로, 8/23이 될 것이다.
else // 현재 남은 탄피가 6발 있고, 최대 탄피는 11발이다. 또한, 총 탄피는 5발 있다. 즉, 6/5 일 경우 -> 11/0 으로 한다.
{
currentBullet += totalBullet; //현재 탄피 + 최대 탄피 = 11
totalBullet = 0; //최대 탄피를 0으로 설정
}
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; //적의 목적지는 플레이어 위치야. -> 추격
}
}
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("다침!");
}
}
}
//..생략
}
public float invincibleTime = 2f; //무적 상태가 지속되는 시간
float lastDamageTime; //마지막으로 피해를 입은 **시점(시간)**을 기록하는 변수
if (hp > 0 && lastDamageTime + invincibleTime < Time.time)
{
hp -= damage;
lastDamageTime = Time.time;
}
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);
}
}


using UnityEngine;
public class LookCamera : MonoBehaviour
{
void Update()
{
transform.LookAt(transform.position + Camera.main.transform.forward);
}
}
