이번 글은 해당 영상을 보고 참고하여 작성하였습니다.
(2) [Unity 2D Game] Tower Defense #03 - 체력, 골드 시스템 - YouTube
적 체력 데이터 처리
EnemyHP.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class EnemyHP : MonoBehaviour
{
[SerializeField]
private float maxHP; // 최대 체력
private float currentHP; // 현재 체력
private bool isDie = false; // 적이 사망 상태이면 true
private Enemy enemy;
public float MaxHP => maxHP;
public float CurrentHP => currentHP;
private void Awake()
{
currentHP = maxHP;
enemy = GetComponent<Enemy>();
spriteRenderer = GetComponent<SpriteRenderer>();
}
public void TakeDamage(float damage)
{
// 적의 체력이 damage 만큼 감소해서 죽을 상황일 때 여러 타워의 공격을
// 동시에 받으면 OnDie() 함수가 여러번 실행될 수 있음
// 현재 적의 상태가 사망 상태이면 아래 코드 실행하지 않음
if (isDie == true) return;
// 현재 체력을 damage 만큼 감소
currentHP -= damage;
// 체력이 0이하 = 적 캐릭터 사망
if(currentHP <=0)
{
isDie = true;
// 적 캐릭터 사망
enemy.OnDie(EnemyDestoryType.Kill);
}
}
}
적의 HP 스크립트를 생성하고 MaxHP는 5로 설정
TowerWeapon.cs
using System.Collections;
using UnityEngine;
// 공격 대상을 탐색하는 SearchTarget, 대상을 공격하는 AttackToTarget
public enum WeaponState { SearchTarget = 0, AttackToTarget }
public class TowerWeapon : MonoBehaviour
{
[SerializeField]
private GameObject projectTilePrefab; // 발사체 프리팹
[SerializeField]
private Transform spawnPoint; // 발사체 생성 위치
[SerializeField]
private float attackRate = .5f; // 공격 속도
[SerializeField]
private float attackRange = 2.0f; // 공격 범위
[SerializeField]
private int attackDamage = 1;
private WeaponState weaponState = WeaponState.SearchTarget; // 타워 무기의 상태
private Transform attackTarget = null; // 공격 대상
private EnemySpawner enemySpawner; // 게임에 존재하는 적 정보 획득용
[SerializeField]
private Animator archerAnimator; // 타워에 있는 아처 애니메이션
private Quaternion spawnArrowRot;
public void Setup(EnemySpawner enemySpawner)
{
this.enemySpawner = enemySpawner;
// 최초 상태를 WeaponState.SearchTarget 으로 설정
ChangeState(WeaponState.SearchTarget);
}
public void ChangeState(WeaponState newState)
{
// 이전에 재생중이던 상태 종료
StopCoroutine(weaponState.ToString());
// 상태 변경
weaponState = newState;
// 새로운 상태 재생
StartCoroutine(weaponState.ToString());
}
private void Update()
{
if(attackTarget != null)
{
RotateToTarget();
}
}
/// <summary>
/// 대상을 바라보게 하는 회전 함수
/// </summary>
private void RotateToTarget()
{
// 원점으로부터의 거리와 수평축으로부터의 각도를 이요해 위치를 구하는 극 좌표게 이용
// 각도 = Arctan(y/x)
// x, y 변위값 구하기
float dx = attackTarget.position.x - transform.position.x;
float dy = attackTarget.position.y - transform.position.y;
// Archer가 왼쪽을 바라볼 수 있게 SpriteRenderer의 Filp.x 설정
if(dx < 0){
archerAnimator.gameObject.GetComponent<SpriteRenderer>().flipX = true;
archerAnimator.SetFloat("Horizontal", -1);
}
else{
archerAnimator.gameObject.GetComponent<SpriteRenderer>().flipX = false;
archerAnimator.SetFloat("Horizontal", 1);
}
if (dy < 0)
archerAnimator.SetFloat("Vertical", -1);
else
archerAnimator.SetFloat("Vertical", 1);
// x, y 변위값을 바탕으로 각도 구하기
// 각도가 radian 단위이기 때문에 Mathf.Rad2Deg를 곱해도 단위를 구함
float degree = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
spawnArrowRot = Quaternion.Euler(0, 0, degree);
}
/// <summary>
/// 현재 타워의 사정거리 안에 있으면서 가장 가까운 적의 거리를 저장
/// </summary>
/// <returns></returns>
private IEnumerator SearchTarget()
{
while(true)
{
// 제일 가까이 있는 적을 찾기 위해 최초 거리를 최대한 크게 설정
float closesDisSqr = Mathf.Infinity;
// EnemySpawner의 EnemyList에 있는 현재 맵에 존재하는 적 검사
for(int i =0; i<enemySpawner.EnemyList.Count; i++)
{
float distance = Vector3.Distance(enemySpawner.EnemyList[i].transform.position, transform.position);
// 현재 감시중인 적과의 거리가 공격범위 내에 있고, 현재까지 감시한 적보다 거리가 가까우면
if(distance <= attackRange && distance <= closesDisSqr)
{
closesDisSqr = distance;
attackTarget = enemySpawner.EnemyList[i].transform;
}
}
if(attackTarget != null)
{
ChangeState(WeaponState.AttackToTarget);
}
yield return null;
}
}
/// <summary>
/// 설정된 적을 공격하는 코루틴함수
/// </summary>
/// <returns></returns>
private IEnumerator AttackToTarget()
{
while(true)
{
// 1. target이 있는지 검사(다른 발사제에 의해 제거, Goal 지점까지 이동해 삭제 등)
if(attackTarget == null)
{
ChangeState(WeaponState.SearchTarget);
break;
}
// 2. target이 공격 범위 안에 있는지 검사(공격 범위를 벗어나면 새로운 적 탐색)
float distance = Vector3.Distance(attackTarget.position, transform.position);
if(distance > attackRange)
{
attackTarget = null;
ChangeState(WeaponState.SearchTarget);
break;
}
// 3. attackRate 시간만큼 대기
yield return new WaitForSeconds(attackRate);
// 4. 공격 (발사체 생성)
archerAnimator.SetTrigger("Attack");
SpawnProjectTile();
}
}
private void SpawnProjectTile()
{
GameObject clone = Instantiate(projectTilePrefab, spawnPoint.position, spawnArrowRot);
// 생성된 발사체에게 공격대상(attackTarget) 정보 제공
clone.GetComponent<Arrow>().Setup(attackTarget,attackDamage);
}
}
타워의 공격력을 설정하고 발사체에게 타워의 공격력 정보를 매개변수로 전달함.
Arrow.cs
using UnityEngine;
/// <summary>
/// 타워가 발사하는 기본 발사체에 부착
/// </summary>
public class Arrow : MonoBehaviour
{
private Movement2D movement2D;
private Transform target;
private int damage;
public void Setup(Transform target, int damage)
{
movement2D = GetComponent<Movement2D>();
this.target = target;
this.damage = damage;
}
/// <summary>
/// 타겟이 존재하면 타겟 방향으로 이동, 타겟이 존재하지 않으면 삭제
/// </summary>
private void Update()
{
if(target != null)
{
// 발사체를 target의 위치로 이동
Vector3 direction = (target.position - transform.position).normalized;
transform.rotation = Quaternion.FromToRotation(Vector3.up, direction);
movement2D.MoveTo(direction);
}
else
{
// 발사체 오브젝트 삭제
Destroy(gameObject);
}
}
/// <summary>
/// 타겟으로 설정된 적과 부딪혔을 때 둘다 삭제
/// </summary>
/// <param name="collision"></param>
private void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("Enemy")) return; // 적이 아닌 대상과 부딪히면
if (collision.transform != target) return; // 현재 target인 적이 아닐 때
collision.GetComponent<EnemyHP>().TakeDamage(damage); // 적 사망 함수 호출
Destroy(gameObject); // 발사체 오브젝트 삭제
}
}
Setup 함수에 데미지를 매개변수로 받고 OnTrigger 함수에서 EnemyHP 체력을 깎는다.
적 체력 정보를 받기 위해 SliderUI를 생성한다. 이때 캔버스와 이벤트시스템도 함께 생성됨
핸드폰 해상도는 보통 19:10 이기 때문에 Scale With Screen Size 로 변경하고 1900 * 1000 으로 설정한다.
슬라이더의 이미지와 이름은 아래와 같이 변경하고
이미지를 9-Slice 하여 이미지를 가로로 늘려도 깨지거나 늘어나지 않도록 설정한다.
Background 이미지로 설정하도록 하고
Image Type은 Sliced 로 설정한다음 Pixcel Per Unit Multiple을 적절히 설정한다.
이렇게 만든 슬라이더를 프리팹으로 생성하고 하이러키뷰의 슬라이더는 삭제한다.
SliderPositionAutoSetter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SliderPositionAutoSetter : MonoBehaviour
{
[SerializeField]
private Vector3 distance = Vector3.up * 20;
private Transform targetTransform;
private RectTransform rectTransform;
public void Setup(Transform target)
{
// Slider UI가 쫓아다닐 target 설정
targetTransform = target;
// RectTransform 컴포넌트 정보 얻어오기
rectTransform = GetComponent<RectTransform>();
return;
}
private void LateUpdate()
{
// 적이 파괴되어 쫓아다닐 대상이 사라지면 Slider UI도 삭제
if(targetTransform == null)
{
Destroy(gameObject);
return;
}
// 오브젝트의 위치가 갱신된 이후에 Slider UI도 함께 위치를 설정하도록 하기 위해 LateUpdate()에서 호출
// 오브젝트의 월드 좌표를 기준으로 화면에서의 좌표 값을 구현
Vector3 screenPosition = Camera.main.WorldToScreenPoint(targetTransform.position);
// 화면내에서 좌표 + distance만큼 떨어진 위치를 Slider UI의 위치로 설정
rectTransform.position = screenPosition + distance;
//
}
}
EnemyHPViewer.cs
using UnityEngine;
using UnityEngine.UI;
public class EnemyHPViewer : MonoBehaviour
{
private EnemyHP enemyHP;
private Slider hpSlider;
public void Setup(EnemyHP enemyHP)
{
this.enemyHP = enemyHP;
hpSlider = GetComponent<Slider>();
}
private void Update()
{
hpSlider.value = enemyHP.CurrentHP / enemyHP.MaxHP;
}
}
EnemySpawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[Header("EnemySpawn")]
[SerializeField]
private GameObject enemyPrefab;
[SerializeField]
private GameObject enemyHPSliderPrefab;
[SerializeField]
private Transform[] myPoints;
[SerializeField]
private float spawnTime;
[Header("UI")]
[SerializeField]
private Transform canvasTransform; // UI를 표현하는 Canvas 오브젝트의 Transform
[Header("PlayerStats")]
[SerializeField]
private PlayerHP playerHP;
[SerializeField]
private PlayerGold playerGold;
private List<Enemy> enemyList;
// 적의 생성과 삭제는 EnemySpawner에서 하기 때문에 Set은 필요 없음
public List<Enemy> EnemyList => enemyList;
private void Awake()
{
// 적 리스트 메모리 할당
enemyList = new List<Enemy>();
// 적 생성 코루틴 함수 호출
StartCoroutine("SpawnEnemy");
}
/// <summary>
/// 적 스폰 코루틴 함수
/// </summary>
/// <returns></returns>
private IEnumerator SpawnEnemy()
{
while(true)
{
GameObject clone = Instantiate(enemyPrefab);
Enemy enemy = clone.GetComponent<Enemy>();
enemy.Setup(this,myPoints);
enemyList.Add(enemy); // 리스트에 방금 생성된 적 정보 저장
SpawnEnemyHPSlider(clone);
yield return new WaitForSeconds(spawnTime);
}
}
public void DestoryEnemy(EnemyDestoryType type, Enemy enemy,int gold)
{
// 적이 목표지점까지 도착했을 때
if(type == EnemyDestoryType.Arrive){
playerHP.TakeDamage(enemy.GetComponent<EnemyHP>().CurrentHP);
}
else if(type==EnemyDestoryType.Kill){
// 적의 종류에 따라 사망 시 골드 획득
playerGold.CurrentGold += gold;
}
// 리스트에서 사망하는 적 정보 삭제
enemyList.Remove(enemy);
// 적 오브젝트 삭제
Destroy(enemy.gameObject);
}
public void SpawnEnemyHPSlider(GameObject enemy)
{
// 적 체력을 나타내는 Slider UI 생성
GameObject sliderClone = Instantiate(enemyHPSliderPrefab);
// Slider UI 오브젝트를 parent("Canvas"오브젝트)의 자식으로 설정
sliderClone.transform.SetParent(canvasTransform);
// 계층 설정으로 바뀐 크기를 다시 (1,1,1)으로 설ㅊ정
sliderClone.transform.localScale = Vector3.one;
// Slider UI가 쫓아다닐 대상을 본인으로 설정
sliderClone.GetComponent<SliderPositionAutoSetter>().Setup(enemy.transform);
// Sldier UI에 자신의 체력 정보를 표시하도록 설정
sliderClone.GetComponent<EnemyHPViewer>().Setup(enemy.GetComponent<EnemyHP>());
}
}
플레이어 체력 관리
PlayerHP.cs
플레이어의 체력을 외부에서 볼 수 있도록 프로퍼티 생성
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class PlayerHP : MonoBehaviour
{
[SerializeField]
private Image imageScreen;
[SerializeField]
private float maxHP = 100;
[SerializeField]
private float currentHP;
public float MaxHP => maxHP;
public float CurrentHP => currentHP;
private void Awake()
{
currentHP = maxHP;
}
public void TakeDamage(float damage)
{
currentHP -= damage;
StopCoroutine(HitAlphaAnimation());
StartCoroutine(HitAlphaAnimation());
// 0이 되면 게임오버
if(currentHP <=0)
{
}
}
private IEnumerator HitAlphaAnimation()
{
// 전체 화면 크기로 배치된 imageScreen의 색상을 color 변수에 저장
// imageScreen의 투명도를 30% 설정
Color color = imageScreen.color;
color.a = 0.3f;
imageScreen.color = color;
// 투명도가 0이 될때까지 감소
while(color.a>=0.0f)
{
color.a -= Time.deltaTime;
imageScreen.color = color;
yield return null;
}
}
}
적이 사라질 때 플레이어에 의해 사망했는지, 끝 지점에 다다라서 사라졌는지 알기 위한 열거형 변수를 생성
Enemy.cs
using System.Collections;
using UnityEngine;
public enum EnemyDestoryType { Kill =0, Arrive }
public class Enemy : MonoBehaviour
{
private int wayPointCount; // 이동 경로 개수
private Transform[] wayPoints; // 이동 경로 정보
private int currentIndex = 0; // 현재 목표지점 인덱스
private Movement2D movement2D; // 오브젝트 이동 제어
private EnemySpawner enemySpawner; // 적의 삭제를 본인이 하지 않고 EnemySpawner에 알려서 삭제
[SerializeField]
private int gold = 10; // 적 사망 시 획득 가능한 골드
public void Setup(EnemySpawner enemySpawner, Transform[] wayPoints)
{
movement2D = GetComponent<Movement2D>();
this.enemySpawner = enemySpawner;
// 적 이동 경로 wayPoint 정보 저장
wayPointCount = wayPoints.Length;
this.wayPoints = new Transform[wayPointCount];
this.wayPoints = wayPoints;
// 적의 위치를 첫번째 wayPoint 위치로 설정
transform.position = wayPoints[currentIndex].position;
StartCoroutine(OnMove());
}
/// <summary>
/// 적 이동/목표지점 설정 코루틴
/// </summary>
/// <returns></returns>
private IEnumerator OnMove()
{
NextMoveTo();
while(true)
{
// 적의 현재위치와 목표위치의 거리가 MoveSpeed 보다 적을 때 if 조건문 실행
// Tip. MoveSpeed 를 곱해주는 이유는 속도가 빠르면 전 프레임에 0.02보다 크게 움직이기 때문에
// if 조건문에 걸리지 않고 경로를 탈주하는 오브젝트가 발생할 수 있음
if(Vector3.Distance(transform.position,wayPoints[currentIndex].position)<0.02f * movement2D.MoveSpeed)
{
NextMoveTo();
}
yield return null;
}
}
/// <summary>
/// 다음 이동 방향 설정
/// </summary>
private void NextMoveTo()
{
// 아직 이동할 wayPoints 가 남아있다면
if(currentIndex < wayPointCount - 1)
{
// 적의 위치를 정확하게 목표 위치로 설정
transform.position = wayPoints[currentIndex].position;
// 이동 방향 설정 => 다음 목표지점(wayPoints)
currentIndex++;
Vector3 direction = (wayPoints[currentIndex].position - transform.position).normalized;
movement2D.MoveTo(direction);
}
// 현재 위치가 마지막 wayPoints 라면
else
{
// 목표지점에 도달해서 사망할 때는 돈을 주지 않도록 적용
gold = 0;
// 적 오브젝트 삭제
OnDie(EnemyDestoryType.Arrive);
}
}
public void OnDie(EnemyDestoryType type)
{
// EnemySpawner에서 리스트로 적 정보를 관리하기 때문에 Destory()를 직접하지 않고
// EnemySpawner에게 본인이 삭제될 때 필요한 처리를 하도록 DestoryEnemy()함수 호출
enemySpawner.DestoryEnemy(type,this,gold);
}
}
OnDie 함수에는 EenmyDestoryType 매개변수를 받아서 처리
플레이어 스탯 UI
PannerlInfoUI 에 해당 이미지를 9-slice 해서 넣고
아래와 같이 적절히 이미지를 삽입
에셋 내에는 돈 이미지가 있어서 쉽게 사용할 수 있었는데
하트는 없어서 챗 gpt를 사용하여 이미지 생성함
이 이미지 맘에 안들긴 하지만 작게하면 괜찮아 보여서 일단 사용 중
글씨체는 에셋 사이트에서 사용중인 글씨체와 비슷한 느낌을 내기 위해 Dafont 에서 찾은
Comic Book 이라는 글씨체 사용
TextTMPViewer.cs
using UnityEngine;
using TMPro;
public class TextTMPViewer : MonoBehaviour
{
[Header("TextMeshPro")]
[SerializeField]
private TextMeshProUGUI textPlayerHP;
[SerializeField]
private TextMeshProUGUI textPlayerGold;
[Header("PlayerStats")]
[SerializeField]
private PlayerHP playerHP;
[SerializeField]
private PlayerGold playerGold;
private void Update()
{
textPlayerHP.text = playerHP.CurrentHP + "/" + playerHP.MaxHP;
textPlayerGold.text = playerGold.CurrentGold.ToString();
}
}
패널에 연결하여 사용
ImageScreen 을 생성하고 빨간색으로 색상 변경 후 Raycast Target 은 체크 해제
오브젝트의 투명도를 0으로 설정하고 코드로 작성하여
플레이어가 데미지를 입으면 30% 의 투명도를 냈다가 다시 0 으로 줄어느는 효과를 냄
플레이어 골드 관리
PlayerGold.cs
using UnityEngine;
public class PlayerGold : MonoBehaviour
{
[SerializeField]
private int currentGold = 100;
public int CurrentGold
{
set => currentGold = Mathf.Max(0, value);
get => currentGold;
}
}
외부에서 변수에 접근할 수 있도록 프로퍼티 생성
TowerSpawner.cs
using UnityEngine;
/// <summary>
/// 타워 생성 제어
/// </summary>
public class TowerSpawner : MonoBehaviour
{
[SerializeField]
private GameObject towerPrefab;
[SerializeField]
private int towerBuildGold = 50; // 타워를 건설할 때 사용되는 골드
[SerializeField]
private EnemySpawner enemySpawner; // 현재 맵에 존재하는 적 리스트 정보를 얻기 위해
[SerializeField]
private PlayerGold playerGold;
/// <summary>
/// 매개변수의 위치에 타워 생성
/// </summary>
/// <param name="tileTransform"></param>
public void SpawnTower(Transform tileTransform)
{
if (towerBuildGold > playerGold.CurrentGold)
return;
Tile tile = tileTransform.GetComponent<Tile>();
// 타워 건설 가능 여부 확인
// 현재 타일의 위치에 이미 타워가 건설되어 있으면 타워건설 x
if (tile.IsBuildTower == true)
{
return;
}
// 타워가 건설되어 있음으로 설정
tile.IsBuildTower = true;
// 타워 건설 골드만큼 플레이어 골드를 감소
playerGold.CurrentGold -= towerBuildGold;
// 선택한 타일의 위치에 타워 건설
GameObject clone = Instantiate(towerPrefab, tileTransform.position, Quaternion.identity);
// 타워 무기에 enemySpawner 정보 전달
clone.GetComponent<TowerWeapon>().Setup(enemySpawner);
}
}
타워를 설치할 때 50 골드씩 차감될 수 있도록 설정
위의 강의 영상과는 다르게 플레이어의 체력이 감소하는 방법은
적의 남아있는 체력만큼 깎이도록 설정하였다
적의 체력이 3만큼 남아있는데 홈에 들어왔다면 -3
적의 체력이 1만큼 남았있는데 홈에 들어왔다면 -1
'Unity Portfolio > TinySwordDefense' 카테고리의 다른 글
3. 타워 배치, 타워 공격 (0) | 2024.07.31 |
---|---|
2. 적 캐릭터 타일맵 이동, 애니메이션 (1) | 2024.07.23 |
1. 2D 디펜스 게임 개발 프로젝트 세팅 (8) | 2024.07.20 |