Home / Articles

턴제 게임 루프를 구현하는 방법 [C#]

공개일: 2020/06/14 업데이트일: 2020/06/14

서론

이 글에서는 턴제 게임 루프를 구현하는 데 사용하는 코드를 소개합니다.

제가 만든 턴제 로그라이크 Treasure Rogue도 기본적으로는 여기 소개하는 코드로 동작합니다. (읽기 쉽게 하기 위해 꼭 필요한 부분만 남기고 나머지는 생략했습니다.)

Commander

Commander 컴포넌트는 “턴제 시스템의 규칙에 따라 동작하는” 오브젝트에 붙입니다.


using System;
using UnityEngine;

public class Commander : MonoBehaviour {

	[SerializeField]
	int m_Priority;

	public int Priority { get => m_Priority; set => m_Priority = value; }

	public bool IsTurn { get; private set; }

	public event Action<Commander> OnBeginTurn;

	void OnEnable () {
		TurnManager.Instance.AddCommander(this);
	}

	void OnDisable () {
		TurnManager.Instance.RemoveCommander(this);
	}

	// Minimal code for a function that starts a turn.
	// Depending on the game, you might add logic such as "skip if HP is 0."
	public bool BeginTurn () {
		if (IsTurn) {
			return false;
		}
		IsTurn = true;

		OnBeginTurn?.Invoke(this);

		return true;
	}

	public void EndTurn () {
		IsTurn = false;
	}

}

BeginTurn을 호출하면 Commander가 자신의 턴을 시작합니다. OnBeginTurn 이벤트가 발생하므로, 턴이 시작될 때 실행할 작업은 여기에 등록하면 됩니다.

턴이 시작된 뒤에는 반드시 EndTurn을 호출해야 합니다.

다음은 Commander를 관리하는 TurnManager 코드입니다.

TurnManager

TurnManager는 턴제 루프를 관리합니다.


using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TurnManager : SingletonMonoBehaviour<TurnManager> {

	public bool startLoopOnStart;

	// Registered Commanders
	readonly List<Commander> m_Commanders = new List<Commander>();

	// Pending Commanders
	readonly HashSet<Commander> m_PendingCommanders = new HashSet<Commander>();

	void Start () {
		if (startLoopOnStart) {
			StartLoop();
		}
	}

	public void StartLoop () {
		StartCoroutine(Loop());
	}

	IEnumerator Loop () {
		while (true) {
			// Add pending Commanders to the loop
			if (m_PendingCommanders.Count > 0) {
				foreach (Commander commander in m_PendingCommanders.ToArray()) {
					if (commander) {
						m_Commanders.Add(commander);
					}
				}
				m_PendingCommanders.Clear();
			}

			// Advance turns
			foreach (Commander commander in OrderedCommanders().ToArray()) {
				if (commander == null) {
					m_Commanders.Remove(commander);
					continue;
				}

				if (commander.BeginTurn()) {
					while ((commander != null) && commander.IsTurn.Value) {
						yield return null;
					}
				}
			}
			yield return null;
		}
	}

	// Return the registered Commanders sorted by priority
	IEnumerable<Commander> OrderedCommanders () {
		return m_Commanders
			.Where(c => c != null)
			.OrderByDescending(c => c.Priority);
	}

	// Temporarily register a Commander
	public bool AddCommander (Commander commander) {
		if (commander == null) {
			throw new ArgumentNullException(nameof(commander));
		}
		return m_PendingCommanders.Add(commander);
	}

	// Remove a Commander from the loop
	public bool RemoveCommander (Commander commander) {
		m_Commanders.Remove(commander);
		return m_PendingCommanders.Remove(commander);
	}
}

AddCommander 함수를 사용하면 턴제 루프에 넣고 싶은 Commander를 추가할 수 있습니다. (Commander는 OnEnable에서 자동으로 AddCommander를 호출합니다.)

맺음말

아직은 매우 최소한의 구성입니다. 하지만 Commander와 TurnManager를 분리하는 것만으로도 턴제 게임 루프의 단단한 기반을 만들 수 있습니다.

그다음에는 턴 순서, 기절 처리, 각 턴을 끝내는 명시적 조건 같은 규칙을 더해 가며 실용적인 시스템으로 확장할 수 있습니다.

공유

Twitter