https://www.youtube.com/watch?v=FyJYWRIq0Ss
이번 강의의 플레이어의 마지막 액션 될 수류탄 던지기를 구현해보려 함
수류탄은 필살기 느낌임
아래 에셋에서 수류탄 프리팹을 하이어라키 창으로 드래그해서
수류탄 하나 생성
이름은 Throw Grenade
미리 준비된 파티클 프리팹을 Throw Grenade 아래 자식오브젝트로 등록
(없음 파티클 새로 만들어야 함)
수류탄이 터지는 파티클임
loop 에 체크 해제된 파티클임
파티클에 꼬리를 넣고싶으면 trails 킨 후
아래 렌더러에 trail material 수정
리지드 바디와 스피어 콜라이더 추
통통 튀어야 하니
피직스 마테리알 추
피직스 마테리알을 스피어 콜라이더에 적용
수류탄의 자식 중 메쉬 오브젝트에 trail renderer 추가
throw grenade 부모 오브젝트를 layer 설정
playerbullet, yes change children 클릭
다 만들엇음
throw grenade를 플팹으로 저장
프리팹 등록후 position x y z 0 0 0 로 설정
프리팹 등록했으면 하이어라키 창에서 지움
public GameObject grenadeObj;
플레이어 스크립트 위쪽에 저 문구 추
프리팹으로 만든 수류탄을
인스펙터 창에서 지정
bool gDown; // 수류탄키
수류탄 버튼을 위한 키보드 입력 만들거임
void GetInput()
{
gDown = Input.GetButtonDown("Fire2");
}
void Update()
{
Grenade();
}
edit - project settings - input manager - fire2 키는 mouse 1 , 마우스 우클릭임
이 키가 수류탄 던지기가 될 고임
GameObject instantGrenade
= Instantiate (grenadeObj, transform.position,
transform.rotation)
instantiate 뜻 :
오브젝트를 파티클처럼
임시로 하나 생성시킨 후 제거하겠다는 뜻
void Grenade()
{
if (hasGrenades == 0)
return; // 수류탄 개수가 0개면 발동 안됨
if (gDown &&
!isReload && !isSwap)
{
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point - transform.position;
nextVec.y = 7;
GameObject instantGrenade
= Instantiate(grenadeObj, transform.position,
transform.rotation);
Rigidbody rigidGrenade =instantGrenade.GetComponent<Rigidbody>();
rigidGrenade.AddForce(nextVec, ForceMode.Impulse);
rigidGrenade.AddTorque
(Vector3.back * 10, ForceMode.Impulse);
hasGrenades --;
//인덱스가 유효한지 확인
if (hasGrenades >= 0 && hasGrenades < grenades.Length) { grenades[hasGrenades].SetActive(false); }
}
}
}
수류탄 투척을 스크립트화 시킨 것
이건 플레이어 스크립트 내부에 적은 거임
hasGrenades --;
grenades[hasGrenades].SetActive(false);
강의에선 위 처럼 하라 했지만, 빨간 버그걸림
2 hasGrenades --;
//인덱스가 유효한지 확인
if (hasGrenades >= 0 && hasGrenades < grenades.Length)
{ grenades[hasGrenades].SetActive(false); }
챗봇에게 수정하라 했더니 이렇게 수정함
수류탄 프리팹 - open - 자식 오브젝트 중 터지는 파티클은 미리 비활성화 시키기
Grenade 라는 이름의 새 스크립트 생성
throw grenade 프리팹에 스크립트 적용
인스펙터 변수 세 개 생성
Mesh Obj 는 자식 메쉬 를
Effect Obj 는 자식 폭발 파티클 을
Rigid 는 부모 오브젝트 를 지정
rigid.Sleep(); / /rigid.velocity=Vector3.zero; 대체코드
rigid velocity vector3 zero 코드는 취소선이 생기는 오류가 있으니
rigid sleep 을 사용함
+
공 모양의 범위 레이 케스트를 활용하여 범위 공격을 구현
SphereCast
SphereCastAll
가 있는데 ,
SphereCast 하나만
SphereCastAll 범위내 모두 라는 뜻
RaycastHit[] rayHits = Physics.SphereCastAll
(transform.position, 15,
Vector3.up, 0f, LayerMask.GetMask("Enemy"));
RaycastHit[] rayHits = Physics.SphereCastAll
(시작 위치, 반지름크기,
쏘는방향 여기선 크게 상관없음, 범위 오프셋 거리, 레이어 한정 마스크);
레이 케스트 피직스 엔 5가지 항목이 차례로 있음
시작 위치, 반지름 크기, 쏘는 방향, 범위 오프셋, 물리판정 받는 레이어 지정
public void HitByGrenade(Vector3 explosionPos)
{
curHealth -= 100;
Vector3 reactVec = transform.position - explosionPos; // 오브젝트 위치와 폭발위치 고려한 반응 계산
StartCoroutine (OnDamage(reactVec));
}
Enemy 스크립트에 위 문구 추가함
충돌시 뱅그르르 돌아가는 게 어색해서 프리즈 x , z 축 했었음
이걸 풀려면
rigid.freezeRotation = false;
스크립트에서 이렇게 써주면 x y z 프리즈 로테이션이 풀림
Destroy(gameObject, 5);
5초 후 자기 자신을 삭제하겠다는 뜻
수류탄은 후폭발 파티클 이펙트 시간도 기다려야 해서 5 초를 설정함
폭발하며 범위 공격을 가하는 수류탄 구현 완료
지금까지
Grenade 스크립트 풀 :
public class Grenade : MonoBehaviour
{
public GameObject meshObj;
public GameObject effectObj;
public Rigidbody rigid;
void Start()
{
StartCoroutine(Explosion());
}
IEnumerator Explosion()
{
yield return new WaitForSeconds(1f);
rigid.Sleep(); //rigid.velocity=Vector3.zero; 대체코드
rigid.angularVelocity = Vector3.zero;
meshObj.SetActive(false);
effectObj.SetActive(true);
RaycastHit[] rayHits = Physics.SphereCastAll
(transform.position, 3,
Vector3.up, 0f, LayerMask.GetMask("Enemy"));
foreach (RaycastHit hitObj in rayHits)
{
hitObj.transform.GetComponent<Enemy>().HitByGrenade(transform.position);
}
Destroy(gameObject, 5);
}
}
Enemy 스크립트 풀 :
public class Enemy : MonoBehaviour
{
public int maxHealth;
public int curHealth;
Material mat;
Rigidbody rigid;
BoxCollider boxCollider;
void Awake()
{
rigid = GetComponent<Rigidbody>();
boxCollider = GetComponent<BoxCollider>();
mat = GetComponent<MeshRenderer>().material;
}
void OnTriggerEnter(Collider other)
{
if(other.tag == "Melee")
{
Weapon weapon = other.GetComponent<Weapon>();
curHealth -= weapon.damage;
Vector3 reactVec = transform.position
- other.transform.position;
StartCoroutine(OnDamage(reactVec, false));
}
else if (other.tag == "Bullet")
{
Bullet bullet = other.GetComponent<Bullet>();
curHealth -= bullet.damage;
Vector3 reactVec = transform.position
- other.transform.position;
Destroy(other.gameObject);
StartCoroutine(OnDamage(reactVec, false));
}
}
public void HitByGrenade(Vector3 explosionPos)
{
curHealth -= 100;
Vector3 reactVec = transform.position - explosionPos;
StartCoroutine (OnDamage(reactVec, true));
}
IEnumerator OnDamage(Vector3 reactVec,
bool isGrenade)
{
mat.color = Color.red;
yield return new WaitForSeconds(0.1f);
if (curHealth > 0)
{ mat.color = Color.white; }
else
{
mat.color = Color.gray;
gameObject.layer = 12;
if (isGrenade)
{
reactVec = reactVec.normalized;
reactVec += Vector3.up * 3 ;
rigid.freezeRotation = false;
rigid.AddForce(reactVec * 5, ForceMode.Impulse);
rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
}
else
{
reactVec = reactVec.normalized;
reactVec += Vector3.up;
rigid.AddForce(reactVec * 5, ForceMode.Impulse);
}
Destroy(gameObject, 2);
}
}
}
플레이어 스크립트 풀 :
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering;
public class PlayerSC : MonoBehaviour
{
public float speed;
public GameObject[] weapons;
public bool[] hasWeapons;
public GameObject[] grenades;
public int hasGrenades;
public GameObject grenadeObj;
public Camera followCamera;
public int ammo;
public int coin;
public int health;
public int maxAmmo;
public int maxCoin;
public int maxHealth;
public int maxHasGrenades;
float hAxis;
float vAxis;
bool wDown; // Walk를 위한 shift 꾹
bool jDown;
bool fDown; // 공격키
bool gDown; // 수류탄키
bool rDown; // 원거리 무기 재장전
bool iDown;
bool sDown1;
bool sDown2;
bool sDown3;
bool isJump;
bool isDodge;
bool isSwap;
bool isReload;
bool isFireReady = true;
bool isBorder;
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>();
}
void Update()
{
GetInput();
Move();
Turn();
Jump();
Grenade();
Attack();
Reload();
Dodge();
Swap();
Interaction(); // 키보드 e 키 눌르면 무기 입수
}
void FreezeRotation()
{
rigid.angularVelocity = Vector3.zero;
}
void StopToWall()
{
Debug.DrawRay(transform.position, transform.forward*5,
Color.green);
isBorder = Physics.Raycast(transform.position,
transform.forward, 1, LayerMask.GetMask("Wall"));
}
void FixedUpdate()
{
FreezeRotation();
Move();
StopToWall();
}
void GetInput()
{
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
wDown = Input.GetButton("Walk"); // GetButton은 꾹 누름
jDown = Input.GetButtonDown("Jump");
fDown = Input.GetButton("Fire1");
gDown = Input.GetButtonDown("Fire2");
rDown = Input.GetButtonDown("Reload");
iDown = Input.GetButtonDown("Interaction");
sDown1 = Input.GetButtonDown("Swap1");
sDown2 = Input.GetButtonDown("Swap2");
sDown3 = Input.GetButtonDown("Swap3");
}
void Move()
{
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
if (isDodge)
moveVec = dodgeVec;
if (isSwap || isReload || !isFireReady)
moveVec = Vector3.zero;
if (!isBorder)
transform.position += moveVec
* speed * (wDown ? 0.2f : 1.0f) * Time.deltaTime;
// Walk 걸을 때 속도 감소
anim.SetBool("isRun", moveVec != Vector3.zero);
anim.SetBool("isWalk", wDown);
}
void Turn()
{
// 키보드에 의한 회전 구현
transform.LookAt(transform.position + moveVec);
// 마우스에 의한 회전 구현
if (fDown)
{
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point - transform.position;
nextVec.y = 0;
transform.LookAt(transform.position + nextVec);
}
}
}
void Jump()
{
// 제자리에서 점프는 점프
if (jDown
&& moveVec == Vector3.zero
&& !isJump && !isDodge && !isSwap)
{
rigid.AddForce(Vector3.up * 7, ForceMode.Impulse);
anim.SetBool("isJump", true);
anim.SetTrigger("doJump");
isJump = true;
}
}
void Grenade()
{
if (hasGrenades == 0)
return; // 수류탄 개수가 0개면 발동 안됨
if (gDown &&
!isReload && !isSwap)
{
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point - transform.position;
nextVec.y = 7;
GameObject instantGrenade
= Instantiate(grenadeObj, transform.position,
transform.rotation);
Rigidbody rigidGrenade =instantGrenade.GetComponent<Rigidbody>();
rigidGrenade.AddForce(nextVec, ForceMode.Impulse);
rigidGrenade.AddTorque
(Vector3.back * 10, ForceMode.Impulse);
hasGrenades --;
//인덱스가 유효한지 확인
if (hasGrenades >= 0 && hasGrenades < grenades.Length)
{ grenades[hasGrenades].SetActive(false); }
}
}
}
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(rDown && isFireReady &&
!isJump && !isDodge && !isSwap )
{
anim.SetTrigger("doReload");
isReload = true;
Invoke("ReloadOut", 0.5f);
}
}
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 && !isSwap)
{
dodgeVec = moveVec;
speed *= 1.5f;
anim.SetTrigger("doDodge");
isDodge = true;
Invoke("DodgeOut", 0.5f);
}
}
void DodgeOut()
{
speed /= 1.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;
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 Interaction()
{
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")
{
anim.SetBool("isJump", false);
isJump = false;
}
}
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)
grenades[hasGrenades].SetActive(true);
hasGrenades += item.value;
if (hasGrenades == maxHasGrenades)
return;
break;
}
Destroy(other.gameObject);
}
}
void OnTriggerStay(Collider other)
{
if (other.tag == "Weapon")
nearObject = other.gameObject;
}
void OnTriggerExit(Collider other)
{
if (other.tag == "Weapon")
nearObject = null;
}
}