게임 개발 과정 (2) - 오토체스류(제목미정)

zebius·2025년 1월 24일
0

UnityPractice

목록 보기
3/4

1월22~24일

기획부분에서는 스토리 구상과 시나리오 등장인물등을 구상하였다.
짧게 설명하자면

6개의 왕국(불,얼음,기계,어둠,숲,빛)이 있는 세상.
아득히 옛부터 내려오는 악마가 내려와 세상을 모두 불태울것이라는 예언이 있었다.
이에 세상은 수호자를 깨우고, 6왕국을 규합하여 악에 대항할것이라는 예언도 같이 내려왔다.
어느날 대륙의 중앙에 있던 버려진 땅에서 균열이 갈라지며 악마들이 쏟아져 내려와 왕국들을 공격하고, 대응하던 왕국의 앞에 깨어난 수호자(플레이어)가 나타난다.

이런식으로 초기 설정을 잡고, 초기 선택 가능한 왕국(빛,불,기계)로 하고 .. 뭐 기획쪽은 여기까지만 적겠다.

입력 처리를 어떻게 할지 좀 고민을 했었다. GameState(FSM)의 OnUpdate부분에 합쳐서 상태에 따른 입력을 관리를 할지. 아니면 별도의 객체를 두어 처리를 할지 말이다.
그런데 결국 유저가 편하게 키 매핑을 할 수 있게 하려면(물론 불필요한 설정일수도 있다.) OnUpdate에 종속적이게 되면 안되겠다고 생각되어 InputHandler라는 객체를 따로 만들었다.

이전에 했던 방식으로 Json을 활용하여 유저의 키 매핑을 관리하는 구조체를 두어 입출력해주는 방식을 도입했다.

public struct KeyMappingJson {
    //Move
    public string MoveUp;
    public string MoveDown;
    public string MoveLeft;
    public string MoveRight;
    //TODO:다른 단축키들 나중에 UI구성하고 마저 짭시다.
}

위처럼 대략 일부 움직임을 관장하는 키부분만 넣어놨다. 내가 만들 게임은 오토체스류라 솔직히 카메라 움직임도 굳이 필요치는 않을것 같았지만, 일단은 넣어놓고 만들다 추후 기능을 빼버리는 한이 있더라도 간단한 카메라 움직임 정도의 구현은 괜찮을거 같았다.(아직은 키설정 부분이지만)

//원래는 아래와 같은 방식으로 작성하려고 했다.
public KeyCode GetMoveUpKey(){
	return moveKey;
}
if(Input.GetKeyDown(InputHandler.GetMoveUpKey())){
  Direction+=Vector2.up;
}
...생략
Direction.Normalize();

원래 요딴식으로 작성하려고 했었다. 물론 부드러운 움직임에 대한 수식은 가속도 따위를 넣어서 처리를 하려고 했었다.

그런데 유니티에서 제공하는 New Input System이 여러 입력 디바이스에 호환성이 좋다는걸 알게되어 코드를 변경하게 되었다.

//움직임을 관장하는 key handler를 예로들어 보겠다.
//여기서 생성자에서 첫번째 인자로 해당 입력의 명칭을 넣고, 그다음은 아래는 이동에 관한 즉
//Vector값을 반환해줄것임으로 type에다가 InputAction.Value를 넣는다 일반적인 트리거 형식
//이라면 InputAction.Button을 넣어주면된다.
InputAction Move=new InputAction("명칭을넣자",type:InputAction.Value);
//그리고 바인딩(입력연결)을 추가해준다.
//키값의 예) <Keyboard>/w,<Gamepad>/leftStick등
//어떻게 처리할지 설정은 예를들어 반전은 "invert" 이제 이동같은 경우는 
//"scaleVector2(x=,y=)"등을 넣어 주면된다.
Move.AddBinding("여기에 키 값을 넣으면된다").WithProccessor("입력된값을 어떻게 처리할지");
//그래서 현재 입력된 키를 기반으로 설정된 Vector2값을 반환시켜준다. =>Move.ReadValue<Vector2>()
public Vector2 ReadDirection () {
      return keymap.Move.ReadValue<Vector2>().normalized;
  }

WithProccesor의 입력가능한 명령어와 커스텀 명령어 작성도 가능하다고 하는데 이 부분은 내가 학습이 더 필요할것 같다.

그 다음으로는 이벤트 시스템을 만들었다.

다른 렌더링 시스템(애니메이션,효과,UI등을 담당)에서 callback 함수를 등록을 통한 연계로 의존성을 줄이면서 시스템이 늘어나더라도 확장하기 좋게 하였다.

private Dictionary<Type,List<Action<IEvent>>>EventSubscribe=new Dictionary<Type, List<Action<IEvent>>>();
public void Subscribe<T> (Action<IEvent> callback)where T: IEvent {
    var type= typeof(T);
    if (!EventSubscribe.ContainsKey(type))
        EventSubscribe[type]=new List<Action<IEvent>>();    
    if(!EventSubscribe[type].Contains(callback))
        EventSubscribe[type].Add(callback);
}
 public void UnSubscribe<T> (Action<IEvent> callback)where T : IEvent {
     var type=typeof(T);
     if(!EventSubscribe.ContainsKey(type))
         return;
     EventSubscribe[type]?.Remove(callback);
 }

