마리오 웹 게임(7) - OOP 및 Utility Type 활용
포스트
취소

마리오 웹 게임(7) - OOP 및 Utility Type 활용

본 포스트는 JS(TS)에 OOP 적용 방법에 대한 포스팅입니다.

OOP

1. OOP 사용 이유

웹 게임을 만들면서 특정 대상의 데이터를 관리하는 좋은 방법에 대해 고민을 했습니다.
예를 들어서 마리오라는 캐릭터에 의존한 데이터들( speed, position 등 )이 있다면 이 데이터들을 어떻게 그룹화하고 원하는 데이터만 변경시키는 방법에 대한 고민입니다.

초기에는 안 좋은 방법이지만 전역 변수로 선언하고 데이터를 관리했습니다.
데이터가 많아질수록 어떤 변수를 선언했고 어떻게 수정해야 하는지 헷갈리기 시작해서 다른 방법을 고민했습니다.

다음으로는 전역 객체를 사용했습니다.
전역 객체의 내부에 모든 변수와 메서드를 선언해서 그룹화해서 사용하니 첫 번째 방법보다는 괜찮았지만, 선언한 전역 객체마저도 점점 늘어나서 코드의 가독성이 안 좋고 사용하기도 불편했습니다.

마지막 방법으로 클래스를 사용했습니다.
클래스를 사용해서 한 번 객체의 틀을 만들어두면 해당 객체에 모든 변수, 함수(프로퍼티, 메서드)가 함께 관리할 수 있어서 사용하기 매우 편했습니다.
하지만 클래스를 이용하면서도 반복적인 코드가 늘어나는 게 느껴졌습니다.
예를 들어서 Mario 클래스와 Goomba 클래스 모두 move(), collisition(), draw() 같은 메서드를 처리합니다.
메서드 내부 동작은 거의 같은 것도 있고, 아예 다른 것도 있습니다.
같은 메서드는 공유해서 사용하고, 다른 메서드는 비슷한 형태로 사용한다는 것을 표현하기 위해서 부모 / 자식 클래스, 가상 클래스, 가상 메서드를 사용했습니다.

C++를 통해서 OOP의 개념과 사용법은 어느정도 알고 있는 상태로 시작했습니다.

2. 구현한 클래스 구조

클래스 다이어그램에 작성된 타입들은 여기를 참고해주세요!

mario_class_diagram

  1. Character class: 모든 캐릭터의 부모가 되는 가상 클래스
  2. Player class: 모든 플레이어의 부모가 되는 가상 클래스
  3. Mario class: 마리오의 클래스
  4. Enemy class: 모든 적의 부모가 되는 가상 클래스
  5. Goomba: 굼바의 클래스

모든 캐릭터의 공통된 부모인 Character를 가상 클래스로 만들고 공통 사용 요소를 정의했습니다.
그리고 사용자가 조작하는 Player, 컴퓨터에 의해 컨트롤될 Enemy를 가상 클래스로 정의했습니다.
마지막으로 구체적인 대상인 MarioGoomba를 정의했습니다.

extends, abstract, 접근 제한자들( public, private, protected )을 적절하게 사용해서 더욱 더 유연하고 유지 ꞏ 보수에도 최대한 유리하도록 구현하려고 노력했습니다.

Utility Type

타입을 유연하게 사용하기 위해서 TypeScriptUtility Type을 활용했습니다.

사용에 관해 설명하기에 앞서 해당 프로젝트에서는 이미지 스프라이트를 사용하기 때문에 이미지 스프라이트에서 어떤 크기에 어떤 위치에 어떤 이미지가 있는지를 알아내기 위한 테이블이 필요하기 때문에 객체로 선언하고 사용하고 있습니다.

아래의 예시처럼 선언하고 사용하고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * 플레이어 키 테이블
 * ( 이미지 스프라이트를 사용하기 때문에 어느 위치에 어떤 이미지인지 매핑해줄 테이블이 필요 )
 */
export const playerKeyTable = {
  // 마리오
  mario: {
    // 작은
    small: {
      // 이미지의 "width"와 "height"
      width: 46,
      height: 64,
      crawlHeight: 46,

      // 이미지의 y위치
      right: 0 * 69,
      left: 1 * 69,

      // 이미지 x위치
      stand: 0 * 46,
      walk: 1 * 46,
      runStand: 2 * 46,
      run: 3 * 46,
      jumpUp: 4 * 46,
      JumpDown: 5 * 46,
      jumpRun: 6 * 46,
      crawl: 7 * 46,
      die: 8 * 46,
    },

    // 큰
    long: {},
  },
};

이 객체에 어떤 key에 어떤 value 타입이 들어있는지 Utility Type을 이용해서 객체를 참조해서 타입이 결정되도록 만들었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 플레이어 종류 ( 마리오 등 )
 */
export type PlayerKinds = keyof typeof playerKeyTable;
/**
 * 마리오 상태 ( 작은, 큰 등 )
 */
export type MarioState = keyof typeof playerKeyTable["mario"];
/**
 * 작은 마리오 몸짓 ( 행동 )
 * 걷기, 달리기, 엎드리기 등
 */
export type SmallMarioMotion = Exclude<
  keyof typeof playerKeyTable["mario"]["small"],
  "width" | "height" | "crawlHeight" | "right" | "left"
>;
/**
 * 작은 마리오 키 테이블 타입
 */
export type SmallMarioKeyTable = typeof playerKeyTable["mario"]["small"];

추가 설명을 조금 하자면 MarioState 같은 경우에는 직접 "small" | "long"과 같이 타입을 정해도 문제없이 동작하지만, 이후에 playerKeyTable가 수정된다면 그것에 맞게 타입을 바꿔줘야 합니다.
하지만 keyof typeof를 사용해서 playerKeyTable에 변경에 의해 유연하게 대응하게 됩니다.

엄청 중요하고 핵심적인 내용은 아니지만 사소한 부분도 구체적으로 구현하기 위해 노력했고, Utility Type을 사용할 수 있다는 것을 보여주기 위해 작성해봤습니다.

다음 포스팅 주제

이후에 구현할 기능은 캐릭터와 적의 충돌로 인한 죽임/죽음입니다.
이것을 구현하기에 앞서 현재 블록과 마리오, 블록과 굼바에 대한 충돌에 대해서는 모두 마리오/굼바의 특정 메서드에 렌더링 된 모든 블럭을 인수로 넘겨줘서 충돌 처리를 합니다.
하지만 이런 방식이 괜찮은 방식이 아니라고 생각해서 구조를 바꿀 생각입니다.
맵 관리 매니저 클래스를 만든 것처럼 충돌 관리 매니저 클래스를 만들어서 모든 충돌에 대해 처리를 하는 것이 좀 더 흐름에 맞는다고 생각해서 충돌 관리 매니저 클래스를 제작할 생각입니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

마리오 웹 게임(6) - 캐릭터 모션 구현

네트워크 - TCP/IP 4계층