본문 바로가기
Unity/땡땡마법사류

2025.01.13 ~ 2025.02.02

by 윤원용 2025. 2. 3.

중간에 연휴도 있었고 게임 분석하느라 생각 외로 시간이 너무 빨리 지나갔다.

생각한 결과를 만들진 못했지만 포스팅해 보겠다.

 

게임을 만들기 위한 작업을 전체를 포스팅하는 게 아니고 처음 접하거나 인상적인 내용만 포스팅하겠다. 

 

목차

  1. 나만의 몬스터 만들기
  2. 몬스터 생성하기
  3. 플레이어의 위치에서 가장 가까운 몬스터에게 미사일 발사
  4. 몬스터가 벽(플레이어의 hp)을 공격하기

1. 나만의 몬스터 만들기

게임을 만들기 위해 무료 에셋을 사용할 수 있지만 나만의 몬스터를 만들기 위해 piskel 사이트를 이용했다.
생각보다 오래 걸렸는데 역시 본인은 미술 감각이 저질이다...

본인 기준으로 잘 만들었다 생각되는 두 갠데 애석하게도 게임에 적용할 수 없을 것 같다. ㅠㅠ

player
enemy
enemy

 

enemy
enemy
enemy

일단은 이런 몬스터 및 플레이어만 적용할 수 있어서 적용해 봤다.

2. 몬스터 생성하기

이전에 포스팅한 게임과 비슷하게 개발했고 일단 돌아가게 만들기 위해 코드는 대충 만들었다.

본인이 원하는 방법은 라운드 별로 생성할 몬스터의 개수가 정해져 있고 몬스터를 생성할 때 일정 딜레이가 있는 것이다.

	public GameObject EnemyPrefab = null;

	private int round = 0;
	private float roundCooltime = 10.0f;
	private float nextRoundCooltime = 0.0f;

	private int[] enemyCntByRound = null;
	private float enemyGenCooltime = 1f;

	void Start () {
		enemyCntByRound = new int[]{ 5, 7, 10, 15, 25, 45 };
	}

	void Update() {
		float t = Time.time;
		if (enemyCntByRound.Length <= round) {
			return;
		}

		if (nextRoundCooltime <= t) {
			nextRoundCooltime = t + roundCooltime;
			int enemyCnt = enemyCntByRound[round++];
			int i = 0;
			while (i < enemyCnt) {
				Invoke("genEnemy", enemyGenCooltime * i);
				i++;
			}
		}
	}

	void genEnemy() {
		Instantiate(EnemyPrefab, getInitPosition(), Quaternion.identity);
	}

	Vector3 getInitPosition() {
		return new Vector3(Random.Range (-2.25f, 2.25f), 5.5f, 50);
	}

코드를 설명하면 Start 함수에서 라운드 별 몬스터의 개수 정보를 생성하고 Update 함수에서 호출 시점의 시간과 nextRoundCooltime 변수의 값을 비교하여 호출 시점의 시간과 같거나 크면,  호출 시점의 시간과  roundCooltime 변수를 더한 값을 nextRoundCooltime에 저장하고 라운드 별 생성할 몬스터의 개수를 갖고 오며 라운드를 증가시킨다. 

그 후 라운드 별 몬스터의 개수만큼 반복하면서 Invoke 함수를 호출하고 몬스터를 생성할 때 지연을 시킬 수 있도록 enemyGenCooltime * i 만큼 지연 시간을 추가한다.

3. 플레이어의 위치에서 가장 가까운 몬스터에게 미사일 발사

개발할 때 이 내용이 제일 어려웠다. 엄청 삽질하다 유니티의 기능을 검색해서 개발했더니 매우 편리했다.

GameObject[] enemyList = GameObject.FindGameObjectsWithTag("Enemy");
int i = 0;
int len = enemyList.Length;
if (len == 0) {
	return;
}
float min = 9999.0f;
int enemyIndex = 0;
while (i < len) {
	GameObject enemy = enemyList[i];
	float dist = Vector3.Distance(enemy.transform.position, prefab.transform.position);
	if (min > dist) {
		min = dist;
		enemyIndex = i;
	}
	++i;
}

