지난번에 이어 게임을 만들어보자.
이번에 참고할 강의는 아래 강의이다.
https://www.youtube.com/watch?v=PAKYDX9gPNQ&list=PLFt_AvWsXl0ctd4dgE1F8g3uec4zKNRV0&index=16
이번 강의에서는 적 사망 시 이펙트를 추가해본다.
시작해보자.
적을 쓰러뜨렸을 때 적에게서 파티클이 나오도록 해보자.
적 프리팹을 적당한 위치에 생성하고 파티클 시스템을 만들어 적 위치에 놓자.
이제 인스펙터에서 렌더러 탭으로 이동해
렌더링 모드 - 메시, 머터리얼 - Enemy 로 변경해준다.
위와 같이 적(Enemy) 머터리얼의 큐브 형태로 파티클이 생성된다.
이제 시작 크기를 설정해 파티클 크기를 조절해주고
수명에 걸친 크기 탭에 크기 부분을 클릭하고 에디터 열기로 커브 형태를 바꿔준다.
이렇게 하고나면 이제 파티클이 시간이 가면 작아지다 없어진다.
중력과 시작 속도도 설정해주자.
그리고 충돌도 World 로 설정해 파티클이 충돌할 수 있도록 설정하는데,
충돌을 원하는 부분은 장애물과 바닥 두곳이다.
따라서 두 레이어 Floor, Obstacle 을 추가로 생성해주고
Map 오브젝트로 가서 Floor 레이어를 설정,
이건 아니요로 하자.
똑같이 Obstacle 프리팹으로 가서 Obstacle 레이어를 적용해주자
맵을 한번 다시 생성해주고, 파티클 시스템의 충돌 탭으로 가서
충돌 대상을 Notinhg 으로 설정한 다음 다시 Floor, Obstacle 로 설정해주자.
그리고 감속을 0.75 정도로 설정해 다른 오브젝트에 충돌 후 속도가 줄어들도록 했다.
이제 모양 탭으로 가서 각도와 반지름을 설정해주고,
이제 지속시간, 시작 수명을 설정하고
이미션에서 시간당 방출량과 폭발을 설정해 파티클이 한번에 몇 개가 나올지를 설정한다.
이렇게 설정하고 나면 대략 이런 파티클이 생성된다.
마치 피를 뿜는...
이제 파티클 시스템 이름을 Enemy Death Effect 로 설정하고
프리팹으로 만든 후 Enemy 오브젝트와 파티클을 씬에서 삭제하자.
이제 Enemy, LivingEntity, IDamageable, Projectile 스크립트를 수정해
적이 죽을 때 그 위치에서 파티클이 생성되도록 해보자.
(지금까지 IDamaneable 로 오타나있어서 수정했다..)
이전부터 사용하던 TakeHit 메소드를 수정해 타격 위치를 가져오도록 하고,
Enemy 스크립트에서 override 가능하게 해 사망 시 해당 위치에 파티클이 생성되도록 한다.
| IDamageable 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IDamageable // 인터페이스 생성.
{
void TakeHit(float damage, Vector3 hitPoint, Vector3 hitDirection); // 타격을 받는 메소드
void TakeDamage(float damage); // 데미지를 받는 메소드
}
| LivingEntity 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LivingEntity : MonoBehaviour, IDamageable // 인터페이스 상속
{
public float startingHealth; // 시작 체력
protected float health; // 체력
/* protected 를 통해 상속 관계가 없는 클래스에서 사용할 수 없게 한다.
인스펙터에서도 보이지 않음. */
protected bool dead; // 캐릭터가 죽었는지
public event System.Action OnDeath;
/* System.Action 은 델리게이트 메소드이다.
여기서 델리게이트란 C++ 의 함수 포인터와 유사하게
메소드의 위치를 가르키고 불러올 수 있는 타입이다.
void 를 리턴하고 입력을 받지 않는다. */
protected virtual void Start()
// 상속 받는 클래스에서 같은 메소드를 재정의 할 수 있도록 virtual 로 선언.
{
health = startingHealth; // 시작 체력 설정
}
// ■ 타격 메소드
public virtual void TakeHit(float damage, Vector3 hitPoint, Vector3 hitDirection) {
TakeDamage(damage); // 데미지 받는 메소드 호출
}
// ■ 데미지 받는 메소드
public virtual void TakeDamage(float damage) {
health -= damage; // 체력에서 데미지만큼 감소
if (health <= 0 && !dead) // 체력이 0 이하고 죽은 경우
{
Die(); // 죽음 메소드 호출
}
}
[ContextMenu("Self-Destruct")]
// 인스펙터에서 스크립트 컴포넌트 우클릭 시 자체 파괴 버튼 추가
protected void Die() // 죽음 메소드
{
dead = true; // 죽은 판정으로 변경.
if(OnDeath != null) // 이벤트가 있는 경우
{
OnDeath(); // 메소드를 호출
}
GameObject.Destroy(gameObject); // 현재 오브젝트 파괴
}
}
| Enemy 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(UnityEngine.AI.NavMeshAgent))]
// 현재 오브젝트에 NavMeshAgent 를 포함
public class Enemy : LivingEntity
{
public enum State { Idle, Cashing, Attacking };
// 적의 상태, { 기본(아무것도 안함), 추적, 공격 }
State currentState; // 현재 상태
public ParticleSystem deathEffect; // 사망 이펙트 레퍼런스
UnityEngine.AI.NavMeshAgent pathfinder; // 내비게이션 레퍼런스
Transform target; // 적의 타겟(플레이어) 트랜스폼
LivingEntity targetEntity; // 타겟(플레이어) 레퍼런스
Material skinMatreial; // 현재 오브젝트의 마테리얼
Color originalColor; // 현재 오브젝트의 기존 색상
float attackDistanceThreshold = 0.5f;
// 공격 거리 임계값(사거리). 유니티에서 단위 1은 1 meter 이다..!!
float timeBetweenAttacks = 1;
// 공격 시간 간격
float nextAttackTime;
// 실제 다음 공격 시간
float damage = 1;
// 적의 공격 데미지
float myCollisionRadius; // 자신의 충돌 범위 반지름
float targetConllisionRadius; // 타겟의 충돌 범위 반지름
bool hasTarget; // 타겟이 있는지 확인
protected override void Start()
// override 로 부모 클래스의 메소드를 재정의.
{
base.Start(); // base 를 통해 부모 클래스의 기존 메소드를 호출.
pathfinder = GetComponent<UnityEngine.AI.NavMeshAgent>();
// NavMeshAgent 레퍼런스 생성
skinMatreial = GetComponent<Renderer>().material; // 현재 오브젝트의 마테리얼 저장.
originalColor = skinMatreial.color; // 현재 색상 저장
if(GameObject.FindGameObjectWithTag("Player") != null)
// 적 스폰 후 플레이어 오브젝트가 있는(플레이어가 살아있는) 경우
{
currentState = State.Cashing; // 기본 상태를 추적 상태로 설정
hasTarget = true; // 타겟(플레이어) 있음으로 설정
target = GameObject.FindGameObjectWithTag("Player").transform;
// 타겟으로 "Player" 라는 태그를 가진 오브젝트의 트랜스폼을 저장
targetEntity = target.gameObject.GetComponent<LivingEntity>();
// 위에서 저장 한 플레이어 오브젝트의 LivingEntity 컴포넌트를 저장
targetEntity.OnDeath += OnTargetDeath;
// 타겟 사망 메소드 추가.
myCollisionRadius = GetComponent<CapsuleCollider>().radius;
// 자신의 충돌체(collider)의 반지름을 저장
targetConllisionRadius = target.GetComponent<CapsuleCollider>().radius;
// 타겟의 충돌체(collider)의 반지름을 저장
StartCoroutine(UpdatePath());
// 지정된 시간마다 목적지 쪽으로 위치를 갱신하는 코루틴 실행
}
}
// ■ 타격 메소드 오버라이딩
public override void TakeHit(float damage, Vector3 hitPoint, Vector3 hitDirection) {
if(damage >= health) { // 데미지가 현재 체력 이상인 경우
Destroy(Instantiate(deathEffect.gameObject, hitPoint, Quaternion.FromToRotation(Vector3.forward, hitDirection)) as GameObject, deathEffect.startLifetime);
// 이펙트(파티클)을 인스턴스화 하여 생성(FromToRotation 으로 방향 설정), 설정한 시간경과 후 파괴
}
base.TakeHit(damage, hitPoint, hitDirection);
// 부모 클래스의 기존 TakeHit 메소드 호출
}
void OnTargetDeath() // 타겟(플레이어)이 죽었을 때 호출되는 메소드
{
hasTarget = false; // 타겟 없음으로 설정
currentState = State.Idle; // 현재 상태를 기본(정지)상태로 변경
}
void Update()
{
if (hasTarget) // 타겟(플레이어)가 있는 경우
{
if (Time.time >= nextAttackTime) // 공격 시간이 지나면
{
float sqrDstToTarget = (target.position - transform.position).sqrMagnitude;
/* 자신(적)과 목표(플레이어) 사이의 거리를 저장.
두 오브젝트 사이의 거리를 측정하기 위해 Vector3 의 Distace 메소드를
쓰는 방법도 있지만 벡터에 제곱 연산이 들어가다 보니 연산 수가 많다.
두 오브젝트 사이의 실제 거리가 필요한 것이 아닌 비교값이 필요 한
것이므로 위와 같이 연산을 줄여 볼 수 있다. */
if (sqrDstToTarget <= Mathf.Pow(attackDistanceThreshold + targetConllisionRadius + myCollisionRadius, 2))
// 거리의 제곱과 임계 거리의 제곱을 비교하여 공격 범위 내인지 확인
{
nextAttackTime = Time.time + timeBetweenAttacks;
// 다음 공격 시간 지정
StartCoroutine(Attack());
// 공격 코루틴 실행
}
}
}
}
IEnumerator Attack() // 적의 공격 코루틴
{
currentState = State.Attacking; // 공격 상태로 변경
pathfinder.enabled = false; // 공격 중 플레이어 추적 중지
Vector3 originalPosition = transform.position; // 자신(적)의 위치
Vector3 dirToTarget = (target.position - transform.position).normalized;
// 타겟으로의 방향 벡터 계산
Vector3 attackPosition = target.position - dirToTarget * (myCollisionRadius);
// 타겟(플레이어)의 위치
float attackSpeed = 3; // 공격 속도
float percent = 0;
skinMatreial.color = Color.magenta; // 공격 시 색상 지정
bool hasAppliedDamage = false; // 데미지를 적용 중인지 확인
while(percent <= 1) // 찌르는 거리가 1 이하일 때 루프
{
if(percent >= .5f && hasAppliedDamage == false)
// 적이 공격 지점에 도달했고 데미지를 적용중이지 않은 경우
{
hasAppliedDamage = true; // 데미지 적용 중으로 설정
targetEntity.TakeDamage(damage); // 타겟(플레이어)에게 데미지 적용
}
percent += Time.deltaTime * attackSpeed;
float interpolation = (-Mathf.Pow(percent, 2) + percent) * 4;
/* 대칭 함수를 사용
원지점->공격지점 이동에서 보간 값을 참조한다. */
transform.position = Vector3.Lerp(originalPosition, attackPosition, interpolation);
/* Lerp 메소드는 내분점을 반환한다.
원점, 공격지점, 보간 값을 참조값으로 전달한다
보간 값이 0이면 원점에, 1이면 공격지점에 있게 된다. */
yield return null;
/* while 루프의 처리 사이에서 프레임을 스킵합니다.
이 지점에서 작업이 멈추고 Update 메소드 작업이 끝나고
다음 프레임으로 넘어가면 이 아래의 코드나 루프가 실행 */
}
skinMatreial.color = originalColor; // 공격이 끝나면 기존 색상으로 변경
currentState = State.Cashing; // 공격이 끝나면 추적 상태로 변경
pathfinder.enabled = true; // 공격이 끝나면 다시 플레이어 추적
}
IEnumerator UpdatePath()
// 해당 코루틴 실행 시 지정한 시간마다 루프문 내부 코드 반복
{
float refreshRate = .25f; // 루프 시간
while(hasTarget) // 타겟이 있을 때 반복
{
if(currentState == State.Cashing)
// 상태가 추적 상태인 경우
{
Vector3 dirToTarget = (target.position - transform.position).normalized;
// 타겟으로의 방향 벡터 계산
Vector3 targetPosition = target.position - dirToTarget * (myCollisionRadius + targetConllisionRadius + attackDistanceThreshold / 2);
/* 자신의 위치에서 자신과 타겟의 반지름 길이에 공격 사거리의 절반을 더하고, 방향 벡터를 곱한 값을 뺀다.
즉, 타겟(플레이어) 공격 가능한 위치를 타겟 위치로 정한다. */
if (!dead) // 죽지 않은 경우
{
pathfinder.SetDestination(targetPosition);
// 내비게이션의 목적지를 타겟(플레이어)의 위치로 설정
}
}
yield return new WaitForSeconds(refreshRate);
// refreshRate 시간만큼 기다림
}
}
}
| Projectile 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
public LayerMask collisionMask;
// 어떤 오브젝트, 레이어가 발사체(총알)과 충돌할지 저장.
float speed = 10.0f; // 기본 속도
float damage = 1; // 데미지
float lifeTime = 3; // 총알 유지 시간
float skinWidth = .1f; // 레이 거리 보정값
void Start()
{
Destroy(gameObject, lifeTime); // 총알 유지 시간이 지났다면 현재 오브젝트(총알)을 제거.
Collider[] initialCollisions = Physics.OverlapSphere(transform.position, 0.1f, collisionMask);
// 현재 오브젝트(총알, 발사체)와 충돌중인 충돌체들의 배열 생성
if(initialCollisions.Length > 0)
// 위 배열의 길이가 0 보다 큰 경우, 즉 충돌체가 하나라도 있는 경우
{
OnHitObject(initialCollisions[0], transform.position);
// 충돌 처리 메소드를 호출하고 첫 번째 충돌체를 전달.
}
}
public void SetSpeed(float newSpeed) // 속도 설정 메소드
{
speed = newSpeed; // 입력받은 속도로 설정
}
void Update()
{
float moveDistance = speed * Time.deltaTime; // 총알의 이동 거리
CheckCollision(moveDistance); // 총알 충돌 확인 메소드 호출
transform.Translate(Vector3.forward * Time.deltaTime * speed);
// 총알 이동
}
void CheckCollision(float moveDistance) // 총알 충돌 확인 메소드
{
Ray ray = new Ray(transform.position, transform.forward);
// 레이 생성, 발사체(총알)위치와 정면 방향을 전달.
RaycastHit hit; // 충돌 오브젝트 반환 정보
if (Physics.Raycast(ray, out hit, moveDistance + skinWidth, collisionMask, QueryTriggerInteraction.Collide))
/* 레이캐스트로 발사체가 오브젝트나 레이어에 닿았는지 확인.
QueryTriggerInteraction 을 Collide 로 하여 트리거 콜라이더와 충돌 하게 설정. */
{
OnHitObject(hit.collider, hit.point); // 충돌 처리 메소드 호출
}
}
void OnHitObject(Collider c, Vector3 hitPoint) // 충돌 시 처리 메소드
{
IDamageable damageableObject = c.GetComponent<IDamageable>();
// 인터페이스 변수 생성, 발사체에 맞은 오브젝트의 인터페이스를 가져와 저장.
if (damageableObject != null)
/* 위 변수가 null 이 아닐 때,
즉 맞은 오브젝트에 인터페이스가 있는, 데미지를 받는 오브젝트인 경우. */
{
damageableObject.TakeHit(damage, hitPoint, transform.forward);
// 해당 오브젝트 인터페이스의 타격 메소드를 호출
}
GameObject.Destroy(gameObject); // 현재 오브젝트(발사체) 제거
}
}
이제 Enemy 프리팹의 Enemy 스크립트의 death Effect 에 만들어둔 Enemy Death Effect 프리팹을 할당한다.
맵 사이즈와 웨이브 별 적 수를 조금 조정하고 실행 해 봤다.
위와 같이 적이 죽으면 파티클이 생성되는 것을 볼 수 있다.
다음 강의에서는 무기 발사 이펙트를 추가해본다.
'Unity > 게임개발' 카테고리의 다른 글
[Unity] 게임 개발 일지 | 탑다운 슈팅 17 (0) | 2023.12.08 |
---|---|
[Unity] 게임 개발 일지 | 탑다운 슈팅 16 (0) | 2023.12.07 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 14 (0) | 2023.12.06 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 13 (2) | 2023.12.05 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 12 (0) | 2023.12.01 |