Google Play Game Services(GPGS)를 활용한 뱀서라이크 게임입니다. 플레이어는 무작위 생성되는 던전을 탐험하며 적을 처치하고 아이템을 수집합니다. 사망 시 모든 진행이 초기화되는 뱀서라이크 특성을 살리면서, 리더보드와 업적 시스템으로 플레이어의 도전 의지를 고취시켰습니다.
| 이름 | 역할 | 담당 업무 |
|---|---|---|
| 김경학 | 프로그래머 | 프로젝트 전체 개발 (던전 생성, 아이템/무기 시스템, AI, 데이터 관리, 수익화) |
무기와 아이템의 구조를 설계하기 위해 상속 구조를 활용했습니다. 공통적인 속성을 가진 기본 클래스를 정의하고, 각 타입별로 상속받아 구현했습니다.
[아이템 상속 구조]
Item (기본 클래스)
├── Weapon (무기)
│ ├── MeleeWeapon (근접 무기)
│ └── RangedWeapon (원거리 무기)
├── Consumable (소모품)
│ ├── Potion (포션)
│ └── Scroll (스크롤)
└── Equipment (장비)
├── Armor (방어구)
└── Accessory (장신구)
// 기본 아이템 클래스
public abstract class Item : ScriptableObject
{
public string itemId;
public string itemName;
public string description;
public Sprite icon;
public int maxStack;
public bool isConsumable;
public abstract void Use(GameObject user);
}
// 무기 클래스
public abstract class Weapon : Item
{
public int baseDamage;
public float attackSpeed;
public int durability;
public int maxDurability;
public abstract void Attack(GameObject target);
public void Repair()
{
durability = maxDurability;
}
}
// 근접 무기 구현
[CreateAssetMenu(fileName = "NewSword", menuName = "Data/Weapons")]
public class MeleeWeapon : Weapon
{
public float attackRange;
public override void Attack(GameObject target)
{
// 근접 공격 로직
if (Vector3.Distance(transform.position, target.transform.position) <= attackRange)
{
var damageable = target.GetComponent<IDamageable>();
damageable?.TakeDamage(baseDamage);
}
}
}
// 원거리 무기 구현
[CreateAssetMenu(fileName = "NewBow", menuName = "Data/Weapons")]
public class RangedWeapon : Weapon
{
public GameObject projectilePrefab;
public float projectileSpeed;
public override void Attack(GameObject target)
{
// 투사체 발사 로직
var projectile = Instantiate(projectilePrefab, transform.position);
projectile.GetComponent<Projectile>().Initialize(target, baseDamage, projectileSpeed);
}
}
던전 생성 시 타일 프리팹을 매번 인스턴스화하지 않고 오브젝트 풀을 사용하여 인스턴스화 비용을 최소화했습니다. 수명이 다한 오브젝트가 자동으로 풀로 반환되도록 구현했습니다.
// 오브젝트 풀 구현
public class ObjectPool<T> where T : MonoBehaviour
{
private Queue<T> pool = new Queue<T>();
private T prefab;
private Transform parent;
private int initialSize;
public ObjectPool(T prefab, Transform parent, int initialSize)
{
this.prefab = prefab;
this.parent = parent;
this.initialSize = initialSize;
PreWarm();
}
private void PreWarm()
{
for (int i = 0; i < initialSize; i++)
{
T obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
public T Get()
{
if (pool.Count > 0)
{
T obj = pool.Dequeue();
obj.gameObject.SetActive(true);
return obj;
}
return Object.Instantiate(prefab, parent);
}
public void Return(T obj)
{
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
// 오브젝트 풀 매니저
public class PoolManager : MonoBehaviour
{
private static PoolManager instance;
private Dictionary<string, object> pools = new Dictionary<string, object>();
public static PoolManager Instance
{
get
{
if (instance == null)
{
GameObject go = new GameObject("PoolManager");
instance = go.AddComponent<PoolManager>();
DontDestroyOnLoad(go);
}
return instance;
}
}
public void CreatePool<T>(string key, T prefab, int size, Transform parent) where T : MonoBehaviour
{
pools[key] = new ObjectPool<T>(prefab, parent, size);
}
public T GetFromPool<T>(string key) where T : MonoBehaviour
{
if (pools.TryGetValue(key, out var pool))
{
return ((ObjectPool<T>)pool).Get();
}
return null;
}
public void ReturnToPool<T>(string key, T obj) where T : MonoBehaviour
{
if (pools.TryGetValue(key, out var pool))
{
((ObjectPool<T>)pool).Return(obj);
}
}
}
Unity Google Sheets (UGS) 라이브러리를 사용하여 데이터 관리 시스템을 구현했습니다. 구글 시트에 적용된 데이터를 빠르게 게임에 적용하여 밸런스 수정 시간을 단축했습니다.
// UGS 데이터 로더
public class UGSDataLoader : MonoBehaviour
{
public string spreadsheetId;
public int sheetIndex = 0;
private void Start()
{
LoadMonsterData();
LoadStageData();
LoadItemData();
}
private async void LoadMonsterData()
{
try
{
var data = await GoogleSheetsReader.Read(
spreadsheetId,
sheetIndex,
"Monsters!A2:F100"
);
var monsterTable = ParseMonsterData(data);
MonsterDataManager.Instance.SetMonsterData(monsterTable);
}
catch (Exception e)
{
Debug.LogError($"Failed to load monster data: {e.Message}");
}
}
private Dictionary<string, MonsterData> ParseMonsterData(string[,] data)
{
var monsterTable = new Dictionary<string, MonsterData>();
for (int row = 0; row < data.GetLength(0); row++)
{
string id = data[row, 0];
if (string.IsNullOrEmpty(id)) continue;
monsterTable[id] = new MonsterData
{
id = id,
name = data[row, 1],
hp = int.Parse(data[row, 2]),
attack = int.Parse(data[row, 3]),
defense = int.Parse(data[row, 4]),
expReward = int.Parse(data[row, 5])
};
}
return monsterTable;
}
}
// 몬스터 데이터 매니저
public class MonsterDataManager : MonoBehaviour
{
private static MonsterDataManager instance;
private Dictionary<string, MonsterData> monsterData;
public static MonsterDataManager Instance
{
get
{
if (instance == null)
{
GameObject go = new GameObject("MonsterDataManager");
instance = go.AddComponent<MonsterDataManager>();
DontDestroyOnLoad(go);
}
return instance;
}
}
public void SetMonsterData(Dictionary<string, MonsterData> data)
{
monsterData = data;
Debug.Log($"Loaded {data.Count} monsters.");
}
public MonsterData GetMonsterData(string id)
{
if (monsterData.TryGetValue(id, out var data))
{
return data;
}
Debug.LogWarning($"Monster data not found: {id}");
return null;
}
}
// 몬스터 데이터 클래스
[System.Serializable]
public class MonsterData
{
public string id;
public string name;
public int hp;
public int attack;
public int defense;
public int expReward;
}
// 스테이지 데이터 로드
private async void LoadStageData()
{
try
{
var data = await GoogleSheetsReader.Read(
spreadsheetId,
sheetIndex,
"Stages!A2:H50"
);
var stageTable = ParseStageData(data);
StageDataManager.Instance.SetStageData(stageTable);
}
catch (Exception e)
{
Debug.LogError($"Failed to load stage data: {e.Message}");
}
}
private List<StageData> ParseStageData(string[,] data)
{
var stageTable = new List<StageData>();
for (int row = 0; row < data.GetLength(0); row++)
{
stageTable.Add(new StageData
{
stageId = data[row, 0],
name = data[row, 1],
floorCount = int.Parse(data[row, 2]),
minMonsterLevel = int.Parse(data[row, 3]),
maxMonsterLevel = int.Parse(data[row, 4]),
bossId = data[row, 5],
stageDifficulty = float.Parse(data[row, 6]),
goldReward = int.Parse(data[row, 7])
});
}
return stageTable;
}
구글 시트에 몬스터 데이터와 스테이지 정보를 입력하고, UGS 라이브러리를 통해 실시간으로 데이터를 로드합니다. 이를 통해 빠르게 밸런스 수정이 가능했습니다.
[구글 시트 구조 예시]
Monsters 시트:
| ID | Name | HP | Attack | Defense | Exp Reward |
|----|------|----|--------|---------|------------|
| M001 | 슬라임 | 50 | 10 | 5 | 20 |
| M002 | 고블린 | 80 | 15 | 8 | 35 |
| M003 | 오크 | 150 | 25 | 12 | 80 |
Stages 시트:
| ID | Name | Floor Count | Min Lv | Max Lv | Boss ID | Difficulty | Gold Reward |
|----|------|-------------|---------|---------|-----------|-------------|
| S001 | 숲의 던전 | 5 | 1 | 10 | B001 | 1.0 | 100 |
| S002 | 동굴 던전 | 8 | 5 | 20 | B002 | 1.5 | 200 |
| S003 | 화산 던전 | 12 | 15 | 30 | B003 | 2.0 | 350 |
Google Play Game Services(GPGS)를 활용하여 전 세계 플레이어와 점수를 경쟁할 수 있는 리더보드 시스템을 구현했습니다.
// GPGS 매니저
public class GPGSManager : MonoBehaviour
{
private void Start()
{
PlayGamesPlatform.Activate();
}
public void SubmitScore(long score, string leaderboardId)
{
Social.ReportScore(score, leaderboardId, (bool success) =>
{
if (success)
{
Debug.Log("Score submitted successfully.");
}
else
{
Debug.LogWarning("Score submission failed.");
}
});
}
public void ShowLeaderboard(string leaderboardId)
{
Social.ShowLeaderboardUI(leaderboardId);
}
}
Unity IAP를 사용하여 인앱결제 상품을 설계하고, AdMob SDK를 사용하여 광고 시스템을 구현했습니다.
// IAP 매니저
public class IAPManager : MonoBehaviour
{
// 상품 종류
public enum ProductType
{
Currency, // 골드
Revive, // 부활
StarterPack // 스타터 팩
}
public async void PurchaseProduct(string productId)
{
try
{
var product = _storeController.products.WithID(productId);
await _storeController.InitiatePurchase(product);
}
catch (Exception e)
{
HandlePurchaseError(e);
}
}
}
// 보상형 광고 매니저
public class RewardAdManager : MonoBehaviour
{
private RewardedAd _rewardedAd;
public void LoadRewardAd()
{
string adUnitId = "ca-app-pub-xxx/yyy";
RewardedAd.Load(adUnitId, CreateAdRequest(),
(RewardedAd ad, LoadAdError error) =>
{
if (error != null)
{
Debug.LogError("Reward ad load failed");
return;
}
_rewardedAd = ad;
});
}
public void ShowRewardAd(Action<Reward> onRewardEarned)
{
if (_rewardedAd != null && _rewardedAd.CanShowAd())
{
_rewardedAd.Show((Reward reward) =>
{
onRewardEarned?.Invoke(reward);
});
}
}
}
와일드 템페스트 프로젝트는 뱀서라이크 장르의 핵심 요소(무한한 플레이, 난이도 밸런스, 전략적 전투)를 구현하고 데이터 주도 개발 방식을 통해 유지보수성을 높였습니다.
public class DungeonGenerator : MonoBehaviour
{
private int width = 50;
private int height = 50;
private int[,] map;
private void GenerateDungeon()
{
map = new int[width, height];
// 셀룰러 오토마타 기반 맵 생성
CellularAutomata();
// 경로 검증
if (!ValidatePath())
{
GenerateDungeon(); // 재생성
}
}
private void CellularAutomata()
{
// 초기화
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
map[x, y] = Random.value < 0.45f ? 1 : 0;
}
}
// 반복 적용
for (int i = 0; i < 5; i++)
{
ApplyCellularAutomataRules();
}
}
}
public class EnemyAI : MonoBehaviour
{
private enum State { Idle, Patrol, Chase, Attack, Flee }
private State currentState;
private IAttackStrategy attackStrategy;
private void Update()
{
switch (currentState)
{
case State.Idle:
if (HasSpottedPlayer()) ChangeState(State.Chase);
break;
case State.Patrol:
PatrolBehavior();
if (HasSpottedPlayer()) ChangeState(State.Chase);
break;
case State.Chase:
MoveTowardsPlayer();
if (IsInAttackRange()) ChangeState(State.Attack);
if (IsLowHealth()) ChangeState(State.Flee);
break;
case State.Attack:
attackStrategy.Execute(this);
if (!IsInAttackRange()) ChangeState(State.Chase);
break;
case State.Flee:
MoveAwayFromPlayer();
if (IsSafe()) ChangeState(State.Patrol);
break;
}
}
}