병합 과정에서의 시간이 오래 걸려서 새벽까지 진행하게 되었습니다.
결국 최종적으로 완료했고 모바일 빌드까지 완성했습니다.
스파르타 코딩클럽 10기, 유니티 입문 팀 프로젝트를 진행했습니다.
void GenerateRooms()
{
// 시작 지점 (0,0)
Vector2Int currentPos = Vector2Int.zero;
// 방 개수 랜덤 지정
roomCount = UnityEngine.Random.Range(8, 12);
while (createRoomCount < roomCount)
{
// 방이 없는 좌표에만 방 생성
if (!roomInstances.ContainsKey(currentPos))
{
GameObject newRoom = Instantiate(room, GridToWorld(currentPos), Quaternion.identity, transform);
Room roomComponent = newRoom.GetComponent<Room>();
Debug.Log(createRoomCount + " : " + currentPos);
// 1. 룸 타입 설정
// RoomType randomType = RoomType.Normal;
// 2. 초기화
roomComponent.Init(currentPos, RoomType.Normal);
// 3. 바운드 설정
roomComponent.SetMargin(new Vector2(2f, 2f)); // 강제 적용
roomComponent.CalculateRoomBounds(); // 이후에 바운드 계산
roomInstances[currentPos] = newRoom;
createRoomCount++;
}
currentPos += GetRandomDirection();
}
}
void Update()
{
Vector2 input = new Vector2(joystick.horizontal, joystick.vertical);
float magnitude = Mathf.Min(input.magnitude / joystick.stickRange, 1f);
if (magnitude < deadZone)
magnitude = 0f;
Vector2 ratioInput = input.normalized * magnitude;
transform.position += (Vector3)(ratioInput * speed * Time.deltaTime);
if (ratioInput.x != 0)
{
_spriteRenderer.flipX = ratioInput.x < 0;
}
if (ratioInput != Vector2.zero)
{
PlayAnimation("Walk");
}
else
{
// PlayAnimation("Idle");
_movement.x = Input.GetAxisRaw("Horizontal");
_movement.y = Input.GetAxisRaw("Vertical");
_movement.Normalize();
// 좌우 방향에 따라 스프라이트 반전
if (_movement.x != 0)
{
_spriteRenderer.flipX = _movement.x < 0;
}
// 애니메이션 전환
if (_movement != Vector2.zero)
{
PlayAnimation("Walk");
}
else
{
PlayAnimation("Idle");
}
}
}
void FixedUpdate()
{
_rb.MovePosition(_rb.position + _movement * _playerStatHandler.MoveSpeed * Time.fixedDeltaTime);
}
public class BaseEnemy<T> : MonoBehaviour,IPoolable, IEnemy, IStateMachineOwner<T> where T : MonoBehaviour, IEnemy, IStateMachineOwner<T>, IPoolable
{
protected StateMachine<T> _fsm = new StateMachine<T>();
[Header("Enemy Settings")]
[SerializeField] public Transform _player;
[SerializeField, Range(0f, 200f)] protected float _health = 10f;
[SerializeField] protected float _detectRange = 5f;
[SerializeField] protected EnemyType _type = EnemyType.Normal;
[SerializeField] protected float _speed = 3f;
[SerializeField] protected float _attackPower = 10f;
[SerializeField] protected Collider2D _innerCollider;
protected SpriteRenderer _spriteRenderer;
public Vector2 _minBounds = new Vector2(-8, -4);
public Vector2 _maxBounds = new Vector2(8, 4);
protected IState<T> _currentState;
private string _poolKey;
private Room _currentRoom;
private bool _isDead = false;
protected Animator _anim;
// Unity �ʱ�ȭ
protected virtual void Awake()
{
_anim = GetComponent<Animator>();
_poolKey = _type.ToString();
_spriteRenderer= gameObject.GetComponent<SpriteRenderer>();
}
private void Start()
{
_player = GameObject.FindWithTag("Player").transform;
ChangeState(new IdleState<T>());
}
protected virtual void Update()
{
_fsm.Update(this as T);
_player = GameObject.FindWithTag("Player").transform;
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
PlayerStatHandler playerStatHandler = other.GetComponent<PlayerStatHandler>();
if (playerStatHandler != null)
{
playerStatHandler.Health = -GetAttackPower();
}
}
if (other.CompareTag("PlayerBullet"))
{
TakeDamage(1);
}
}
public void ChangeState(IState<T> _currentState)
{
_fsm.ChangeState(_currentState, this as T);
}
public Transform GetPlayerPosition() => _player;
public float GetPlayerHealth() => _health;
public bool CheckInPlayerInRanged() => Vector3.Distance(transform.position, _player.position) < _detectRange;
public EnemyType GetEnemyType() => _type;
public Animator GetAnimator() => _anim;
public Transform GetEnemyPosition() => transform;
public float GetHealth() => _health;
public float SetSpeed(float amount) => _speed = amount;
public float GetSpeed() => _speed;
public void TakeDamage(float amount) // 몬스터가 공격을 받는 거
{
Debug.Log(_health);
if (_isDead) return;
_health -= amount;
if (_health <= 0f)
{
ChallengeManager.Instance.IncreaseProgress("kill_monsters", 1);
_isDead = true;
_currentRoom?.EnemyDied();
ChangeState(new DieState<T>(_type.ToString()));
}
}
public Room GetCurrentRoom() => _currentRoom;
public virtual void SetCurrentRoom(Room room)
{
_currentRoom = room;
}
public void OnSpawned()
{
// 초기화
gameObject.SetActive(true);
_speed = UnityEngine.Random.Range(1f, 2f); // 여기에 원하는 범위 설정
_isDead = false;
_player = GameObject.FindWithTag("Player").transform;
if (_type == EnemyType.Boss)
_fsm.ChangeState(new CloneState<T>(), this as T);
else _fsm.ChangeState(new IdleState<T>(), this as T); // T = ����� Enemy Ÿ��
}
public void OnDespawned()
{
gameObject.SetActive(false);
}
public void ReturnToPool()
{
switch (_type)
{
case EnemyType.Flee:
PoolManager.Instance.Return(_poolKey, this as FleeEnemy);
break;
case EnemyType.Normal:
PoolManager.Instance.Return(_poolKey, this as MoveEnemy);
break;
case EnemyType.Boss:
PoolManager.Instance.Return(_poolKey, this as Boss);
break;
case EnemyType.Teleport:
PoolManager.Instance.Return(_poolKey, this as TeleportEnemy);
break;
case EnemyType.Ranged:
PoolManager.Instance.Return(_poolKey, this as RangedEnemy);
break;
case EnemyType.Rush:
PoolManager.Instance.Return(_poolKey, this as RushEnemy);
break;
case EnemyType.Minion:
PoolManager.Instance.Return(_poolKey, this as MinionEnemy);
break;
case EnemyType.Explode:
PoolManager.Instance.Return(_poolKey, this as ExplodeEnemy);
break;
case EnemyType.Elite1:
PoolManager.Instance.Return(_poolKey, this as ElitEnemy);
break;
case EnemyType.Elite2:
PoolManager.Instance.Return(_poolKey, this as ElitEnemy);
break;
default:
break;
}
}
public IState<T> CurrentState => _currentState;
public float GetAttackPower()=> _attackPower;
public SpriteRenderer GetSpriteRenderer()
{
return _spriteRenderer;
}
}
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool<T> where T : MonoBehaviour, IPoolable
{
Queue<T> pool = new Queue<T>();
private T _prefab;
private Transform _parent;
public ObjectPool(T prefab, int size, Transform parent = null, string poolKey = "")
{
this._prefab = prefab;
this._parent= parent;
for(int i=0; i<size; i++)
{
T obj = Object.Instantiate(_prefab, _parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
public T Get()
{
if(pool.Count==0)
{
T objTemp = Object.Instantiate(_prefab, _parent);
objTemp.gameObject.SetActive(true);
return objTemp;
}
T obj = pool.Dequeue();
obj.gameObject.SetActive(true);
obj.OnSpawned();
return obj;
}
public void Return(T obj)
{
obj.OnDespawned();
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
public float Speed;
private float _damage;
private List<IProjectileModule> _modules;
private Vector2 _direction;
public Transform Target;
public bool CanPenetrate;
public float AttackDuration;
private PlayerStatHandler _statHandler;
[SerializeField] private LayerMask _targetLayers;
[SerializeField] private LayerMask _wallLayers;
public float HitCooldown = 0.2f;
private Dictionary<Collider2D, float> _lastHitTime = new Dictionary<Collider2D, float>();
public void Init(PlayerStatHandler statHandler, Transform enemyTransform, List<IProjectileModule> modules)
{
_statHandler = statHandler;
_damage = statHandler.Damage;
Speed = statHandler.AttackSpeed;
_modules = modules;
Target = enemyTransform;
AttackDuration = statHandler.AttackDuration;
this.transform.localScale = new Vector2(statHandler.ProjectileSize, statHandler.ProjectileSize);
foreach (var mod in modules)
{
mod.OnFire(this);
}
Destroy(gameObject, AttackDuration);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (((1 << collision.gameObject.layer) & _targetLayers) != 0)
{
float lastTime;
_lastHitTime.TryGetValue(collision, out lastTime);
if (Time.time - lastTime >= HitCooldown)
{
var enemy = collision.GetComponent<IEnemy>();
enemy?.TakeDamage(_damage);
_lastHitTime[collision] = Time.time;
Rigidbody2D rb = collision.attachedRigidbody;
if (rb != null)
{
Vector2 knockbackDir = transform.up;
rb.AddForce(knockbackDir * _statHandler.KnockbackForce, ForceMode2D.Impulse);
}
if (!CanPenetrate)
Destroy(gameObject);
}
}
if (((1 << collision.gameObject.layer) & _wallLayers) != 0)
{
Destroy(gameObject);
}
}
void Update()
{
if(Target == null)
{
Destroy(gameObject);
}
foreach (var mod in _modules)
{
mod.OnUpdate(this);
}
transform.position += transform.up * Speed * Time.deltaTime;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boss : BaseEnemy<Boss>,IRangedEnemy
{
[SerializeField] private GameObject _cloneBossPrefab;
[Header("투사체 Prefabs")]
[SerializeField] private GameObject _forwardProjectilePrefab;
[SerializeField] private GameObject _radialProjectilePrefab;
public GameObject GetClonePrefab()
{
return _cloneBossPrefab;
}
public GameObject GetProjectilePrefab(string type)
{
switch (type)
{
case "Forward":
return _forwardProjectilePrefab;
case "Radial":
return _radialProjectilePrefab;
default:
return null;
}
}
}
using UnityEngine;
public enum SFXType {Jump, Hit, Die} //임시 예시입니다 필요하신 sfx추가하시면 됩니다!
public class SoundManager : Singleton<SoundManager>
{
[SerializeField] AudioSource bgmSource;
[SerializeField] AudioSource sfxSource;
[SerializeField] AudioClip defaultBGMClip;
//중복되는 사운드
[SerializeField] private AudioClip jumpClip;
[SerializeField] private AudioClip hitClip;
[SerializeField] private AudioClip dieClip;
public AudioClip DefaultBGMClip => defaultBGMClip;
private void Start()
{
bgmSource.volume = PlayerPrefs.GetFloat("BGMVolume", 1f);
sfxSource.volume = PlayerPrefs.GetFloat("SFXVolume", 1f);
//PlayBGMSource(defalutBGMClip); //배경음 자동실행
}
public void PlayBGMSource(AudioClip audioClip) //배경음악 교체시
{
if(audioClip==null) return;
bgmSource.clip=audioClip;
bgmSource.loop = true;
bgmSource.Play();
}
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(this.gameObject);
}
//사운드만 갈경우
public void PlaySFX(AudioClip audioClip) //효과음 교체시
{
if(audioClip==null) return;
sfxSource.PlayOneShot(audioClip);
}
//중복되는 사운드 사용할 경우
public void PlaySFX(SFXType type)
{
switch(type)
{
case SFXType.Jump: sfxSource.PlayOneShot(jumpClip); break;
case SFXType.Hit:sfxSource.PlayOneShot(hitClip); break;
case SFXType.Die:sfxSource.PlayOneShot(dieClip); break;
}
}
public void SetBGMVolume(float volume)
{
bgmSource.volume = volume;
}
public void SetSFXVolume(float volume)
{
sfxSource.volume = volume;
}
}
using System;
[Serializable]
public enum ChallengeType
{
CountBased, // 카운트
ConditionBased // 조건
}
[Serializable]
public class Challenge
{
public string id; // 도전과제 ID
public string description; // 설명
public int goal; // 목표
public int currentCount; // 현재 진행도
public bool isCompleted; // 완료 여부
public ChallengeType type; // 도전과제 타입
public string rewardCharacterId; // 이 도전과제 완료 시 해금될 캐릭터 ID
}
// 챌린지 관리자
public void IncreaseProgress(string id, int amount)
{
foreach (Challenge challenge in challenges)
{
if (challenge.id == id && !challenge.isCompleted && challenge.type == ChallengeType.CountBased)
{
challenge.currentCount += amount;
if (challenge.currentCount >= challenge.goal)
{
challenge.isCompleted = true;
ShowReward(challenge);
// 도전과제 완료 시 캐릭터 해금
if (!string.IsNullOrEmpty(challenge.rewardCharacterId))
{
// 캐릭터 해금 요청
CharacterManager.Instance.UnlockCharacter(challenge.rewardCharacterId);
}
}
break;
}
}
SaveChallenges();
}
// 사용 예시 : ChallengeManager.Instance.IncreaseProgress("kill_monsters", 1);
public void CompleteConditionChallenge(string id)
{
foreach (Challenge challenge in challenges)
{
if (challenge.id == id && !challenge.isCompleted && challenge.type == ChallengeType.ConditionBased)
{
challenge.isCompleted = true;
ShowReward(challenge);
// 도전과제 완료 시 캐릭터 해금
if (!string.IsNullOrEmpty(challenge.rewardCharacterId))
{
// 캐릭터 해금 요청
CharacterManager.Instance.UnlockCharacter(challenge.rewardCharacterId);
}
SaveChallenges();
break;
}
}
}
팀장 - 장유성 (Object Pool몬스터 생성 로직, 사운드 로직, 싱글톤 유틸리티 제작)
팀원 - 김경민 (데이터, UI, 게임 설정 )
깃헙 - https://github.com/rudals446
팀원 - 김예지 (몬스터 ai, 몬스터 소환 로직)
깃헙 - https://github.com/yejii-gi
팀원 - 설민우 (플레이어 공격 로직, 아이템, 스킬)
깃헙 - https://github.com/coolblue185
팀원 - 한수정 (플레이어 로직, 맵 로직, 인트로 씬)