리소스
게임을 테스트 하기 위해선 우선 시각적으로 보여야 하는 리소스들이 필요하다.
각 팀의 성채와 소환되어질 캐릭터, 지면, 게임의 배경등이 우선 가장 필요한 리소스들이라 생각했다.
저작권 없이 자유롭게 사용 가능한 아래 소스들을 다운받아서 유니티 프로젝트를 만들고 에셋들을 임포팅한다.
방법은 유니티 화면에 프로젝트 창에 끌얻 넣기만 하면 됨으로 혹시나 이 글을 보는 분들이 있다면 다른 강좌를 찾아보시길 추천드린다.
아래는 해당 리소스들의 다운 링크이다.
캐릭터 리소스
https://sventhole.itch.io/bandits
https://sventhole.itch.io/hero-knight
배경과 지면
https://edermunizz.itch.io/free-pixel-art-hill
성채
https://piposchpatz.itch.io/sprites-buildings-random
구조 정하기
어떤 프로그램을 만들던, 무엇을 어떻게 만들것인지에 대한 계획을 잘 만들어 두는 것이 가장 중요하다.
그리고 그 구조를 확장성 있게 잘 만들어 두는 것이 추후 기능 추가를 할 때 필요한 작업을 최소한으로 줄일 수 있게 된다.
게임 오브젝트
전쟁시대종류의 게임에는 크게 4종류의 오브젝트들이 존재한다.
- 환경오브젝트
게임의 배경화면과 캐릭터들이 걸어서 이동하는 지면에 해당한다.유니티에서 제공하는 기능이 있으므로 화면에 배치하고, 유니티 컴포넌트를 붙여 주는 것 만으로 쉽게 구현된다. - 캐릭터 유닛
이름, HP, 방어력, 이동속도, 공격력, 공격 인식 범위, 공격 후 다음 공격까지 걸리는 쿨타임, 공격 행동에 걸리는 시간, 스폰되기까지 걸리는 대기시간, 팀 정보 등 캐릭터가 소환되고, 이동하며, 공격과 데미지 처리에 필요한 요소들을 기본으로 포함한다.
시야 내에서 가장 가까운 적을 찾아 공격하며, 시야 내에 적이 없으며 공격 범위 내에 성채가 있을 경우 성채를 공격한다. - 성채 유닛
플레이어의 업그레이드에 따라 HP, 방어력 상승이 가능하며, 방어탑 유닛을 업그레이드를 통해 장착이 가능하다.
캐릭터들은 이곳에서 스폰된다. - 방어탑 유닛
성채 유닛을 업그레이드 하면 장착되는 부품이다. 일정 거리 안으로 적 캐릭터가 들어 오면 해당 캐릭터를 향해 공격한다.
시스템
이 유닛들의 이벤트들과 행동을 핸들링하는 시스템들 또한 존재해야 한다..
- ♟ 유닛 스폰 시스템
유닛 스폰을 담당한다. - ⚔ 공격 처리 시스템
공격 이벤트 발생에 대한 계산, 유닛 사망에 따른 점수 반영을 담당한다. - 💸 경제 시스템
다른 시스템들과 연계하여 각 팀별 점수를 처리한다. - 🎮 게임 상태 매니징 시스템
게임 시작, 정지, 게임오버등의 게임 진행을 담당한다. - 🖥 UI 매니징 시스템
게임 속 UI를 캐싱 해 두었다가 변경을 요청한 시스템에 제공한다. - 💾 캐릭터 유닛데이터 제공 시스템
캐릭터별 정보들을 저장해 두었다가 필요한 시스템에 이를 제공한다.
필요 이유: 메모리 낭비와 중복된 코드 작성 방지를 위해서.
캐릭터별로 공격, 이동, 스폰에 필요한 정보 등이 서로 다르므로 별도로 저장해 두어야 하며,
이 시스템이 없으면 필요할 때 마다 데이터를 메모리에 가져 오는 작업을 행해야 함으로 이를 막기 위해 필요하다.
작업 우선순위 정하기
작업 우선 순위를 정하는 것은 매우 중요하다.
작업 우선 순위를 정하면 이 작업 뒤에 무엇을 할 것인지에 대한 고민을 줄이고 작업에 집중할 수 있기 때문이다.
물론 지금 정하는 작업 우선 순위는 추후에 작업 해 보면서 바뀔 수 있을지도 모르지만... 🙄
필요한것을 생각해 보자
- 전쟁시대 종류의 게임에서 가장 중요한 것은 유닛이다.
유닛이 있어야 게임을 시작할 수 있다. 이는 성채, 캐릭터 모두를 포함하는 최상위 class가 필요한 셈이다. - 캐릭터 유닛과 성채 유닛이 필요하다.
둘 다 체력을 갖고 있고 피격을 당할 수 있지만, 성채는 움직일 수 없고, 공격도 할 수 없다.
반대로 캐릭터는 움직일 수 있고 공격할 수도 있다. 대분류 상으로 두 종류는 유닛에 속하지만 같은 종류는 아니다.
따라서 유닛을 상속받는 별도의 2개의 class가 필요하다. - 2번을 완성했다면 이제 캐릭터 스폰 기능의 구현이 필요하다.
스폰이 되어야 게임을 시작할 수 있다. (UI 구현은 코드상으로도 스폰 하도록 만들 수 있기 때문에 우선순위는 나중에...) - 스폰을 하려면 게임상에 표시될 오브젝트들 구현이 필요하다
그래야 유니티 컴포넌트를 붗일 수 있고 내가 코드를 정상적을 잘짰는지 확인 가능하기 때문. - 어떤 유닛을 어떤 상태로 스폰할지 기본 데이터 구현이 필요하다.
요약하자면..
- 스폰시스템과 캐릭터 유닛 데이터 제공 시스템 구현을 목표로 화면에 표시 가능한 수준으로 유닛을 배치
- Unit class를 만든 뒤 이를 부모로 삼는 성채 Unit과 캐릭터 Unit class를 만든다.
- 캐릭터 기본 데이터 구조체와 이를 저장해 두는 시스템을 만든다.
- 스폰 시스템을 구현한다.
오브젝트 배치
매우 기본적인 기획이 끝났으니 코드작업을 하기에 앞서 오브젝트들을 배치해 보자.
방어탑은 업그레이드 기능 추가를 고려하여 아래에서 위로 천천히 한 층씩 쌓아 갈 수 있도록 조각으로 나누어 배치했다.
모든 이미지는 z축값은 0으로하되 Sorting Layer를 조절하여 화면에 보이도록 했다.
코딩 - Unit class
유닛을 요약한다면...
유닛은 피격이 가능한 모든 오브젝트이다.
유닛은 [캐릭터와 성채] 두 종류로 만들어 질 것임을 앞선 기획에서 다루었다. 그리고 캐릭터유닛과 성채 유닛 클래스는 Unit 클래스를 상속 하도록 구조를 구상하였기에, 캐릭터와 성채의 클래스가 가지는 공통 요소를 생각해 볼 필요가 있다.
두 유닛종류의 특징을 나열해 보자
성채
1. HP와 방어력을 가진다.
2. 공격 받아 파괴 될 수 있다.
3. 업그레이드가 가능하다.
4. 이곳에서 캐릭터를 스폰한다.
캐릭터
1. HP와 방어력을 가진다.
2. 업그레이드가 가능하다.
3. 공격 받아 사망 할 수 있다.
4. 공격 할 수 있다.
5. 이동 가능하다.
6. 성체에서 소환되며, 소환에 필요한 정보를 갖고 있다.
7. (그외에 이동, 공격, 소환당함 등에 필요한 정보들과 기능들)
성채 유닛은 공격 받을 수 있지만 다른 유닛을 직접적으로 공격 할 수 없고, 방어탑이 이 기능을 대신한다.
반면 캐릭터 유닛은 공격을 받을수 있고 공격을 할 수도 있다.
즉 유닛은 업그레이드와 피격 가능한 오브젝트가 되는 것이다.
IUnit 인터페이스
우선 Unit 코드를 작성하기 전 어떤 기능이 필요할 지 더 자세하게 생각해 보았다.
피격이 된다면 우선 체력 감소 기능이 필요하다고 생각되어 HP, Armor, TakeDemage(Attack attack) 함수를 만들었다.
Attack은 구조체로, 공격을 하는 쪽에서 만들어 내는 공격에 대한 정보이다.
처음에는 계산 된 데미지 숫자값을 인자로 받아 오도록 하려 했으나, 전생시대 원본 게임을 보면 원거리 공격을 하는 캐릭터, 근거리 공격을 하는 캐릭터 등 다양항 공격 방식을 보여 주고 있기 때문에 다르게 구현해야 한다는 생각을 하게 되었고, 이 정보를 한번에 받아 올 수 있도록 이렇게 만들었다.
enum 값인 Team은, 컴퓨터 팀과 유저 팀을 구분할 수 있게 하기 위해 사용했으며, 유닛이 공격받다가 일정 시점이 되어 HP가 0 이하로 떨어 지는 시점이 오면 여기에 필요한 유닛 하나에만 국한 되지 않는 기능을 수행하도록 DieEvent를 발동 시키기 위해 코드를 추가 했다.
using UnityEngine.Events;
public enum Team { None, Red, Blue }
public interface IUnit : IInitializable
{
// 소속팀
public Team Teams { get; }
// 체력
public float HP { get; }
// 방어력
public float Armor { get; }
// 유닛 사망 시 발생하는 이벤트입니다.
public UnityEvent DieEvent { get; }
/// <summary>
/// 데미지를 받는 쪽의 실제 HP 감소 연산을 수행합니다.
/// <see cref="Armor"/>값이 높을수록 받는 데미지 양은 감소합니다.
/// </summary>
/// <param name="attack">데미지 받게 할 양입니다.</param>
/// <returns>데미지를 받은 후 남은 HP양 입니다.</returns>
public float TakeDemage(Attack attack);
}
Attack 구조체
공격을 발생 시키는 오브젝트에서 Attack 변수를 생성하고, 매개변수에 인자값으로 넘겨 주면, 공격 방식에 따라 달라지는 공격 계산식을 GetDemageAmount() 함수에서 계산하여 반환하도록 구성할 생각이다.
추후 기획에 따라 많이 달라질 것 같아서 일단은 러프하게 방향성만 잡아 두었다.
public enum AttackStyle { Projectile, ShortRange } // 장거리, 근접
public enum AttackPowerType { Normal, Magical } // 일반, 마법
public struct Attack
{
// 공격력
public float AttackPower { get => _atkPower; }
// 공격 방식
public AttackStyle attackStyle { get => _atkStyle; }
// 공격 종류
public AttackPowerType attackPowerType { get => _atkPowerType; }
private float _atkPower;
private AttackStyle _atkStyle;
private AttackPowerType _atkPowerType;
public Attack(float attackPower)
{
_atkPower = attackPower;
_atkStyle = AttackStyle.ShortRange;
_atkPowerType = AttackPowerType.Normal;
}
public Attack(float attackPower, AttackStyle attackStyle)
{
_atkPower = attackPower;
_atkStyle = attackStyle;
_atkPowerType = AttackPowerType.Normal;
}
public Attack(float attackPower, AttackStyle attackStyle, AttackPowerType powerType)
{
_atkPower = attackPower;
_atkStyle = attackStyle;
_atkPowerType = powerType;
}
public float GetDemageAmount()
{
return AttackPower;
}
}
Unit 클래스
하위 class에서 필요한 남은 기능들을 구현하고 덮어 씌우도록 할 것이기 때문에 abstract키워드를 붙여 추상클래스로 선언했다.
using UnityEngine;
using UnityEngine.Events;
public abstract class Unit : MonoBehaviour, IUnit
{
public Team Teams => _team;
public float HP => _hp;
public float Armor => _armor;
public UnityEvent DieEvent => _dieEvent;
private Team _team;
private float _hp = 100;
private float _armor = 0;
private UnityEvent _dieEvent;
private void Awake()
{
Initialize();
}
public virtual void Initialize()
{
if (_dieEvent is null) _dieEvent = new UnityEvent();
_dieEvent.AddListener(OnDie);
}
public float TakeDemage(Attack attack)
{
float actualDemage = Armor - attack.GetDemageAmount();
if (actualDemage <= 0) return HP; //Miss Demage
if ((HP - actualDemage) <= 0) // Dead
{
DieEvent.Invoke();
return 0;
}
else
{
_hp = _hp - actualDemage;
return HP;
}
}
protected virtual void OnDie() { }
}
CastleUnit 클래스
성채 유닛의 class이다. 기본적으로 성채 유닛의 기능은 Unit 클래스와 다른점이 많지 않아 코드가 짧다.
성채 파괴 시 필요한 행동에 대한 함수만 재정의 하면 될 정도니...
아직 성채 파괴 시 작동해야 할 다른 시스템이 구축 되지 않았음으로 일단 로그만 남기도록 했다.
public class CastleUnit : Unit
{
protected override void OnDie()
{
Debug.Log("END Game!");
}
}
CharacterUnit 클래스
우선 필요한 변수만 먼저 나열해 두었다.
적을 감지하는 방식, 공격 정보를 보내는 방식 등에 대한 고민이 있어서... 다음 편에 적어 둬야할 듯하다.
using UnityEngine;
public enum LookDirection { Left, Right }
[RequireComponent(typeof(SpriteRenderer))]
public class CharacterUnit : Unit
{
public string CharacterName { get => _characterName; }
public float MoveSpeed { get => _moveSpeed; }
public float ATK { get => _atk; }
public float EnemyDetectionRadious { get => _enemyDetectRadius; }
public float NormalAttackCooldown { get => _normalAttackCooldown; }
public float NormalAttackCastingTime { get => _noramlAttackCastingTime; }
public float SpawnWaitingTime { get => _spawnWaitingTime; }
/// <summary>
/// 기본 시선 방향
/// </summary>
[SerializeField]
public LookDirection defaultLookDirection;
protected Transform AttackTargetPos
{
get
{
if (AttackTarget is null)
throw new System.NullReferenceException("공격 대상의 좌표를 가져 오려 했으나 현재 이 유닛은 공격대상이 비어 있습니다.");
return AttackTarget.transform;
}
}
protected Unit AttackTarget;
private string _characterName = "Unit";
private float _moveSpeed = 1f;
private float _atk = 1f;
private float _enemyDetectRadius = 1f;
private float _normalAttackCooldown = 1f;
private float _noramlAttackCastingTime = 0.25f;
private float _spawnWaitingTime = 1f;
public override void SetTeam(Team settingValue)
{
if (settingValue == Team.Blue && defaultLookDirection == LookDirection.Left
|| settingValue == Team.Red && defaultLookDirection == LookDirection.Right)
{
GetComponent<SpriteRenderer>().flipX = true;
}
base.SetTeam(settingValue);
}
}
'개발 > 게임 개발' 카테고리의 다른 글
간단한 유니티 2D 타워 디펜스 게임 만들기 5편 - 캐릭터 이동 (0) | 2022.10.03 |
---|---|
간단한 유니티 2D 타워 디펜스 게임 만들기 4편 - 캐릭터 스폰 (0) | 2022.09.24 |
간단한 유니티 2D 타워 디펜스 게임 만들기 3편 - 캐릭터 유닛의 행동에 관한 고려 (0) | 2022.09.13 |
간단한 유니티 2D 타워 디펜스 게임 만들기 1편 - 개요 (0) | 2022.08.21 |