🎮 Game Dev (게임개발)/PC (데스크탑, 노트북, 터치패널)
[3D 액션게임] 07. 원거리무기 공격구현
- -
🔔 유튜브 크리에이터 골든메탈님의 유니티강의 3D 쿼터뷰 액션게임 [BE5] 를 보고 공부하여 작성한 게시글입니다! 🔔
전체코드보기
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public float speed;
public GameObject[] weapons;
public bool[] hasWeapons;
public GameObject[] grenades;
public int hasGrenades;
public Camera followCamera;
public int ammo;
public int health;
public int coin;
public int maxAmmo;
public int maxHealth;
public int maxCoin;
public int maxHasGrenades;
float hAxis;
float vAxis;
bool rDown;
bool jDown;
bool fDown;
bool reDown;
bool iDown;
bool sDown1;
bool sDown2;
bool sDown3;
bool isJump;
bool isDodge;
bool isSwap;
bool isReload;
bool isFireReady = true;
Vector3 moveVec;
Vector3 dodgeVec;
Rigidbody rigid;
Animator anim;
GameObject nearObject;
Weapon equipWeapon;
int equipWeaponIndex = -1;
float fireDelay;
void Awake()
{
rigid = GetComponent<Rigidbody>();
anim = GetComponentInChildren<Animator>();
}
// Update is called once per frame
void Update()
{
GetInput();
Move();
Turn();
Jump();
Attack();
Reload();
Dodge();
Swap();
Interation();
}
void GetInput()
{
// 키보드입력에 따라 0 ~ 1로 변환 left right up down
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
rDown = Input.GetButton("Run");
jDown = Input.GetButtonDown("Jump");
reDown = Input.GetButtonDown("Reload");
fDown = Input.GetButton("Fire1");
iDown = Input.GetButtonDown("Interation");
sDown1 = Input.GetButtonDown("Swap1");
sDown2 = Input.GetButtonDown("Swap2");
sDown3 = Input.GetButtonDown("Swap3");
}
void Move()
{
// x y z
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
// 회피 시 업데이트 안되게
if (isDodge)
moveVec = dodgeVec;
// 스왑 및 공격 시 못움직이게
if (isSwap || !isFireReady || isReload)
moveVec = Vector3.zero;
// transform
transform.position += moveVec * (rDown ? 1.3f : 1f) * speed * Time.deltaTime;
// animator
anim.SetBool("isWalk", moveVec != Vector3.zero);
anim.SetBool("isRun", rDown);
}
void Turn()
{
// #1. 키보드에 의한 회전
transform.LookAt(transform.position + moveVec);
// #2. 마우스에 의한 회전
// Ray란?, ScreenPointToRay() : 스크린에서 월드로 Ray를 쏘는 함수
if(fDown)
{
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
// out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point;
nextVec.y = 0;
transform.LookAt(nextVec);
}
}
}
void Jump()
{
if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) {
// 물리엔진에 힘을 준다. 여기선 즉발적인 Impulse
rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
anim.SetBool("isJump", true);
anim.SetTrigger("doJump");
isJump = true;
}
}
void Attack()
{
// 공격할 조건만 플레이어에 두고, 공격로직은 무기에 위임한다.
if (equipWeapon == null)
return;
fireDelay += Time.deltaTime;
isFireReady = equipWeapon.rate < fireDelay;
if(fDown && isFireReady && !isDodge && !isSwap) {
equipWeapon.Use();
anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
fireDelay = 0;
}
}
void Reload()
{
if (equipWeapon == null)
return;
if (equipWeapon.type == Weapon.Type.Melee)
return;
if (ammo == 0)
return;
if(reDown && !isJump && !isDodge && !isSwap && isFireReady)
{
anim.SetTrigger("doReload");
isReload = true;
Invoke("ReloadOut", 2f);
}
}
void ReloadOut()
{
int reAmmo = ammo < equipWeapon.maxAmmo ? ammo : equipWeapon.maxAmmo;
equipWeapon.curAmmo = reAmmo;
ammo -= reAmmo;
isReload = false;
}
void Dodge()
{
// 이동하면서 점프할때,
if (jDown && moveVec != Vector3.zero && !isJump && !isDodge)
{
dodgeVec = moveVec;
speed *= 2;
anim.SetTrigger("doDodge");
isDodge = true;
Invoke("DodgeOut", 0.5f); // 시간지연 라이브러리 기능
}
}
void DodgeOut()
{
speed *= 0.5f;
isDodge = false;
}
void Swap()
{
if (sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))
return;
if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))
return;
if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))
return;
int weaponIndex = -1;
if (sDown1) weaponIndex = 0;
if (sDown2) weaponIndex = 1;
if (sDown3) weaponIndex = 2;
// 1, 2 ,3 버튼 누를때
if((sDown1 || sDown2 || sDown3) && !isJump && !isDodge) {
if(equipWeapon != null)
equipWeapon.gameObject.SetActive(false);
equipWeaponIndex = weaponIndex;
equipWeapon = weapons[weaponIndex].GetComponent<Weapon>();
equipWeapon.gameObject.SetActive(true);
anim.SetTrigger("doSwap");
// 스왑 중
isSwap = true;
Invoke("SwapOut", 0.4f);
}
}
void SwapOut()
{
isSwap = false;
}
void Interation()
{
if(iDown && nearObject != null && !isJump && !isDodge) {
if(nearObject.tag == "Weapon") {
Item item = nearObject.GetComponent<Item>();
int weaponIndex = item.value;
hasWeapons[weaponIndex] = true;
Destroy(nearObject);
}
}
}
// 충돌 시 이벤트 함수로 착지 구현
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.tag == "Floor"){
isJump = false;
anim.SetBool("isJump", false);
}
}
// 아이템 획득
private void OnTriggerEnter(Collider other)
{
if(other.tag == "Item") {
Item item = other.GetComponent<Item>();
switch (item.type){
case Item.Type.Ammo:
ammo += item.value;
if (ammo > maxAmmo)
ammo = maxAmmo;
break;
case Item.Type.Coin:
coin += item.value;
if (coin > maxCoin)
coin = maxCoin;
break;
case Item.Type.Heart:
health += item.value;
if (health > maxHealth)
health = maxHealth;
break;
case Item.Type.Grenade:
if (hasGrenades == maxHasGrenades)
return;
grenades[hasGrenades].SetActive(true);
hasGrenades += item.value;
break;
}
Destroy(other.gameObject);
}
}
private void OnTriggerStay(Collider other)
{
if (other.tag == "Weapon")
nearObject = other.gameObject;
}
private void OnTriggerExit(Collider other)
{
if (other.tag == "Weapon")
nearObject = null;
}
}
🧷 1. 총알 및 탄피 만들기
- 총알 , 탄피 프리팹 만들기
- Hierachy 공간에 creaty empty를 하여, 핸드건 총알, 서브머신건 총알을 명명 해줍시다.
- 에셋에 들어있는 Bullet Case도 사용해 우리들만의 탄창 프리팹으로도 만들 예정이기에 총알과 같이 꾸며봅시다.
- 이후에 Rigidbody, Collider을 총알들, 탄창에 맞게 넣어줍니다 또한 각 총알들에게만 Trail Renderer을 넣어 총알이 나갈 때 효과를 주었습니다.
- Trail Renderer: 객체를 따라가는 이펙트효과로 사용할 것입니다.
- 각자 원하는 효과대로 Width, Color, Time 등등 설정해줍시다.
- 그러면 총알이 나갈 때, 잔상처럼 이펙트가 이쁘게 나갑니다.
- 이후에 Bullet 스크립트까지 만들어 총알들, 탄피에 넣어줍시다.
/* bullet script */
public class Bullet : MonoBehaviour
{
public int damage;
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.tag == "Floor") {
Destroy(gameObject, 3);
}
else if(collision.gameObject.tag == "Wall") {
Destroy(gameObject);
}
}
}
- bullet 스크립트는, 공격력과, 벽에 부딪히거나, 바닥에 닿으면 사라지도록 설정해둡니다.
- Destroy(객체, 사라지기까지 시간)
- 이후에 나만의 프리팹에 끌여당겨 저장해줍시다.
- 총알 및 탄피 위치 잡기
- 총알이 나가는 위치를 잡기위해 Player아래에 Empty를 만든 후 위치를 잡아줍니다.
- 마찬가지로 탄피가 나가는 위치를 각 총에 맞게 설정해줍니다.
🧷 2. 총 쏘기 기능 구현
- Weapon 스크립트에 Shot 기능 추가
/* Weapon script , 추가되는 부분만 넣었습니다. */
public class Weapon : MonoBehaviour
{
public Transform bulletPos;
public GameObject bullet;
public Transform bulletCasePos;
public GameObject bulletCase;
public int maxAmmo;
public int curAmmo;
public void Use()
{
if(type == Type.Melee) {
StopCoroutine("Swing");
StartCoroutine("Swing");
}
else if(type == Type.Range && curAmmo > 0) {
curAmmo--;
StartCoroutine("Shot");
}
}
IEnumerator Shot()
{
// 1. 총알 발사 , Instantiate() 함수로 총알 인스턴스화 하기
GameObject instantBullet = Instantiate(bullet, bulletPos.position, bulletPos.rotation);
Rigidbody bulletRigid = instantBullet.GetComponent<Rigidbody>();
bulletRigid.velocity = bulletPos.forward * 50; // velocity 속력주기
yield return null;
// 2. 탄피 배출
GameObject instantCase = Instantiate(bulletCase, bulletCasePos.position, bulletCasePos.rotation);
Rigidbody CaseRigid = instantCase.GetComponent<Rigidbody>();
Vector3 caseVec = bulletCasePos.forward * Random.Range(-3, -2) + Vector3.up * Random.Range(2, 3);
CaseRigid.AddForce(caseVec, ForceMode.Impulse);
CaseRigid.AddTorque(Vector3.up * 10, ForceMode.Impulse); // 탄피 회전
}
}
- 총알과, 총알 포지션, 탄피, 탄피 포지션을 담을 오브젝트를 변수로 추가합니다.
- 또한, 각 총의 장전된 총알, 최대 장전총알까지 변수로 추가합니다.
- 이후 Use() 함수에서 근접무기때처럼, 원거리공격시 코루틴으로 Shot() 함수를 추가합니다. 이때, 총알이 한개 나가면 StopCoruoutine을 해줄 필요가 없기에 제외했습니다. 그리고 무조건 코루틴을 사용할 때 yield 를 넣어야합니다.
- Shot() 함수에서는 인스턴트총알 = (총알, 총알포지션값, 총알회전값)을 주어 인스턴트화 시켰습니다.
- instantBullet의 Rigidbody값도 변수에 저장합니다.
- 유니티를 보고 총알이 나가는 위치는 z축 방향으로 앞을 바라보고 있으니 인스턴트 총알의 Rigidbody값에 velocity를 bulletPos.forward 벡터값을 준 뒤, 원하는 총알 속도를 넣어 총알의 속도 값을 넣어줍니다.
- 이 후, 탄피 배출에도 인스턴트 값, 탄피가 배출되는 랜덤값, 회전값등을 주었습니다.
- 스크립트가 완성 되었으니, 플레이어가 오른손에 들고있는 총들의 Weapon 스크립트 값에 Bullet, Bullet Pos, Bullet Case Pos, Bullet Case 오브젝트를 연결시켜 주었습니다.
- Player 스크립트에 애니메이션 추가
/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
void Attack()
{
// 공격할 조건만 플레이어에 두고, 공격로직은 무기에 위임한다.
if (equipWeapon == null)
return;
fireDelay += Time.deltaTime;
isFireReady = equipWeapon.rate < fireDelay;
if(fDown && isFireReady && !isDodge && !isSwap) {
equipWeapon.Use();
anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
fireDelay = 0;
}
}
}
}
- 플레이어 스크립트에서 현재 장착하고 있는 무기에 따른 애니메이션 트리거를 삼항 연산자로 설정해줍시다.
- 애니메이션 컨트롤러에서도, doShot 트리거와 함께 에셋에서 Shot 모션도 넣어 설정해줍니다.
🧷 3. 재장전 기능 구현
- 재장전 변수 추가
/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
bool reDown;
bool isReload;
void GetInput()
{
reDown = Input.GetButtonDown("Reload");
}
}
- Input Manager 에서 Reload 값을 r key로 설정하고 스크립트에 넣어주었습니다.
- 또한 isReload를 통해 리로드 중인지 확인할 변수도 넣어줍시다.
- 재장전 기능 추가
/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
void Reload()
{
if (equipWeapon == null)
return;
if (equipWeapon.type == Weapon.Type.Melee)
return;
if (ammo == 0)
return;
if(reDown && !isJump && !isDodge && !isSwap && isFireReady)
{
anim.SetTrigger("doReload");
isReload = true;
Invoke("ReloadOut", 2f);
}
}
void ReloadOut()
{
int reAmmo = ammo < equipWeapon.maxAmmo ? ammo : equipWeapon.maxAmmo;
equipWeapon.curAmmo = reAmmo;
ammo -= reAmmo;
isReload = false;
}
}
- Reload() 함수를 만들어, 재장전시 조건(장착하고 있는 무기가 있고, 타입이 근접무기가 아니고, 총알이 가지고있는 재장전 총알이 0이 아닐때) 을 추가하였습니다.
- 또한 조건을 통과 한 후, reload 키를 누를 시, 애니메이션 트리거가 실행되고, 리로드 중이라는 isReload = true 값이 됩니다.
- 이 후 재장전 속도를 주기 위해 Invoke()와, ReloadOut()함수를 사용해 2초의 시간 후 ReloadOut() 함수가 실행 되도록 설정 하였습니다.
- ReloadOut 에서는 재장전시 탄창의 수를 재조정하는 코드를 작성했습니다.
++ 재장전 연타시, 탄창이 자꾸 갈아끼워지는 버그를 고칠 예정이며, 남아있는 총알의 개수는 재장전시 탄창에서 빼줄 예정입니다.
- 재장전 애니메이션 추가
- 애니메이션 컨트롤러에서 doReload트리거를 만들고 모션을 추가하여 연결시켜 주었습니다.
🧷 4. 마우스 방향으로 총 쏘기
거의 다 왔습니다!!. 이제는 마우스 클릭에 따라서 총알이 나가게 설정 할 기능을 추가합니다.
/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
public Camera followCamera;
void Turn()
{
// #1. 키보드에 의한 회전
transform.LookAt(transform.position + moveVec);
// #2. 마우스에 의한 회전
// Ray란?, ScreenPointToRay() : 스크린에서 월드로 Ray를 쏘는 함수
if(fDown)
{
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
// out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point;
nextVec.y = 0;
transform.LookAt(nextVec);
}
}
}
}
- 카메라 변수를 가져와, 실제 우리 카메라 오브젝트를 끌여당겨 넣어줍시다.
- Turn() 함수에 총 공격시, Ray 함수를 쓸껀데요, 이 ray를 만들어 RaycastHIt 클래스의 rayHit에 저장 합니다
- rayHit.point 값을 통해 플레이어의 방향을 조절합니다.
- nextVec.y = 0 을 두어 x,z축의 값에만 반응하도록 만들고
- transform.LookAt(nextVec) 값으로 플레이어의 회전을 돕습니다.
++ Ray 함수는 따로 다뤄 보도록 하겠습니다.
++ 2022.04.03 - [Game Dev/개발지식] - [Unity] 레이(Ray), 레이 캐스트(Raycast)에 대해
출처: 골든메탈님 유튜브
https://www.youtube.com/watch?v=07q9RUTRq4M&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy&index=7
'🎮 Game Dev (게임개발) > PC (데스크탑, 노트북, 터치패널)' 카테고리의 다른 글
[3D 액션게임] 08. 플레이어 물리엔진 고치기 (0) | 2022.04.04 |
---|---|
[3D 액션게임] 07-1. 원거리 무기 재장전시 모션캔슬 (0) | 2022.04.04 |
[3D 액션게임] 06. 근접무기 공격구현 (0) | 2022.04.01 |
[3D 액션게임] 05.아이템 획득과 공전효과 (0) | 2022.03.31 |
[3D 액션게임] 04.무기 획득과 변경 (0) | 2022.03.30 |
Contents
소중한 공감 감사합니다