using UnityEngine;
public class GameManager : MonoBehaviour
{
public static bool canPlayerMove = true;//플레이어의 움직임 제어
public static bool isOpenInventory = false;// 인벤토리 활성화
public static bool isOpenCraftManual = false;//건축메뉴창 활성화
public static bool isNight = false;//낮과 밤 On/off
public static bool isWater = false;//물속 On/off
private WeaponManager theWM;//웨폰 메니져 불러오기
private bool flag = false;
private void Start()
{
theWM = FindAnyObjectByType<WeaponManager>();
}
// Update is called once per frame
void Update()
{
if (isOpenInventory || isOpenCraftManual)
{
Cursor.lockState = CursorLockMode.None; //마우스 커서를 보임
Cursor.visible = true; //마우스를 보이게 함
canPlayerMove = false;
}
else
{
Cursor.lockState = CursorLockMode.Locked; //커서를 장금
Cursor.visible = false; //마우스를 보이지 않게 함
canPlayerMove = true;
}
if (isWater)
{
if (!flag)
{
StopAllCoroutines();
StartCoroutine(theWM.WeaponInCoroutine());
flag = true;
}
}
else
{
if (flag)
{
flag = false;
theWM.WeaponOut();
}
}
}
}
using System.Collections;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//스피드 조정 변수
[SerializeField]//시리얼라이즈 필드,인스펙터에 공개
private float walkSpeed;//이동 스피드
[SerializeField]
private float runSpeed;//달리기 스피드
[SerializeField]
private float crouchSpeed;
[SerializeField]
private float swimSpeed;//수영스피드
[SerializeField]
private float swimFastSpeed;//빠른 수영 스피드
[SerializeField]
private float upSwimSpeed;//수영 상승 스피드
private float applySpeed;//현재 걷는,뛰는
[SerializeField]
private float jumpForce;
//상태변수
private bool isWalk = false;
private bool isRun = false;
private bool isCrouch = false;
private bool isGround = true;
//움직임 체크 변수
private Vector3 lastPos;
//앉았을때 얼마나 앉을지 결정하는 변수
[SerializeField]
private float crouchPosY;
private float originPosY;
private float applyCrouchPosY;
//땅 착지 여부
private CapsuleCollider capsuleCollider;
//카메라 민감도
[SerializeField]
private float lookSensitivity;
[SerializeField]
//카메라 한계
private float cameraRotationLimit;
private float currentCameraRotationX = 0;
//필요 컴포넌트
[SerializeField]
private Camera theCamera;//카메라
private Rigidbody myRigid;//리지드바디
[SerializeField]
private GunController theGunController;
private Crosshair theCrosshair;
private StatusController theStatusController;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
capsuleCollider = GetComponent<CapsuleCollider>();
myRigid = GetComponent<Rigidbody>();//Player에 있는 리지드바디 장착
theGunController = FindAnyObjectByType<GunController>();
theCrosshair = FindAnyObjectByType<Crosshair>();
theStatusController = FindAnyObjectByType<StatusController>();
//초기화
applySpeed = walkSpeed;
originPosY = theCamera.transform.localPosition.y;
applyCrouchPosY = originPosY;
}
// Update is called once per frame
void Update()
{
if (GameManager.canPlayerMove)
{
WaterCheck();
IsGround();
TryJump();
if (GameManager.isWater)
{
TryRun();
}
TryCrouch();
Move();
MoveCheck();
CameraRotation();
CharacterRotation();
}
}
private void WaterCheck()
{
if (GameManager.isWater)
{
if (Input.GetKeyDown(KeyCode.LeftShift))
applySpeed = swimFastSpeed;
applySpeed = swimSpeed;
}
}
private void TryCrouch()//앉기 시도
{
if (Input.GetKeyDown(KeyCode.LeftControl))
{
Crouch();
}
}
private void Crouch()//실제 앉기
{
isCrouch = !isCrouch;
theCrosshair.CrouchingingAnimation(isCrouch);
//if (isCrouch)
// isCrouch = false;
//else
// isCrouch = true;위 한줄짜리 코드 축약임
if (isCrouch)
{
applySpeed = crouchSpeed;
applyCrouchPosY = crouchPosY;
}
else
{
applySpeed = walkSpeed;
applyCrouchPosY = originPosY;
}
StartCoroutine(CrouchCoroutine());
}
IEnumerator CrouchCoroutine()//부드러운 앉기 동작 실행
{
float _posY = theCamera.transform.localPosition.y;
int count = 0;
while(_posY != applyCrouchPosY)
{
count++;
_posY = Mathf.Lerp(_posY, applyCrouchPosY, 0.5f);
theCamera.transform.localPosition = new Vector3(0, _posY, 0);
if (count > 15)
break;
yield return new WaitForSeconds(0.1f);
}
theCamera.transform.localPosition = new Vector3(0, applyCrouchPosY, 0f);
}
private void IsGround()//지면 체크
{
isGround = Physics.Raycast(transform.position, Vector3.down, capsuleCollider.bounds.extents.y + 0.1f);
theCrosshair.JumpingAnimation(!isGround);
}
private void TryJump()//점프시도
{
if (Input.GetKeyDown(KeyCode.Space) && isGround && theStatusController.GetCurrentSP() > 0 && !GameManager.isWater)
{
Jump();
}
else if (Input.GetKey(KeyCode.Space) && GameManager.isWater)
UpSwim();
}
private void UpSwim()
{
myRigid.linearVelocity = transform.up * upSwimSpeed;
}
private void Jump()//점프
{
if (isCrouch)
Crouch();
theStatusController.DecreaseStamina(100);
myRigid.linearVelocity = transform.up * jumpForce;
}
private void TryRun()//달리기 시도
{
if (Input.GetKey(KeyCode.LeftShift) && theStatusController.GetCurrentSP() > 0)
{
Running();
}
if (Input.GetKeyUp(KeyCode.LeftShift) || theStatusController.GetCurrentSP() <= 0)
{
RunningCancel();
}
}
private void Running()//달리기 실행
{
if (isCrouch)
Crouch();
theGunController.CancelFineSight();
isRun = true;
theCrosshair.RunningAnimation(isRun);
theStatusController.DecreaseStamina(10);
applySpeed = runSpeed;
}
private void RunningCancel()//달리기 취소
{
isRun = false;
theCrosshair.RunningAnimation(isRun);
applySpeed = walkSpeed;
}
private void Move()//걷기 실행
{
float _moveDirX = Input.GetAxisRaw("Horizontal");//좌우 방향키를 입력시 +1 ~ -1이 반환된다
float _moveDirZ = Input.GetAxisRaw("Vertical");//상하 움직임 입력시 +1 ~ -1이 반환된다
Vector3 _moveHorizontal = transform.right * _moveDirX;
Vector3 _moveVertical = transform.forward * _moveDirZ;//실제 입력과 같이 이동을 처리함
Vector3 _velocity = (_moveHorizontal + _moveVertical).normalized * applySpeed;
myRigid.MovePosition(transform.position + _velocity * Time.deltaTime);
}
//움직임 체크
private void MoveCheck()
{
if (!isRun && !isCrouch && isGround)
{
if (Vector3.Distance(lastPos, transform.position) >= 0.01f)
isWalk = true;
else
isWalk = false;
theCrosshair.WalkingAnimation(isWalk);
lastPos = transform.position;
}
}
private void CharacterRotation()//좌우 캐릭터 회전
{
float _yRotation = Input.GetAxisRaw("Mouse X");
Vector3 _characterRotationY = new Vector3(0f, _yRotation, 0f) * lookSensitivity;
myRigid.MoveRotation(myRigid.rotation * Quaternion.Euler(_characterRotationY));
//Debug.Log(myRigid.rotation);
//Debug.Log(myRigid.rotation.eulerAngles);
}
private void CameraRotation()//시점 위아래 이동
{
float _xRotation = Input.GetAxisRaw("Mouse Y");
float _cameraRotationX = _xRotation * lookSensitivity;
currentCameraRotationX -= _cameraRotationX;
currentCameraRotationX = Mathf.Clamp(currentCameraRotationX, -cameraRotationLimit, cameraRotationLimit);
theCamera.transform.localEulerAngles = new Vector3(currentCameraRotationX, 0f, 0f);
}
}
using NUnit.Framework.Constraints;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponManager : MonoBehaviour
{
//무기 중복 교체 실행방지
public static bool isChangeWeapon = false;//static 공유되는 자원
//현재 무기의 애니메이션
public static Transform currentWeapon;
public static Animator currentWeaponAnim;
//현재 무기 타입
[SerializeField]
private string currentWeaponType;
[SerializeField]
private float changeWeaponDelayTime;
[SerializeField]
private float changeWeaponEndDelayTime;
//무기 종류들 전부 관리
[SerializeField]
private Gun[] guns;
[SerializeField]
private CloseWeapon[] hands;
[SerializeField]
private CloseWeapon[] axes;
[SerializeField]
private CloseWeapon[] pickaxes;
//관리 차원에서 쉽게 무기 접근이 가능하게 만듦
private Dictionary<string, Gun> gunDictionary = new Dictionary<string, Gun>();
private Dictionary<string, CloseWeapon> handDictionary = new Dictionary<string, CloseWeapon>();
private Dictionary<string, CloseWeapon> axeDictionary = new Dictionary<string, CloseWeapon>();
private Dictionary<string, CloseWeapon> pickaxesDictionary = new Dictionary<string, CloseWeapon>();
//필요한 컴포넌트
[SerializeField]
private GunController theGunController;
[SerializeField]
private HandController theHandController;
[SerializeField]
private AxeController theAxeController;
[SerializeField]
private PickaxeController thePickaxeController;
void Start()
{
for (int i = 0; i < guns.Length; i++)
{
gunDictionary.Add(guns[i].gunName, guns[i]);
}
for (int i = 0; i < hands.Length; i++)
{
handDictionary.Add(hands[i].closeWeaponName, hands[i]);
}
for (int i = 0; i < axes.Length; i++)
{
axeDictionary.Add(axes[i].closeWeaponName, axes[i]);
}
for (int i = 0; i < pickaxes.Length; i++)
{
pickaxesDictionary.Add(pickaxes[i].closeWeaponName, pickaxes[i]);
}
}
void Update()
{
if (!isChangeWeapon)
{
if (Input.GetKeyDown(KeyCode.Alpha1))
StartCoroutine(ChangeWeaponCoroutine("HAND", "맨손"));
else if (Input.GetKeyDown(KeyCode.Alpha2))
StartCoroutine(ChangeWeaponCoroutine("GUN", "SubMachineGun1"));
else if (Input.GetKeyDown(KeyCode.Alpha3))
StartCoroutine(ChangeWeaponCoroutine("AXE", "Axe"));
else if (Input.GetKeyDown(KeyCode.Alpha4))
StartCoroutine(ChangeWeaponCoroutine("PICKAXE", "Pickaxe"));
}
}
public IEnumerator ChangeWeaponCoroutine(string _type, string _name)
{
isChangeWeapon = true;
currentWeaponAnim.SetTrigger("Weapon_Out");
yield return new WaitForSeconds(changeWeaponDelayTime);
CancelPreWeaponAction();
WeaponChange(_type, _name);
yield return new WaitForSeconds(changeWeaponEndDelayTime);
currentWeaponType = _type;
isChangeWeapon = false;
}
//무기 취소 함수
private void CancelPreWeaponAction()
{
switch (currentWeaponType)
{
case "GUN":
theGunController.CancelFineSight();
theGunController.CancelReload();
GunController.isActivate = false;
break;
case "HAND":
HandController.isActivate = false;
break;
case "AXE":
AxeController.isActivate = false;
break;
case "PICKAXE":
PickaxeController.isActivate = false;
break;
}
}
private void WeaponChange(string _type, string _name)
{
if(_type == "GUN")
theGunController.GunChange(gunDictionary[_name]);
else if(_type == "HAND")
theHandController.CloseWeaopnChange(handDictionary[_name]);
else if (_type == "AXE")
theAxeController.CloseWeaopnChange(axeDictionary[_name]);
else if (_type == "PICKAXE")
thePickaxeController.CloseWeaopnChange(pickaxesDictionary[_name]);
}
public IEnumerator WeaponInCoroutine()
{
isChangeWeapon = true;
currentWeaponAnim.SetTrigger("Weapon_Out");
yield return new WaitForSeconds(changeWeaponDelayTime);
currentWeapon.gameObject.SetActive(false);
}
public void WeaponOut()
{
isChangeWeapon = false;
currentWeapon.gameObject.SetActive(true);
}
}
using UnityEngine;
public class Crosshair : MonoBehaviour
{
[SerializeField]
private Animator animator;
//크로스헤어 상태에 따른 총의 정확도
private float gunAccuracy;
//크로스 헤어 비활성화를 위한 부모 객체
[SerializeField]
private GameObject go_crosshairHUD;
[SerializeField]
private GunController theGunController;
public void WalkingAnimation(bool _flag)
{
if (!GameManager.isWater)
{
WeaponManager.currentWeaponAnim.SetBool("Walk", _flag);
animator.SetBool("Walking", _flag);
}
}
public void RunningAnimation(bool _flag)
{
if (!GameManager.isWater)
{
WeaponManager.currentWeaponAnim.SetBool("Run", _flag);
animator.SetBool("Running", _flag);
}
}
public void JumpingAnimation(bool _flag)
{
if (!GameManager.isWater)
{
animator.SetBool("Running", _flag);
}
}
public void CrouchingingAnimation(bool _flag)
{
if (!GameManager.isWater)
{
animator.SetBool("Crouching", _flag);
}
}
public void FineingAnimation(bool _flag)
{
if (!GameManager.isWater)
{
animator.SetBool("FineSight", _flag);
}
}
public void FireAnimation()
{
if (!GameManager.isWater)
{
if (animator.GetBool("Walking"))
animator.SetTrigger("Walk_Fire");
else if (animator.GetBool("Crouching"))
animator.SetTrigger("Crouch_Fire");
else
animator.SetTrigger("Idle_Fire");
}
}
public float GetAccuracy()
{
if (animator.GetBool("Walking"))
gunAccuracy = 0.06f;
else if (animator.GetBool("Crouching"))
gunAccuracy = 0.015f;
else if (theGunController.GetFineSightMode())
gunAccuracy = 0.001f;
else
gunAccuracy = 0.035f;
return gunAccuracy;
}
}
using UnityEngine;
using UnityEngine.UI;
public class Water : MonoBehaviour
{
[SerializeField] private float waterDrag;//물속 중력
private float originDrag;
[SerializeField] private Color waterColor;//물속 색깔
[SerializeField] private float waterFogDensity;//물의 탁함 정도
[SerializeField] private Color waterNightColor;// 밤 상태의 물속 색깔
[SerializeField] private float waterNightFogDensity;
private Color originColor;//물속 색깔 오리진
private float originFogDensity;//물의 탁함 정도 오리진
//밤낮 오리진
[SerializeField] private Color originNightColor;
[SerializeField] private float originNightFogDensity;
[SerializeField] private string sound_WaterOut;//물속에서의 사운드
[SerializeField] private string sound_WaterIn;
[SerializeField] private string sound_Breathe;
[SerializeField] private float breatheTime;//물속에서 숨쉬기 소리
private float currentBreatheTime;
[SerializeField] private float totalOxygen;//산소
private float currentOxygen;
private float temp;
[SerializeField] private GameObject go_BaseUi;//게이지 ON/OFF
[SerializeField] private Text text_totalOxygen;
[SerializeField] private Text txet_currentOxygen;
[SerializeField] private Image image_gauge;
private StatusController theStatusStat;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
originColor = RenderSettings.fogColor;
originFogDensity = RenderSettings.fogDensity;
originDrag = 0;
theStatusStat = FindAnyObjectByType<StatusController>();
currentOxygen = totalOxygen;
text_totalOxygen.text = totalOxygen.ToString();
}
// Update is called once per frame
void Update()
{
if (GameManager.isWater)
{
currentBreatheTime += Time.deltaTime;
if(currentBreatheTime >= breatheTime)
{
SoundManager.instance.PlaySE(sound_Breathe);
currentBreatheTime = 0;
}
}
DecreaseOxygen();
}
private void DecreaseOxygen()
{
if (GameManager.isWater)
{
currentOxygen -= Time.deltaTime;
txet_currentOxygen.text = Mathf.RoundToInt(currentOxygen).ToString();//소수점을 없세고 정수만 출력
image_gauge.fillAmount = currentOxygen / totalOxygen;
if(currentOxygen <= 0)
{
temp += Time.deltaTime;
if(temp >= 1)
{
theStatusStat.DecreaseHP(1);
temp = 0;
}
}
}
}
private void OnTriggerEnter(Collider other)
{
if(other.transform.tag == "Player")
{
GetWater(other);
}
}
private void OnTriggerExit(Collider other)
{
if (other.transform.tag == "Player")
{
GetOutWater(other);
}
}
private void GetWater(Collider _player)
{
SoundManager.instance.PlaySE(sound_WaterIn);
go_BaseUi.SetActive(true);
GameManager.isWater = true;
_player.transform.GetComponent<Rigidbody>().linearDamping = waterDrag;
if (GameManager.isNight)
{
RenderSettings.fogColor = waterColor;
RenderSettings.fogDensity = waterFogDensity;
}
else
{
RenderSettings.fogColor = waterNightColor;
RenderSettings.fogDensity = waterNightFogDensity;
}
}
private void GetOutWater(Collider _player)
{
if (GameManager.isWater)
{
go_BaseUi.SetActive(false);
currentOxygen = totalOxygen;
SoundManager.instance.PlaySE(sound_WaterOut);
GameManager.isWater = false;
_player.transform.GetComponent<Rigidbody>().linearDamping = originDrag;
if (!GameManager.isNight)
{
RenderSettings.fogColor = originColor;
RenderSettings.fogDensity = originFogDensity;
}
else if (GameManager.isNight)
{
RenderSettings.fogColor = originNightColor;
RenderSettings.fogDensity = originNightFogDensity;
}
}
}
}
이번 강좌에서는 수중으로 들어갔을때 수영을 하는 것과 호흡하는 것을 구현할 것이다
호흡을 모두 다 하면 HP가 달게 할 것이다
일단 PlayerController 스크립트부터 열어보자
기본 변수에서 표시한 부분을 추가해주자
주석으로 설명을 해두었다
84번줄에 if문으로 GameManger에서 isWater이 아닐때 TryRun()함수를 실행하게 하자
다음은 WaterCheck()함수를 작성하자
WaterCheck 함수이다
GameManger에서 isWater이 true일때
99번 줄에 if문으로 왼쪽 시프트키를 누르고 있으면
100번줄은 빠르게 수영하는 변수(swimFastSpeed)가 적용되는 것이고
아니면 applySpeed가 swimSpeed의 속도로 바뀐다
이제 TryJump함수를 수정하자
160번에 if문에 GameManager.isWater이 false일때 Jump즉 수영중이 아닐때 점프를 하게하고
164번줄을 보면 else if문으로 스페이스 바 버튼을 누르는 것을 같지만 &&으로 추가조건에 GameManager.isWater이 true일때
UpSwim()을 호출하게 한다
UpSwim()함수를 작성하자
KD님의 강좌에는 myRigid.Velocity라고 되어 있지만 유니티6에서는 변경되어서 linearVelocity로 변경되었다
어쨌든 리지드바디에 transform.up * upSwimSpeed로 수면 위로 올라가게 한다
이제 유니티엔진 인스팩터에 값을 넣어주고 실행하면 천천히 가라앉으며 스페이스바를 누르면 수면위로 오르게 되고 시프르를 누르고 있으면 빠르게 해엄친다
다음은 수영할때 무기를 집어 넣는 것을 구현해보겠다
WeaponManager 스크립트를 열어서 수정하자
일단 코루틴 WeaponInCoroutine함수를 추가해주겠다
isChangeWeapon을 ture로 바꿔주고
애니메이션 Weapon_Out를 해준다
그뒤 140번줄에 웨폰 체인지 딜레이 만큼 실행하여 대기시간을 주고
웨폰 오브젝트를 false해준다
다음은 WeaponOut함수이다
isChangeWeapon 함수를 false해주고
웨폰 오브젝트를 true해준다
이제 GameManager에서 사용하게 GameManager 스크립트를 수정하자
기본 변수 목록이다
주석으로 설명해주었고 추가로 작성해준다
표시한 부분을 작성해주자
35번줄은 isWater이 true일때
if문으로 flag가 false일때
모든 코루틴을 스돕하고 웨펀메니져의 weaponInCoroutine()함수를 실행하고
flag를 true해준다
44에 else로는
if문으로 flag가 fasle일때
웨펀메니져의 WeaponOut()를 호출해준다
여기까지 작성했다면 유니티엔진에서 물속에 들어가면 무기를 해재하는 부분까지 구현되었을 것이다
그런데 여기서 문제가 무장해제 애니메이션을 실행했는데 콜솔에 오류가 좀 나올 것이다 에니메이션을 호출하는게 꼬인게 원인이므로 조건문을 넣어주자
Crosshair 스크립트를 열어보자
표시한 부분이 많은데 간단하게 생각함면 isWater이 false일대 애니메이터가 실행되게 바꿔주는 것이다
그러면 애니메이터로 인해 오류가 나는 부분이 해결될 것이다
이제 호흡 UI게이지를 만들어보자
유니티 하이러키에 빈객체를 만들고 이름을 Oxygen 이라고 명명한다
그뒤 하위에 Image를 만들어주고 Base_UI라고 명명한다
그 뒤 왼쪽의 사진과 같은 위치에 이동시켜주고 인스펙터를 수정해서 게이지의 틀같은 이미지로 만들어 준다
다음은 아위에 gauge 라고 명명한 Image를 만들어 주고 이미지 타입을 Filled로 바꿔주고 Horizontal로 바꿔준다
이 부분이 실제 게이지가 늘어났다 줄어들었다 하는 부분이다
이제 하위에 Text를 레거시 타입으로 만들어서 3개를 준비한 뒤
왼쪽의 100부분과
경계선인 /
오른쪽의 총량 100을 나누어서 배치해준다
이제 이 부분을 스크립트로 관리하자
이제 Water 스크립트를 열어서 수정해주자
위에 표시한 부분을 작성해주자
29번은 산소 총량이고
30번줄은 현재 산소
31번은 temp는 임시계산
33번 줄은 게이지 이미지 ON/Off
34번과 35번은 게이지 초량과 현제 게이지 텍스트 반영
36번은 게이지의 이미지의 이동
38번은 StatusController 스크립트를 장착한다
void Start 함수이다
47번은 스터테이스 스크립트를 장착하는 것이고
48번은 현재 산소 초기화
49번은 토탈 산소 초기화이다
void Updtae함수이다
DecreaseOxygen 함수를 호출한다
DecreaseOxygen 함수이다
if문으로 GameManager.isWater이 true일때
현재 산소에서 실제 시간을 빼주고
74번줄에 현재 산소 텍스트에서 수학 매서드를 적용하여 소수점을 반올림하는 정수출력을 해준다
그뒤 75번 줄에서 백분율로 현제 게이지가 줄어들게 만들어준다
77번에 if문으로 현제 산소가 0보다 작거나 같다면
if문으로 temp에 실제 시간을 더해주면서
if문으로 temp가 1보다 크거나 같으면
스터테이스에서 DecreaseHP(1)을 해주고
temp를 0으로 바꿔준다
이제 유니티엔진에서 인스펙터를 수정해주고...
슬롯을 채워준뒤 테스트를 하면...
이렇게 구현이 되었다
물속에 들어가면 무장이 해제되며
산소게이지가 나오고 잠수하면 게이지가 줄어들지만 수면위로 오르면 산소게이지가 회복된다
'FPS서바이벌 디펜스' 카테고리의 다른 글
33.타이틀 메뉴 (2) | 2025.06.06 |
---|---|
32.일시정지 메뉴 (0) | 2025.06.06 |
30.물구현과 수영 시스템 (0) | 2025.06.03 |
29.낮과 밤 구현하기 (0) | 2025.06.03 |
28.함정 발동 (1) | 2025.06.03 |