지난번에 이어 게임을 만들어보자.
이번에 참고할 강의는 아래 강의이다.
https://www.youtube.com/watch?v=v0zVBtZpB-8&list=PLctzObGsrjfyevwpeEVQ9pxGVwZtS7gZK&index=5
이번 강의에서는 데미지 시스템을 만들어본다.
시작해보자.
먼저, 지난 시간에 충돌 시 충돌을 감지하도록 했다.
데미지를 주기 위해서는 이를 오브젝트에 전달해야 한다.
이를 위해 데미지를 받는 메소드를 가지는 인터페이스를 생성,
데미지를 받아야 할 각 오브젝트는 인터페이스를 상속받아
발사체에 맞은 것을 감지하는 메소드를 구현하자.
(클래스가 다중상속이 되지 않기 때문에 다중상속이 가능한 인터페이스를 사용하여
추상화된 멤버를 각 클래스에서 구현하여 사용한다)
인터페이스를 생성하기 위해 스크립트 IDamaneable 을 생성한다.
| IDamaneable 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IDamaneable // 인터페이스 생성.
{
void TakeHit(float damage, RaycastHit hit); // 데미지를 받는 메소드
}
그리고 Projectile 스크립트의 OnHitObject (충돌 처리) 메소드를 수정해준다.
| Projectile 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
public LayerMask collisionMask;
// 어떤 오브젝트, 레이어가 발사체(총알)과 충돌할지 저장.
float speed = 10.0f; // 기본 속도
float damage = 1; // 데미지
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, collisionMask, QueryTriggerInteraction.Collide))
/* 레이캐스트로 발사체가 오브젝트나 레이어에 닿았는지 확인.
QueryTriggerInteraction 을 Collide 로 하여 트리거 콜라이더와 충돌 하게 설정. */
{
OnHitObject(hit); // 충돌 처리 메소드 호출
}
}
void OnHitObject(RaycastHit hit) // 충돌 시 처리 메소드
{
IDamaneable damageableObject = hit.collider.GetComponent<IDamaneable>();
// 인터페이스 변수 생성, 발사체에 맞은 오브젝트의 인터페이스를 가져와 저장.
if(damageableObject != null)
/* 위 변수가 null 이 아닐 때,
즉 맞은 오브젝트에 인터페이스가 있는, 데미지를 받는 오브젝트인 경우. */
{
damageableObject.TakeHit(damage, hit);
// 해당 오브젝트 인터페이스의 데미지 받기 메소드를 호출
}
print(hit.collider.gameObject.name); // 충돌 시 콘솔에 출력
GameObject.Destroy(gameObject); // 현재 오브젝트(발사체) 제거
}
}
데미지를 받는다는 것은 각 캐릭터(플레이어, 적)에 체력 또한 있어야 한다는 것이다.
체력을 관리 할 스크립트 LivingEntity 스크립트를 만들고 작성하자.
(데미지 적용 또한 이 클래스에서 이뤄질 것이다.)
| LivingEntity 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LivingEntity : MonoBehaviour, IDamaneable // 인터페이스 상속
{
public float startingHealth; // 시작 체력
protected float health; // 체력
/* protected 를 통해 상속 관계가 없는 클래스에서 사용할 수 없게 한다.
인스펙터에서도 보이지 않음. */
protected bool dead; // 캐릭터가 죽었는지
protected virtual void Start()
// 상속 받는 클래스의 같은 메소드를 재정의 할 수 있도록 virtual 로 선언.
{
health = startingHealth; // 시작 체력 설정
}
public void TakeHit(float damage, RaycastHit hit) // 데미지 받는 메소드 구현/
{
health -= damage; // 체력에서 데미지만큼 감소
if(health <= 0 && !dead) // 체력이 0 이하고 죽은 경우
{
Die(); // 죽음 메소드 호출
}
}
protected void Die() // 죽음 메소드
{
dead = true; // 죽은 판정으로 변경.
GameObject.Destroy(gameObject); // 현재 오브젝트 파괴
}
}
위와 같이 작성했다면 이제 Player 와 Enemy 가 해당 스크립트를 상속받아야
각 캐릭터의 체력을 관리 할 수 있다.
Player 와 Enemy 스크립트의 상속 부분에서 MonoBehaviour 를 LivingEntity 로 변경하여서
해당 클래스를 상속받도록 한다.
(이미 LivingEntity 에서 MonoBehaviour 와 IDamaneable 인터페이스를 상속받고 있어서
해당 클래스만 상속받으면 된다.)
이 때, 주의할 점이 있다.
Start 메소드를 상속받는 클래스인 Player 와 Enemy 에서도 사용하는데,
이런 경우 부모 클래스인 LivingEntity 의 Start 메소드의 코드는 사용하지 않게 된다.
이를 해결하기 위해 오버라이딩을 사용한다.
부모 클래스의 메소드를 선언 할 때 virtual 로 선언하고 상속받는 클래스의 메소드는 override 로 선언한다.
이렇게 하면 상속받는 클래스에서 메소드를 재정의 할 수 있게되고, base 를 통해 부모 클래스의 원래
메소드도 호출하여 사용할 수 있게 된다.
Player 스크립트와 Enemy 스크립트를 수정해주자.
| Player 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(GunController))] // GunController 스크립트를 포함
[RequireComponent(typeof(PlayerController))] // PlayerController 스크립트를 포함
public class Player : LivingEntity
{
public float moveSpeed = 5.0f; // 플레이어 이동속도
Camera viewCamera; // 카메라
PlayerController controller; // 플레이어 컨트롤러
GunController gunController; // 총 컨트롤러
protected override void Start() { // override 로 부모 클래스의 메소드를 재정의.
base.Start(); // base 를 통해 부모 클래스의 기존 메소드를 호출.
controller = GetComponent<PlayerController>();
// 현재 오브젝트의 플레이어 컨트롤러를 가져옴
gunController = GetComponent<GunController>();
// 현재 오브젝트의 총 컨트롤러를 가져옴
viewCamera = Camera.main;
// 카메라
}
void Update() {
// 이동 입력
Vector3 moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxis("Vertical"));
// GetAxisRaw 로 스무딩 X
Vector3 moveVelocity = moveInput.normalized * moveSpeed;
// normalized 로 단위벡터로 변환, 이동 속도를 계산
controller.Move(moveVelocity);
// 이동 속도를 이동 메소드로 전달
// 방향 입력
Ray ray = viewCamera.ScreenPointToRay(Input.mousePosition);
// 카메라 -> 마우스 커서 위치로 레이 발사.
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
// 평면의 법선벡터 생성.
float rayDistance;
// 위에서 쏜 레이와 평면의 교차지점까지 거리
if (groundPlane.Raycast(ray, out rayDistance)) {
// 레이가 평면과 교차하는 경우 교차지점까지의 거리를 rayDistance 에 저장.
Vector3 point = ray.GetPoint(rayDistance);
// GetPoint 와 교차지점까지의 거리로 교차지점의 위치를 저장.
// 카메라부터 교차지점까지 선으로 표시 : Debug.DrawLine(ray.origin, point, Color.red);
controller.LookAt(point);
// 플레이어 컨트롤러의 방향 전환 메소드에 교차 지점(방향) 전달.
}
// 무기 조작 입력
if (Input.GetMouseButton(0)) // 마우스 왼쪽 버튼 클릭 확인
{
gunController.Shoot(); // 총알 발사 메소드 호출
}
}
}
| Enemy 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(UnityEngine.AI.NavMeshAgent))]
// 현재 오브젝트에 NavMeshAgent 를 포함
public class Enemy : LivingEntity
{
UnityEngine.AI.NavMeshAgent pathfinder; // 내비게이션 레퍼런스
Transform target; // 적의 타겟(플레이어) 트랜스폼
protected override void Start()
// override 로 부모 클래스의 메소드를 재정의.
{
base.Start(); // base 를 통해 부모 클래스의 기존 메소드를 호출.
pathfinder = GetComponent<UnityEngine.AI.NavMeshAgent>();
// NavMeshAgent 레퍼런스 생성
target = GameObject.FindGameObjectWithTag("Player").transform;
// 타겟으로 "Player" 라는 태그를 가진 오브젝트의 트랜스폼을 저장
StartCoroutine(UpdatePath());
// 지정된 시간마다 목적지 쪽으로 위치를 갱신하는 코루틴 실행
}
void Update()
{
}
IEnumerator UpdatePath()
// 해당 코루틴 실행 시 지정한 시간마다 루프문 내부 코드 반복
{
float refreshRate = .25f; // 루프 시간
while(target != null) // 타겟이 있을 때 반복
{
Vector3 targetPosition = new Vector3(target.position.x, 0, target.position.z);
// 타겟의 x, z 좌표만 가져와 벡터로 저장
if(!dead) // 죽지 않은 경우
{
pathfinder.SetDestination(targetPosition);
// 내비게이션의 목적지를 타겟(플레이어)의 위치로 설정
}
yield return new WaitForSeconds(refreshRate);
// refreshRate 시간만큼 기다림
}
}
}
이렇게 작성하고 이제 인스펙터에서 체력을 설정해주자.
플레이어는 10, 적은 5로 설정했다.
그러면 이제 게임을 실행해보자.
위와 같이 적에게 총알을 5발 맞추자 적 오브젝트가 죽음 판정으로 사라진다.
다음 시간에는 적 스폰 시스템을 구현해보자.
'Unity > 게임개발' 카테고리의 다른 글
[Unity] 게임 개발 일지 | 탑다운 슈팅 6 (0) | 2023.11.24 |
---|---|
[Unity] 게임 개발 일지 | 탑다운 슈팅 5 (1) | 2023.11.23 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 3 (1) | 2023.11.22 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 2 (0) | 2023.11.17 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 1 (0) | 2023.11.16 |