원래 이벤트 시스템과 ECS 시스템을 같이 사용할때 이벤트 발생시 Entity에 Component를 추가하는 작업을 비동기에서 하려고 했었다.(23일)
그런데 EntityManager가 메인스레드에서만 호출이 된다는걸 알게되어 결국 수정하였다.(24일)
(메인스레드에서만 호출되는 이유는 entity를 생성하거나 component를 추가하는 과정에서 archetype chunk가 변경되면 변경전에 해당 정보들을 가져가 작업중이던 녀석들은 이전 archetype chunk 기준임으로 동기화에 문제가 발생된다.그걸 방지하기위해 Unity에서 막아놓은것.)

대략 23일에 생각했던 코드는 아래와 같다.

//이벤트 발생시 여러 스레드에서 동시 다발적으로 발생될수 있으니(AI가 많아질수록) 큐에 이벤트 
// 발행시 넣어서 비동기로 처리하자는게 골자였다.
private ConcurrentQueue<IEvent>EventPublisher = new ConcurrentQueue<IEvent>();
//각 이벤트의 타입을 키로 하고 함수포인터(Action)의 배열을 값으로 하여, 각 렌더링등의 담당
//시스템에서 타입에따라 등록해주는 콜백함수를 다르게 할수 있게한다.
private Dictionary<Type,List<Action<IEvent>>>EventSubscribe = 
	new Dictionary<Type,List<Action<IEvent>>>();
//비동기 작업(Task)에서 작업의 중단 여부를 전달해줄 토큰
private CancellationTokenSource cts=null;
//lock을 걸어 동시접근을 방지해주자.
private object _lock=new object();
public void Publish(IEvent e){
  //이때까지만해도 메인스레드에서 모두 처리할생각이 없어서 메인스레드에서 처리되어야 할 
  if(EventSubscribe.ContainKey(e.type)){
    foreach(var callback in EventSubscribe[e.type]){
      callback?.Invoke(e);
    }
  }
  EventPublisher.Enqueue(e);
}
public void EventExecutorBegin(){
	lock(_lock){
    	if(cts==null || cts.IsCancellationRequested){
          cts=new CancellationTokenSource();
          Task.Run(()=>EventExecute(cts.Token));
        }
	}
}
private void EventExecute(CancellationToken token){
	//토큰에서 취소가 요청되었는지 확인후 요청 되었으면 반복문을 끝내어 작업 종료
	while(!token.IsCancellationRequested){
    	if(EventPUblisher.TryDequeue(out IEvent e)){
        	...처리
        }
    }
}
public void EventExecutorDispose(){
	cts?.cancel();
    cts=null;
}

변경된 24일 코드 -> 굳이 큐에 넣지 않고 이벤트 발행시 바로 처리.

public void publish(IEvent e){
	if(EventSubscribe.ContainKey(e.type)){
    	foreach(var callback in EventSubscribe[e.type]){
      	callback?.Invoke(e);
    	}
  	}
    //여기서 메시지 타입에 따라 처리 
}

ECS 시스템의 장점은 복잡한 연산을 비동기로 넘겨서 메인스레드의 부담을 줄이는데 있다. 한마디로 충돌등의 물리적인 계산 부분을 처리할때 장점이 충분히 발휘된다. 그래서 물리 부분을 Unity 에서 제공하는 PhysX api를 이용하는게 아닌 Component로 하여 CollisionSystem을 만들어볼 생각이다.

일단 기본적인 엔티티 시스템을 일부 설계했다. ECS에서는 렌더링을 담당하게 하지 않을 계획임으로 엔티티 생성시 매핑이 중요하다고 생각했다. 그렇다고 EntitySystem에서 직접 UnitSystem 따위를 호출하게되면 의존성이 너무 강하게 되어서 interface로 매핑할 추상 함수를 만들어서 각 시스템에 구현해 확장성과 유연성을 확보했다.

//엔티티를 생성할때 유닛,UI등에 같이 매핑 하기위해서 Interface로 클래스를 하나만든다.
public interface IEntitySystemInjection{
 void OnCreatedEntity(Entity e);
 void OnDestroyedEntity(Entity e);
}
//아래는 일단 임시로 velog에 올리려고 대충 틀만 짜놓은 유닛시스템이다.
public class UnitSystem : Singleton<UnitSystem>,IEntitySystemInjection{
	//임시다.GameObject로할지 새로 class를 만들지 미정이다.
	private Queue<GameObject>UnitPool=new Queue<GameObejct>();
    private Dictionary<Entity,GameObject>EntityMapping
    	=new Dictionary<Entity,GameObject>();
	public void OnCreatedEntity(Entity e){
    	var unit = GetUnitByPool();
        //추가 : 생각해보니 Unit class를 짜서 Position값등을 Component에 맞게 바꿔줄
        //SpawnUnit(float3 position,...)을 만들어야겠다.
        if(!EntityMapping.ContainKey(e)){
        	EntityMapping[e]=unit;
        }
    }
    //임시 반환값
    public GameObejct GetUnitByPool(){
    	if(UnitPool.TryDequeue(out GameObject unit)){
        	return unit;
        }        
        return new GameObject(); //혹은 추후 prefab을 통한 Instantiate(..)가 될수도 있다.
    }    
...
}
public class EntitySystem : Singleton<EntitySystem>{
	//의존성을 줄이기위해 인터페이스를 통해 entity와 Unit object나 UI등을 매핑
	private List<IEntitySystemInjection> injections=new List<IEntitySystemInjection>();
    public void CreateEntity(){
    	//풀링은 유닛과 같으므로 생략하겠다.
    	var entity = GetEntityByPool();
        foreach(var system in injections)
        	system.OnCreatedEntity(entity);
        }
    }
}
profile
스타터입니다.

0개의 댓글

관련 채용 정보