유니티3D 프로그래밍
Unity 6주차 3,4,5일 수업 내용 : Zombie (21.04.14) 본문
레트로 유니티 게임 프로그래밍의 좀비 서바이벌을 구현
AmmoPack : 총알 채우는 아이템
using UnityEngine;
// 총알을 충전하는 아이템
public class AmmoPack : MonoBehaviour, IItem {
public int ammo = 30; // 충전할 총알 수
public void Use(GameObject target) {
// 전달 받은 게임 오브젝트로부터 PlayerShooter 컴포넌트를 가져오기 시도
PlayerShooter playerShooter = target.GetComponent<PlayerShooter>();
// PlayerShooter 컴포넌트가 있으며, 총 오브젝트가 존재하면
if (playerShooter != null && playerShooter.gun != null)
{
// 총의 남은 탄환 수를 ammo 만큼 더합니다.
playerShooter.gun.ammoRemain += ammo;
}
// 사용되었으므로, 자신을 파괴
Destroy(gameObject);
}
}
Coin : 점수를 올려주는 아이템
using UnityEngine;
// 게임 점수를 증가시키는 아이템
public class Coin : MonoBehaviour, IItem {
public int score = 200; // 증가할 점수
public void Use(GameObject target) {
// 게임 매니저로 접근해 점수 추가
GameManager.instance.AddScore(score);
// 사용되었으므로, 자신을 파괴
Destroy(gameObject);
}
}
Enemy : 적 AI에 관련된 모든 정보
using System.Collections;
using UnityEngine;
using UnityEngine.AI; // AI, 내비게이션 시스템 관련 코드를 가져오기
// 적 AI를 구현한다
public class Enemy : LivingEntity {
public LayerMask whatIsTarget; // 추적 대상 레이어
private LivingEntity targetEntity; // 추적할 대상
private NavMeshAgent pathFinder; // 경로계산 AI 에이전트
public ParticleSystem hitEffect; // 피격시 재생할 파티클 효과
public AudioClip deathSound; // 사망시 재생할 소리
public AudioClip hitSound; // 피격시 재생할 소리
private Animator enemyAnimator; // 애니메이터 컴포넌트
private AudioSource enemyAudioPlayer; // 오디오 소스 컴포넌트
private Renderer enemyRenderer; // 렌더러 컴포넌트
public float damage = 20f; // 공격력
public float timeBetAttack = 0.5f; // 공격 간격
private float lastAttackTime; // 마지막 공격 시점
// 추적할 대상이 존재하는지 알려주는 프로퍼티
private bool hasTarget
{
get
{
// 추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
if (targetEntity != null && !targetEntity.dead)
{
return true;
}
// 그렇지 않다면 false
return false;
}
}
private void Awake() {
// 게임 오브젝트로부터 사용할 컴포넌트들을 가져오기
pathFinder = GetComponent<NavMeshAgent>();
enemyAnimator = GetComponent<Animator>();
enemyAudioPlayer = GetComponent<AudioSource>();
// 렌더러 컴포넌트는 자식 게임 오브젝트에게 있으므로
// GetComponentInChildren() 메서드를 사용
enemyRenderer = GetComponentInChildren<Renderer>();
}
// 적 AI의 초기 스펙을 결정하는 셋업 메서드
public void Setup(float newHealth, float newDamage,
float newSpeed, Color skinColor) {
// 체력 설정
startingHealth = newHealth;
health = newHealth;
// 공격력 설정
damage = newDamage;
// 내비메쉬 에이전트의 이동 속도 설정
pathFinder.speed = newSpeed;
// 렌더러가 사용중인 머테리얼의 컬러를 변경, 외형 색이 변함
enemyRenderer.material.color = skinColor;
}
private void Start() {
// 게임 오브젝트 활성화와 동시에 AI의 추적 루틴 시작
StartCoroutine(UpdatePath());
}
private void Update() {
// 추적 대상의 존재 여부에 따라 다른 애니메이션을 재생
enemyAnimator.SetBool("HasTarget", hasTarget);
}
// 주기적으로 추적할 대상의 위치를 찾아 경로를 갱신
private IEnumerator UpdatePath() {
// 살아있는 동안 무한 루프
while (!dead)
{
if (hasTarget)
{
// 추적 대상 존재 : 경로를 갱신하고 AI 이동을 계속 진행
pathFinder.isStopped = false;
pathFinder.SetDestination(
targetEntity.transform.position);
}
else
{
// 추적 대상 없음 : AI 이동 중지
pathFinder.isStopped = true;
// 20 유닛의 반지름을 가진 가상의 구를 그렸을때, 구와 겹치는 모든 콜라이더를 가져옴
// 단, whatIsTarget 레이어를 가진 콜라이더만 가져오도록 필터링
Collider[] colliders =
Physics.OverlapSphere(transform.position, 20f, whatIsTarget);
// 모든 콜라이더들을 순회하면서, 살아있는 LivingEntity 찾기
for (int i = 0; i < colliders.Length; i++)
{
// 콜라이더로부터 LivingEntity 컴포넌트 가져오기
LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();
// LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아있다면,
if (livingEntity != null && !livingEntity.dead)
{
// 추적 대상을 해당 LivingEntity로 설정
targetEntity = livingEntity;
// for문 루프 즉시 정지
break;
}
}
}
// 0.25초 주기로 처리 반복
yield return new WaitForSeconds(0.25f);
}
}
// 데미지를 입었을때 실행할 처리
public override void OnDamage(float damage,
Vector3 hitPoint, Vector3 hitNormal) {
// 아직 사망하지 않은 경우에만 피격 효과 재생
if (!dead)
{
// 공격 받은 지점과 방향으로 파티클 효과를 재생
hitEffect.transform.position = hitPoint;
hitEffect.transform.rotation
= Quaternion.LookRotation(hitNormal);
hitEffect.Play();
// 피격 효과음 재생
enemyAudioPlayer.PlayOneShot(hitSound);
}
// LivingEntity의 OnDamage()를 실행하여 데미지 적용
base.OnDamage(damage, hitPoint, hitNormal);
}
// 사망 처리
public override void Die() {
// LivingEntity의 Die()를 실행하여 기본 사망 처리 실행
base.Die();
// 다른 AI들을 방해하지 않도록 자신의 모든 콜라이더들을 비활성화
Collider[] enemyColliders = GetComponents<Collider>();
for (int i = 0; i < enemyColliders.Length; i++)
{
enemyColliders[i].enabled = false;
}
// AI 추적을 중지하고 내비메쉬 컴포넌트를 비활성화
pathFinder.isStopped = true;
pathFinder.enabled = false;
// 사망 애니메이션 재생
enemyAnimator.SetTrigger("Die");
// 사망 효과음 재생
enemyAudioPlayer.PlayOneShot(deathSound);
}
private void OnTriggerStay(Collider other) {
// 자신이 사망하지 않았으며,
// 최근 공격 시점에서 timeBetAttack 이상 시간이 지났다면 공격 가능
if (!dead && Time.time >= lastAttackTime + timeBetAttack)
{
// 상대방으로부터 LivingEntity 타입을 가져오기 시도
LivingEntity attackTarget
= other.GetComponent<LivingEntity>();
// 상대방의 LivingEntity가 자신의 추적 대상이라면 공격 실행
if (attackTarget != null && attackTarget == targetEntity)
{
// 최근 공격 시간을 갱신
lastAttackTime = Time.time;
// 상대방의 피격 위치와 피격 방향을 근삿값으로 계산
Vector3 hitPoint
= other.ClosestPoint(transform.position);
Vector3 hitNormal
= transform.position - other.transform.position;
// 공격 실행
attackTarget.OnDamage(damage, hitPoint, hitNormal);
}
}
}
}
EnemySpawner : 적을 생성하는 방법
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// 적 게임 오브젝트를 주기적으로 생성
public class EnemySpawner : MonoBehaviour {
public Enemy enemyPrefab; // 생성할 적 AI
public Transform[] spawnPoints; // 적 AI를 소환할 위치들
public float damageMax = 40f; // 최대 공격력
public float damageMin = 20f; // 최소 공격력
public float healthMax = 200f; // 최대 체력
public float healthMin = 100f; // 최소 체력
public float speedMax = 3f; // 최대 속도
public float speedMin = 1f; // 최소 속도
public Color strongEnemyColor = Color.red; // 강한 적 AI가 가지게 될 피부색
private List<Enemy> enemies = new List<Enemy>(); // 생성된 적들을 담는 리스트
private int wave; // 현재 웨이브
private void Update() {
// 게임 오버 상태일때는 생성하지 않음
if (GameManager.instance != null && GameManager.instance.isGameover)
{
return;
}
// 적을 모두 물리친 경우 다음 스폰 실행
if (enemies.Count <= 0)
{
SpawnWave();
}
// UI 갱신
UpdateUI();
}
// 웨이브 정보를 UI로 표시
private void UpdateUI() {
// 현재 웨이브와 남은 적의 수 표시
UIManager.instance.UpdateWaveText(wave, enemies.Count);
}
// 현재 웨이브에 맞춰 적을 생성
private void SpawnWave() {
// 웨이브 1 증가
wave++;
// 현재 웨이브 * 1.5에 반올림 한 개수 만큼 적을 생성
int spawnCount = Mathf.RoundToInt(wave * 1.5f);
// spawnCount 만큼 적을 생성
for (int i = 0; i < spawnCount; i++)
{
// 적의 세기를 0%에서 100% 사이에서 랜덤 결정
float enemyIntensity = Random.Range(0f, 1f);
// 적 생성 처리 실행
CreateEnemy(enemyIntensity);
}
}
// 적을 생성하고 생성한 적에게 추적할 대상을 할당
private void CreateEnemy(float intensity) {
// intensity를 기반으로 적의 능력치 결정
float health = Mathf.Lerp(healthMin, healthMax, intensity);
float damage = Mathf.Lerp(damageMin, damageMax, intensity);
float speed = Mathf.Lerp(speedMin, speedMax, intensity);
// intensity를 기반으로 하얀색과 enemyStrength 사이에서 적의 피부색 결정
Color skinColor = Color.Lerp(Color.white, strongEnemyColor, intensity);
// 생성할 위치를 랜덤으로 결정
Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
// 적 프리팹으로부터 적 생성
Enemy enemy = Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);
// 생성한 적의 능력치와 추적 대상 설정
enemy.Setup(health, damage, speed, skinColor);
// 생성된 적을 리스트에 추가
enemies.Add(enemy);
// 적의 onDeath 이벤트에 익명 메서드 등록
// 사망한 적을 리스트에서 제거
enemy.onDeath += () => enemies.Remove(enemy);
// 사망한 적을 10 초 뒤에 파괴
enemy.onDeath += () => Destroy(enemy.gameObject, 10f);
// 적 사망시 점수 상승
enemy.onDeath += () => GameManager.instance.AddScore(100);
}
}
GameManager : 게임의 전반적인 UI를 담당함
using UnityEngine;
// 점수와 게임 오버 여부, 게임 UI를 관리하는 게임 매니저
public class GameManager : MonoBehaviour {
// 외부에서 싱글톤 오브젝트를 가져올때 사용할 프로퍼티
public static GameManager instance
{
get
{
// 만약 싱글톤 변수에 아직 오브젝트가 할당되지 않았다면
if (m_instance == null)
{
// 씬에서 GameManager 오브젝트를 찾아 할당
m_instance = FindObjectOfType<GameManager>();
}
// 싱글톤 오브젝트를 반환
return m_instance;
}
}
private static GameManager m_instance; // 싱글톤이 할당될 static 변수
private int score = 0; // 현재 게임 점수
public bool isGameover { get; private set; } // 게임 오버 상태
private void Awake() {
// 씬에 싱글톤 오브젝트가 된 다른 GameManager 오브젝트가 있다면
if (instance != this)
{
// 자신을 파괴
Destroy(gameObject);
}
}
private void Start() {
// 플레이어 캐릭터의 사망 이벤트 발생시 게임 오버
FindObjectOfType<PlayerHealth>().onDeath += EndGame;
}
// 점수를 추가하고 UI 갱신
public void AddScore(int newScore) {
// 게임 오버가 아닌 상태에서만 점수 증가 가능
if (!isGameover)
{
// 점수 추가
score += newScore;
// 점수 UI 텍스트 갱신
UIManager.instance.UpdateScoreText(score);
}
}
// 게임 오버 처리
public void EndGame() {
// 게임 오버 상태를 참으로 변경
isGameover = true;
// 게임 오버 UI를 활성화
UIManager.instance.SetActiveGameoverUI(true);
}
}
Gun : 총을 구현하는 스크립트
using System.Collections;
using UnityEngine;
// 총을 구현한다
public class Gun : MonoBehaviour {
// 총의 상태를 표현하는데 사용할 타입을 선언한다
public enum State {
Ready, // 발사 준비됨
Empty, // 탄창이 빔
Reloading // 재장전 중
}
public State state { get; private set; } // 현재 총의 상태
public Transform fireTransform; // 총알이 발사될 위치
public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
public ParticleSystem shellEjectEffect; // 탄피 배출 효과
private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러
private AudioSource gunAudioPlayer; // 총 소리 재생기
public AudioClip shotClip; // 발사 소리
public AudioClip reloadClip; // 재장전 소리
public float damage = 25; // 공격력
private float fireDistance = 50f; // 사정거리
public int ammoRemain = 100; // 남은 전체 탄약
public int magCapacity = 25; // 탄창 용량
public int magAmmo; // 현재 탄창에 남아있는 탄약
public float timeBetFire = 0.12f; // 총알 발사 간격
public float reloadTime = 1.8f; // 재장전 소요 시간
private float lastFireTime; // 총을 마지막으로 발사한 시점
private void Awake() {
// 사용할 컴포넌트들의 참조를 가져오기
gunAudioPlayer = GetComponent<AudioSource>();
bulletLineRenderer = GetComponent<LineRenderer>();
// 사용할 점을 두개로 변경
bulletLineRenderer.positionCount = 2;
// 라인 렌더러를 비활성화
bulletLineRenderer.enabled = false;
}
private void OnEnable() {
// 현재 탄창을 가득채우기
magAmmo = magCapacity;
// 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
state = State.Ready;
// 마지막으로 총을 쏜 시점을 초기화
lastFireTime = 0;
}
// 발사 시도
public void Fire() {
// 현재 상태가 발사 가능한 상태
// && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
if (state == State.Ready
&& Time.time >= lastFireTime + timeBetFire)
{
// 마지막 총 발사 시점을 갱신
lastFireTime = Time.time;
// 실제 발사 처리 실행
Shot();
}
}
// 실제 발사 처리
private void Shot() {
// 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
RaycastHit hit;
// 총알이 맞은 곳을 저장할 변수
Vector3 hitPosition = Vector3.zero;
// 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
if (Physics.Raycast(fireTransform.position,
fireTransform.forward, out hit, fireDistance))
{
// 레이가 어떤 물체와 충돌한 경우
// 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
IDamageable target =
hit.collider.GetComponent<IDamageable>();
// 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
if (target != null)
{
// 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
target.OnDamage(damage, hit.point, hit.normal);
}
// 레이가 충돌한 위치 저장
hitPosition = hit.point;
}
else
{
// 레이가 다른 물체와 충돌하지 않았다면
// 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
hitPosition = fireTransform.position +
fireTransform.forward * fireDistance;
}
// 발사 이펙트 재생 시작
StartCoroutine(ShotEffect(hitPosition));
// 남은 탄환의 수를 -1
magAmmo--;
if (magAmmo <= 0)
{
// 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
state = State.Empty;
}
}
// 발사 이펙트와 소리를 재생하고 총알 궤적을 그린다
private IEnumerator ShotEffect(Vector3 hitPosition) {
// 총구 화염 효과 재생
muzzleFlashEffect.Play();
// 탄피 배출 효과 재생
shellEjectEffect.Play();
// 총격 소리 재생
gunAudioPlayer.PlayOneShot(shotClip);
// 선의 시작점은 총구의 위치
bulletLineRenderer.SetPosition(0, fireTransform.position);
// 선의 끝점은 입력으로 들어온 충돌 위치
bulletLineRenderer.SetPosition(1, hitPosition);
// 라인 렌더러를 활성화하여 총알 궤적을 그린다
bulletLineRenderer.enabled = true;
// 0.03초 동안 잠시 처리를 대기
yield return new WaitForSeconds(0.03f);
// 라인 렌더러를 비활성화하여 총알 궤적을 지운다
bulletLineRenderer.enabled = false;
}
// 재장전 시도
public bool Reload() {
if (state == State.Reloading ||
ammoRemain <= 0 || magAmmo >= magCapacity)
{
// 이미 재장전 중이거나, 남은 총알이 없거나
// 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
return false;
}
// 재장전 처리 시작
StartCoroutine(ReloadRoutine());
return true;
}
// 실제 재장전 처리를 진행
private IEnumerator ReloadRoutine() {
// 현재 상태를 재장전 중 상태로 전환
state = State.Reloading;
// 재장전 소리 재생
gunAudioPlayer.PlayOneShot(reloadClip);
// 재장전 소요 시간 만큼 처리를 쉬기
yield return new WaitForSeconds(reloadTime);
// 탄창에 채울 탄약을 계산한다
int ammoToFill = magCapacity - magAmmo;
// 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
// 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
if (ammoRemain < ammoToFill)
{
ammoToFill = ammoRemain;
}
// 탄창을 채운다
magAmmo += ammoToFill;
// 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
ammoRemain -= ammoToFill;
// 총의 현재 상태를 발사 준비된 상태로 변경
state = State.Ready;
}
}
HealthPack : 체력 회복 아이템
using UnityEngine;
// 체력을 회복하는 아이템
public class HealthPack : MonoBehaviour, IItem {
public float health = 50; // 체력을 회복할 수치
public void Use(GameObject target) {
// 전달받은 게임 오브젝트로부터 LivingEntity 컴포넌트 가져오기 시도
LivingEntity life = target.GetComponent<LivingEntity>();
// LivingEntity컴포넌트가 있다면
if (life != null)
{
// 체력 회복 실행
life.RestoreHealth(health);
}
// 사용되었으므로, 자신을 파괴
Destroy(gameObject);
}
}
IDamageable : 데미지를 입을 수 있는 타입이 가져야 하는 공통적인 인터페이스
using UnityEngine;
// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable {
// 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
// OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}
IItem : 아이템 타입들은 무조건 구현해야 하는 인터페이스
using UnityEngine;
// 아이템 타입들이 반드시 구현해야하는 인터페이스
public interface IItem {
// 입력으로 받는 target은 아이템 효과가 적용될 대상
void Use(GameObject target);
}
ItemSpawner : 아이템을 생성하는 스크립트
using UnityEngine;
using UnityEngine.AI; // 내비메쉬 관련 코드
// 주기적으로 아이템을 플레이어 근처에 생성하는 스크립트
public class ItemSpawner : MonoBehaviour {
public GameObject[] items; // 생성할 아이템들
public Transform playerTransform; // 플레이어의 트랜스폼
public float maxDistance = 5f; // 플레이어 위치로부터 아이템이 배치될 최대 반경
public float timeBetSpawnMax = 7f; // 최대 시간 간격
public float timeBetSpawnMin = 2f; // 최소 시간 간격
private float timeBetSpawn; // 생성 간격
private float lastSpawnTime; // 마지막 생성 시점
private void Start() {
// 생성 간격과 마지막 생성 시점 초기화
timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
lastSpawnTime = 0;
}
// 주기적으로 아이템 생성 처리 실행
private void Update() {
if (Time.time >= lastSpawnTime + timeBetSpawn && playerTransform != null)
{
lastSpawnTime = Time.time; // 마지막 생성 시간 갱신
timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax); // 생성 주기를 랜덤으로 변경
Spawn(); // 실제 아이템 생성
}
}
// 실제 아이템 생성 처리
private void Spawn() {
// 플레이어 근처의 네브 메쉬위의 랜덤 위치를 가져옵니다.
Vector3 spawnPosition = GetRandomPointOnNavMesh(playerTransform.position, maxDistance);
spawnPosition += Vector3.up * 0.5f; // 바닥에서 0.5만큼 위로 올립니다.
// 아이템 중 하나를 무작위로 골라 랜덤 위치에 생성합니다.
GameObject item = Instantiate(items[Random.Range(0, items.Length)], spawnPosition, Quaternion.identity);
// 생성된 아이템을 5초 뒤에 파괴
Destroy(item, 5f);
}
// 네브 메시 위의 랜덤한 위치를 반환하는 메서드
// center를 중심으로 distance 반경 안에서 랜덤한 위치를 찾는다.
private Vector3 GetRandomPointOnNavMesh(Vector3 center, float distance) {
// center를 중심으로 반지름이 maxDinstance인 구 안에서의 랜덤한 위치 하나를 저장
// Random.insideUnitSphere는 반지름이 1인 구 안에서의 랜덤한 한 점을 반환하는 프로퍼티
Vector3 randomPos = Random.insideUnitSphere * distance + center;
// 네브 메시 샘플링의 결과 정보를 저장하는 변수
NavMeshHit hit;
// randomPos를 기준으로 maxDistance 반경 안에서, randomPos에 가장 가까운 네브 메시 위의 한 점을 찾음
NavMesh.SamplePosition(randomPos, out hit, distance, NavMesh.AllAreas);
// 찾은 점 반환
return hit.position;
}
}
LivingEntity : 생명체로서 동작할 게임 오브젝트들을 위한 뼈대를 제공 (체력, 데미지 받아들이기, 사망 기능, 사망 이벤트)
using System;
using UnityEngine;
// 생명체로서 동작할 게임 오브젝트들을 위한 뼈대를 제공
// 체력, 데미지 받아들이기, 사망 기능, 사망 이벤트를 제공
public class LivingEntity : MonoBehaviour, IDamageable {
public float startingHealth = 100f; // 시작 체력
public float health { get; protected set; } // 현재 체력
public bool dead { get; protected set; } // 사망 상태
public event Action onDeath; // 사망시 발동할 이벤트
// 생명체가 활성화될때 상태를 리셋
protected virtual void OnEnable() {
// 사망하지 않은 상태로 시작
dead = false;
// 체력을 시작 체력으로 초기화
health = startingHealth;
}
// 데미지를 입는 기능
public virtual void OnDamage(float damage, Vector3 hitPoint,
Vector3 hitNormal) {
// 데미지만큼 체력 감소
health -= damage;
// 체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행
if (health <= 0 && !dead)
{
Die();
}
}
// 체력을 회복하는 기능
public virtual void RestoreHealth(float newHealth) {
if (dead)
{
// 이미 사망한 경우 체력을 회복할 수 없음
return;
}
// 체력 추가
health += newHealth;
}
// 사망 처리
public virtual void Die() {
// onDeath 이벤트에 등록된 메서드가 있다면 실행
if (onDeath != null)
{
onDeath();
}
// 사망 상태를 참으로 변경
dead = true;
}
}
PlayerHealth : 플레이어의 체력과 동작이 관련된 스크립트
using UnityEngine;
using UnityEngine.UI; // UI 관련 코드
// 플레이어 캐릭터의 생명체로서의 동작을 담당
public class PlayerHealth : LivingEntity {
public Slider healthSlider; // 체력을 표시할 UI 슬라이더
public AudioClip deathClip; // 사망 소리
public AudioClip hitClip; // 피격 소리
public AudioClip itemPickupClip; // 아이템 습득 소리
private AudioSource playerAudioPlayer; // 플레이어 소리 재생기
private Animator playerAnimator; // 플레이어의 애니메이터
private PlayerMovement playerMovement; // 플레이어 움직임 컴포넌트
private PlayerShooter playerShooter; // 플레이어 슈터 컴포넌트
private void Awake() {
// 사용할 컴포넌트를 가져오기
playerAnimator = GetComponent<Animator>();
playerAudioPlayer = GetComponent<AudioSource>();
playerMovement = GetComponent<PlayerMovement>();
playerShooter = GetComponent<PlayerShooter>();
}
protected override void OnEnable() {
// LivingEntity의 OnEnable() 실행 (상태 초기화)
base.OnEnable();
// 체력 슬라이더 활성화
healthSlider.gameObject.SetActive(true);
// 체력 슬라이더의 최대값을 기본 체력값으로 변경
healthSlider.maxValue = startingHealth;
// 체력 슬라이더의 값을 현재 체력값으로 변경
healthSlider.value = health;
// 플레이어 조작을 받는 컴포넌트들 활성화
playerMovement.enabled = true;
playerShooter.enabled = true;
}
// 체력 회복
public override void RestoreHealth(float newHealth) {
// LivingEntity의 RestoreHealth() 실행 (체력 증가)
base.RestoreHealth(newHealth);
// 체력 갱신
healthSlider.value = health;
}
// 데미지 처리
public override void OnDamage(float damage, Vector3 hitPoint,
Vector3 hitDirection) {
if (!dead)
{
// 사망하지 않은 경우에만 효과음을 재생
playerAudioPlayer.PlayOneShot(hitClip);
}
// LivingEntity의 OnDamage() 실행(데미지 적용)
base.OnDamage(damage, hitPoint, hitDirection);
// 갱신된 체력을 체력 슬라이더에 반영
healthSlider.value = health;
}
// 사망 처리
public override void Die() {
// LivingEntity의 Die() 실행(사망 적용)
base.Die();
// 체력 슬라이더 비활성화
healthSlider.gameObject.SetActive(false);
// 사망음 재생
playerAudioPlayer.PlayOneShot(deathClip);
// 애니메이터의 Die 트리거를 발동시켜 사망 애니메이션 재생
playerAnimator.SetTrigger("Die");
// 플레이어 조작을 받는 컴포넌트들 비활성화
playerMovement.enabled = false;
playerShooter.enabled = false;
}
private void OnTriggerEnter(Collider other) {
// 아이템과 충돌한 경우 해당 아이템을 사용하는 처리
// 사망하지 않은 경우에만 아이템 사용가능
if (!dead)
{
// 충돌한 상대방으로 부터 Item 컴포넌트를 가져오기 시도
IItem item = other.GetComponent<IItem>();
// 충돌한 상대방으로부터 Item 컴포넌트가 가져오는데 성공했다면
if (item != null)
{
// Use 메서드를 실행하여 아이템 사용
item.Use(gameObject);
// 아이템 습득 소리 재생
playerAudioPlayer.PlayOneShot(itemPickupClip);
}
}
}
}
PlayerInput : 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지, 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
using UnityEngine;
// 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지
// 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
public class PlayerInput : MonoBehaviour {
public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름
public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름
// 값 할당은 내부에서만 가능
public float move { get; private set; } // 감지된 움직임 입력값
public float rotate { get; private set; } // 감지된 회전 입력값
public bool fire { get; private set; } // 감지된 발사 입력값
public bool reload { get; private set; } // 감지된 재장전 입력값
// 매프레임 사용자 입력을 감지
private void Update() {
// 게임오버 상태에서는 사용자 입력을 감지하지 않는다
if (GameManager.instance != null
&& GameManager.instance.isGameover)
{
move = 0;
rotate = 0;
fire = false;
reload = false;
return;
}
// move에 관한 입력 감지
move = Input.GetAxis(moveAxisName);
// rotate에 관한 입력 감지
rotate = Input.GetAxis(rotateAxisName);
// fire에 관한 입력 감지
fire = Input.GetButton(fireButtonName);
// reload에 관한 입력 감지
reload = Input.GetButtonDown(reloadButtonName);
}
}
PlayerMovement : 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
using UnityEngine;
// 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
public class PlayerMovement : MonoBehaviour {
public float moveSpeed = 5f; // 앞뒤 움직임의 속도
public float rotateSpeed = 180f; // 좌우 회전 속도
private Animator playerAnimator; // 플레이어 캐릭터의 애니메이터
private PlayerInput playerInput; // 플레이어 입력을 알려주는 컴포넌트
private Rigidbody playerRigidbody; // 플레이어 캐릭터의 리지드바디
private void Start() {
// 사용할 컴포넌트들의 참조를 가져오기
playerInput = GetComponent<PlayerInput>();
playerRigidbody = GetComponent<Rigidbody>();
playerAnimator = GetComponent<Animator>();
}
// FixedUpdate는 물리 갱신 주기에 맞춰 실행됨
private void FixedUpdate() {
// 회전 실행
Rotate();
// 움직임 실행
Move();
// 입력값에 따라 애니메이터의 Move 파라미터 값을 변경
playerAnimator.SetFloat("Move", playerInput.move);
}
// 입력값에 따라 캐릭터를 앞뒤로 움직임
private void Move() {
// 상대적으로 이동할 거리 계산
Vector3 moveDistance =
playerInput.move * transform.forward * moveSpeed * Time.deltaTime;
// 리지드바디를 통해 게임 오브젝트 위치 변경
playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
}
// 입력값에 따라 캐릭터를 좌우로 회전
private void Rotate() {
// 상대적으로 회전할 수치 계산
float turn =
playerInput.rotate * rotateSpeed * Time.deltaTime;
// 리지드바디를 통해 게임 오브젝트 회전 변경
playerRigidbody.rotation = playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f);
}
}
PlayerShooter : 주어진 Gun 오브젝트를 쏘거나 재장전, IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
using UnityEngine;
// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour {
public Gun gun; // 사용할 총
public Transform gunPivot; // 총 배치의 기준점
public Transform leftHandMount; // 총의 왼쪽 손잡이, 왼손이 위치할 지점
public Transform rightHandMount; // 총의 오른쪽 손잡이, 오른손이 위치할 지점
private PlayerInput playerInput; // 플레이어의 입력
private Animator playerAnimator; // 애니메이터 컴포넌트
private void Start() {
// 사용할 컴포넌트들을 가져오기
playerInput = GetComponent<PlayerInput>();
playerAnimator = GetComponent<Animator>();
}
private void OnEnable() {
// 슈터가 활성화될 때 총도 함께 활성화
gun.gameObject.SetActive(true);
}
private void OnDisable() {
// 슈터가 비활성화될 때 총도 함께 비활성화
gun.gameObject.SetActive(false);
}
private void Update() {
// 입력을 감지하고 총 발사하거나 재장전
if (playerInput.fire)
{
// 발사 입력 감지시 총 발사
gun.Fire();
}
else if (playerInput.reload)
{
// 재장전 입력 감지시 재장전
if (gun.Reload())
{
// 재장전 성공시에만 재장전 애니메이션 재생
playerAnimator.SetTrigger("Reload");
}
}
// 남은 탄약 UI를 갱신
UpdateUI();
}
// 탄약 UI 갱신
private void UpdateUI() {
if (gun != null && UIManager.instance != null)
{
// UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
}
}
// 애니메이터의 IK 갱신
private void OnAnimatorIK(int layerIndex) {
// 총의 기준점 gunPivot을 3D 모델의 오른쪽 팔꿈치 위치로 이동
gunPivot.position =
playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);
// IK를 사용하여 왼손의 위치와 회전을 총의 오른쪽 손잡이에 맞춘다
playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);
playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand,
leftHandMount.position);
playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand,
leftHandMount.rotation);
// IK를 사용하여 오른손의 위치와 회전을 총의 오른쪽 손잡이에 맞춘다
playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1.0f);
playerAnimator.SetIKPosition(AvatarIKGoal.RightHand,
rightHandMount.position);
playerAnimator.SetIKRotation(AvatarIKGoal.RightHand,
rightHandMount.rotation);
}
}
Rotator : 게임 오브젝트를 지속적으로 회전하는 스크립트
using UnityEngine;
// 게임 오브젝트를 지속적으로 회전하는 스크립트
public class Rotator : MonoBehaviour {
public float rotationSpeed = 60f;
private void Update() {
transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f);
}
}
UIManager : 필요한 UI에 즉시 접근하고 변경할 수 있도록 허용하는 UI 매니저
using UnityEngine;
using UnityEngine.SceneManagement; // 씬 관리자 관련 코드
using UnityEngine.UI; // UI 관련 코드
// 필요한 UI에 즉시 접근하고 변경할 수 있도록 허용하는 UI 매니저
public class UIManager : MonoBehaviour {
// 싱글톤 접근용 프로퍼티
public static UIManager instance
{
get
{
if (m_instance == null)
{
m_instance = FindObjectOfType<UIManager>();
}
return m_instance;
}
}
private static UIManager m_instance; // 싱글톤이 할당될 변수
public Text ammoText; // 탄약 표시용 텍스트
public Text scoreText; // 점수 표시용 텍스트
public Text waveText; // 적 웨이브 표시용 텍스트
public GameObject gameoverUI; // 게임 오버시 활성화할 UI
// 탄약 텍스트 갱신
public void UpdateAmmoText(int magAmmo, int remainAmmo) {
ammoText.text = magAmmo + "/" + remainAmmo;
}
// 점수 텍스트 갱신
public void UpdateScoreText(int newScore) {
scoreText.text = "Score : " + newScore;
}
// 적 웨이브 텍스트 갱신
public void UpdateWaveText(int waves, int count) {
waveText.text = "Wave : " + waves + "\nEnemy Left : " + count;
}
// 게임 오버 UI 활성화
public void SetActiveGameoverUI(bool active) {
gameoverUI.SetActive(active);
}
// 게임 재시작
public void GameRestart() {
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
'Unity > 수업내용' 카테고리의 다른 글
Unity UGUI Test (21.04.20) (0) | 2021.04.20 |
---|---|
Unity 6주차 5일 수업 내용 : 다단계 Scene 구성 (21.04.06) (0) | 2021.04.16 |
Unity 6주차 2일 수업 내용 : 쿠키런 구현해보기 (21.04.13) (0) | 2021.04.13 |
Unity 5주차 5일 수업 내용 : Dodge, Fov (21.04.09) (0) | 2021.04.09 |
Unity 5주차 4일 수업 내용 : Ray, 캐릭터 이동 (21.04.08) (0) | 2021.04.08 |