몬스터 Prefab에 Enemy 태그를 추가하여 태그로 생성된 모든 몬스터 오브젝트를 가져온다.

몬스터 오브젝트들을 순회하면서 플레이어와 몬스터의 거리 차이를 Vector3.Distance 함수를 사용하여 구한 후 제일 가까운 몬스터를 찾도록 만들었다.

 

찾은 몬스터를 스킬의 타깃으로 전달해야 하는데

GameObject skillObj = Instantiate(bullet, prefab.transform.position, Quaternion.identity);
SkillInterface skill = skillObj.GetComponent<SkillInterface>();
skill.setTargetEnemy(enemyList[enemyIndex]);

스킬을 만든 후 setTargetEnemy 함수를 호출하여 전달하도록 만들었다.

 

이제 생성된 스킬이 몬스터에게 이동하면 끝나는데

	void Update () {
		if (target == null) {
			return;
		}

		float seed = getSpeed ();
		Vector3 targetPosition = target.transform.position - prefab.transform.position;
		prefab.transform.Translate(Time.deltaTime * seed * targetPosition.normalized);
	}

	public int getDamage() {
		return 10;	
	}

	public float getSpeed() {
		return 5.0f;
	}

	public void setTargetEnemy(GameObject t) {
		target = t;
	}

 몬스터의 위치에서 스킬의 현재 위치를 뺀 후 스킬을 이동시키는데 normalized를 사용하여 이동 방향을 정한다.

 

4. 몬 스터가 벽(플레이어의 hp)을 공격하기

기능은 구현했지만 UI를 구현하지 못했다. 

Rigidbody와 Box Collider 컴포넌트를 사용하여 몬스터와 벽이 부딪칠 때 이벤트를 받을 수 있도록 만든다.

void Update () {
	if (isMove) {
		gameObject.transform.Translate(Time.deltaTime * moveSeed * -Vector3.up);
	}
	if (isAttack) {
		float t = Time.time;
		if (nextAttackCooltime < t) {
			nextAttackCooltime = t + attackCoolTime;
			GameObject wallObj = GameObject.FindGameObjectWithTag("Wall");
			WallControl wall = wallObj.GetComponent<WallControl>();
			wall.attack(attackPower);
		}
	}
}

void OnTriggerEnter(Collider other) {
	if (other.tag == "Wall") {
		this.isMove = false;
		this.isAttack = true;
	}
 }

벽과 부딪칠 때 몬스터의 움직임을 멈추고 공격할 수 있도록 OnTriggerEnter 함수에서 상태를 변경한다.

Update 함수에서 isAttack 값이 true면 Wall 태그가 있는 컴포넌트를 찾은 후 attack 함수에 몬스터의 데미지를 파라메터로 넣으며 호출한다.

	public Slider hpbar = null;
	private float HP = 3000.0f;
	private float hp = 3000.0f;

	public void attack(float damage) {
		if (hp <= 0) {
			return;
		}

		hp -= damage;
		Debug.Log(hp / HP);
		hpbar.value = hp / HP;

		if (hp <= 0) {
			//[TODO] Game over
		}
	}

UI 슬라이더 컴포넌트를 사용하여 hp를 표현하고 몬스터의 데미지만큼 벽의 체력을 깎고 슬라이더 컴포넌트의 값을 변경한다.

 

 

포스트는 이걸로 마치고 작업할 때 몬스터가 겹치는 경우 스킬이 겹친 몬스터들에게 충돌이 적용됐는데 Rigidbody 컴포넌트의 Is kinematic 옵션을 비활성화해야 한다.

 

 

이번 주에 작업할 목록

  1. 라운드 UI 표시
  2. LEVEL UP 시 사용할 팝업
  3. 팝업에서 스킬을 upgrade 할 수 있도록 기능 추가

 

엄청 이상하게 만드는 것 같지만 뭔가 재밌고 아이디어가 뿜뿜 한다 ㅎㅎ 어려운 건 당연한 거고... ㅠㅠ