2023 경기콘텐츠창의학교 최우수상 프로젝트
깃허브 : https://github.com/dkdkdsa/Weak-Demon-Lord-Defense
다운로드 : https://drive.google.com/drive/folders/1_5sdus83Eqdhc8Wn0MTYH4lhE-NTRyZk?usp=drive_link
영상 : https://www.youtube.com/watch?v=SWcP7Odgtaw
1. 게임 개요
장르 : 전략 디펜스
개발 인원 : 4명(2학년 4명)
설명 : 2023 경기콘텐츠창의학교 대회를 위하여 2학년 4명이서 개발한 프로젝트 입니다
저는 이 프로젝트에서 매인 개발을 담당했고 경기콘텐츠창의학교에서 최우수상이라는 성적을 달성하였습니다
기획 의도 : 게임을 기획하며 보통 매체에서 강하게 그려지는 마왕이 오히려 약하면 어떨까? 라는 아이디어가 나와서 마왕이 약해 마왕성으로 쳐들어온 용사들을 용병을 고용하여 막아내는 디펜스 게임을 만들면 재미있겠다 라고 생각하여 기획하게 되었다
2. 개발 전 구상
약한 마왕이 용병을 공용
용병이 마왕성을 향해 몰려온다
2.5D느낌의 그래픽
3. 구현
유닛
용병과 용사들을 같은 스크립트를 공유하고 다른 타겟만 가질 수 있도록 작성하였다 용병과 용사의 값만 바꾸면 최대한 여러 타입들이 나올 수 있도록 디자인 하였 NavMesh를 사용하여 길찾기 AI를 구현하였고 FSM을 응용하여 상태를 관리하도록 디자인 하였다 그 이외에도 아이템 장착 기능도 구현하여 높은 웨이브에 가더라도 생존이 용이하도록 하였다
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class UnitDataController : MonoBehaviour
{
[field: Header("값")]
[field: SerializeField] public float moveSpeed { get; protected set; }
[field: SerializeField] public float attackPower { get; protected set; }
[field: SerializeField] public float defenceValue { get; protected set; }
[field: SerializeField] public float attackTime { get; protected set; }
[field: SerializeField] public float skillCoolDown { get; protected set; }
[field: SerializeField] public float maxHP { get; protected set; }
[field: SerializeField] public SkillRoot skill { get; protected set; }
[field: SerializeField] public LayerMask targetLayer { get; protected set; }
[field: SerializeField] public HPBar hPBarPrefab { get; protected set; }
[field: SerializeField] public float lvUpValue { get; protected set; }
[field: SerializeField] public float lvUpCost { get; protected set; }
[field: SerializeField] public string unitName { get; protected set; }
[field: Space]
[field: Header("AI")]
[field: SerializeField] public float range { get; protected set; }
[field: SerializeField] public float attackAbleRange { get; protected set; }
[field: Space]
[field: Header("Mat")]
[field: SerializeField] public Material rootMat { get; protected set; }
protected UnitAnimator animator;
protected HPBar hpBar;
protected Dictionary<ItemType, ItemData?> itemContainer = new();
protected SpriteRenderer head, body, pants1, pants2, weapon;
protected FeedBackPlayer feedBackPlayer;
protected TMP_Text levelupText;
public event Action OnValueChanged;
public int lv { get; protected set; } = 1;
public float currentHP { get; protected set; }
public float extraAttack { get; protected set; }
public float extraDef { get; protected set; }
public float extraHP { get; protected set; }
public bool attackAble { get; set; } = true;
public bool skillAble { get; set; } = true;
protected virtual void Awake()
{
currentHP = maxHP;
feedBackPlayer = GetComponent<FeedBackPlayer>();
levelupText = GetComponentInChildren<TMP_Text>();
levelupText?.gameObject.SetActive(false);
animator = transform.Find("Visual/UnitRoot").GetComponent<UnitAnimator>();
skill = Instantiate(skill);
hpBar = Instantiate(hPBarPrefab, transform.position + new Vector3(0, 2, 0.3f), Quaternion.Euler(45, 0, 0), transform);
head = transform.Find("Visual/UnitRoot/Root/BodySet/P_Body/HeadSet/P_Head/P_Helmet/11_Helmet1").GetComponent<SpriteRenderer>();
body = transform.Find("Visual/UnitRoot/Root/BodySet/P_Body/Body/P_ArmorBody/BodyArmor").GetComponent<SpriteRenderer>();
pants1 = transform.Find("Visual/UnitRoot/Root/P_LFoot/P_LCloth/_2L_Cloth").GetComponent<SpriteRenderer>();
pants2 = transform.Find("Visual/UnitRoot/Root/P_RFoot/P_RCloth/_11R_Cloth").GetComponent<SpriteRenderer>();
weapon = transform.Find("Visual/UnitRoot/Root/BodySet/P_Body/ArmSet/ArmL/P_LArm/P_Weapon/L_Weapon").GetComponent<SpriteRenderer>();
}
public virtual void TakeDamage(float damage)
{
damage -= defenceValue + extraDef;
damage = Mathf.Clamp(damage, 0f, maxHP + extraHP);
SoundManager.Instance.PlaySound("HitHurt");
if (damage == 0)
{
feedBackPlayer.PlayFeedback(-1);
}
else
{
feedBackPlayer.PlayFeedback((int)damage);
}
currentHP -= damage;
if(currentHP <= 0)
{
animator.SetDie();
}
OnValueChanged?.Invoke();
}
private void Update()
{
}
private void SettingValue(ItemType type, bool remove = false)
{
var item = itemContainer[type];
if (!remove)
{
extraAttack += item.Value.attack;
extraDef += item.Value.defense;
extraHP += item.Value.hp;
}
else
{
extraAttack -= item.Value.attack;
extraDef -= item.Value.defense;
extraHP -= item.Value.hp;
}
currentHP = maxHP + extraHP;
OnValueChanged?.Invoke();
}
private void SettingItemSpriet(Sprite sprite, ItemType type)
{
switch (type)
{
case ItemType.Body:
body.sprite = sprite;
break;
case ItemType.Head:
head.sprite = sprite;
break;
case ItemType.Pants:
pants1.sprite = sprite;
pants2.sprite = sprite;
break;
case ItemType.Weapon:
weapon.sprite = sprite;
break;
}
}
public bool EquipItem(ItemData item)
{
if (itemContainer.ContainsKey(item.type)) return false;
itemContainer.Add(item.type, item);
SettingValue(item.type);
SettingItemSpriet(item.itemSprite, item.type);
return true;
}
public bool ReleaseItem(ItemType type)
{
if (!itemContainer.ContainsKey(type)) return false;
SettingItemSpriet(null, type);
SettingValue(type, true);
itemContainer.Remove(type);
return true;
}
public ItemData? GetItem(ItemType type)
{
if (itemContainer.ContainsKey(type))
{
return itemContainer[type];
}
return null;
}
public void LvUp()
{
lv++;
extraHP += lvUpValue;
extraAttack += lvUpValue / 5;
extraDef += lvUpValue / 10;
lvUpCost += lvUpValue * 2;
currentHP = maxHP + extraHP;
if(levelupText != null)
{
Vector3 settingPos = levelupText.transform.position + new Vector3(0, 1, 0);
Vector3 orgPos = levelupText.transform.position;
levelupText.transform.position = settingPos;
levelupText.gameObject.SetActive(true);
levelupText.transform.DOMoveY(orgPos.y, 0.5f).SetEase(Ease.OutBounce).OnComplete(()
=> { levelupText.gameObject.SetActive(false); });
}
OnValueChanged?.Invoke();
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
var old = Gizmos.color;
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, range);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackAbleRange);
Gizmos.color = old;
}
#endif
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UnitController : StateController<UnitState>
{
private void Awake()
{
controllState.Add(UnitState.Idle, new IdleState(transform, this));
controllState.Add(UnitState.Skill, new SkillState(transform, this));
controllState.Add(UnitState.Attack, new AttackState(transform, this));
controllState.Add(UnitState.Move, new MoveState(transform, this));
controllState.Add(UnitState.Die, new DieState(transform, this));
currentState = UnitState.Idle;
}
}
파이어베이스
파이어베이스를 사용하여 회원가입 로그인 랭킹보드등을 구현하였다
using Firebase.Auth;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Firebase.Database;
using Firebase;
using System.Linq;
using System.Threading.Tasks;
using System;
[System.Serializable]
public struct UserData
{
public string email;
public string userName;
public int time;
public int maxWave;
}
public class AuthManager
{
public static AuthManager instance;
public UserData userData;
private FirebaseAuth auth;
private DatabaseReference database;
public AuthManager()
{
auth = FirebaseAuth.DefaultInstance;
database = FirebaseDatabase.DefaultInstance.RootReference;
userData = new UserData();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Init()
{
instance = new AuthManager();
}
public void SingUpComplete(string email, string userName)
{
userData = new UserData();
userData.email = email;
userData.userName = userName;
database.Child(userName).SetRawJsonValueAsync(JsonUtility.ToJson(userData));
}
public void Setting()
{
database.Child(userData.userName).SetRawJsonValueAsync(JsonUtility.ToJson(userData));
}
public async void SingUp(string email, string password, string userName, Action<bool> comp)
{
if (userData.userName != null) return;
try
{
var res = await auth.CreateUserWithEmailAndPasswordAsync(email, password);
SingUpComplete(email, userName);
comp?.Invoke(true);
}
catch(System.Exception e)
{
Debug.Log("이미 가입된 email");
Debug.Log(e);
comp?.Invoke(false);
}
}
public async void Login(string email, string password, Action<bool> comp)
{
if (userData.userName != null) return;
try
{
await auth.SignInWithEmailAndPasswordAsync(email, password);
var res = await database.GetValueAsync();
var s = res.Children.ToList().Find((x) =>
{
var r = (IDictionary)x.Value;
if (r["email"].ToString() == email)
{
return true;
}
return false;
});
var obj = (IDictionary)s.Value;
userData.userName = obj["userName"].ToString();
userData.email = obj["email"].ToString();
userData.time = int.Parse(obj["time"].ToString());
userData.maxWave = int.Parse(obj["maxWave"].ToString());
comp?.Invoke(true);
}
catch(System.Exception e)
{
Debug.Log(e.ToString());
comp?.Invoke(false);
}
}
public async Task<List<IDictionary>> GetAllValue()
{
var res = await database.GetValueAsync();
List<IDictionary> ress = new List<IDictionary>();
Debug.Log(res.ChildrenCount);
res.Children.ToList().ForEach((x) =>
{
var obj = (IDictionary)(x.Value);
if(obj != null)
{
ress.Add(obj);
}
});
return ress;
}
}
4. 프로젝트의 문제점
경기콘텐츠창의학교를 너무 급하게 준비하다 보니 코드가 더럽고 구조적이지 않은 부분이 많은거 같다
5. 프로젝트로 느낀점
경기콘텐츠창의학교라는 첫 외부대회를 나간다는 경험이 너무 재미있었고 그 대회에서 최우수상을 수상하여 부듯했다
'포트폴리오 > 팀 프로젝트' 카테고리의 다른 글
[팀 프로젝트] Ancient City : Pottery (0) | 2024.04.29 |
---|---|
[팀 프로젝트] 참새의 여행 (0) | 2024.03.06 |
[팀 프로젝트] Hypertension (0) | 2024.03.05 |