adsense



[JAVA/slick2d] 2D 슈팅게임 위치즈 스크램블 제작강좌 -step 1-

※이제부터 진행되는 내용은, Slick2D 엔진을 사용하지만 실제 구현은 awt나 javax.swing으로 해도 큰 차이가 없습니다.
하지만 Slick2D를 사용함으로서 속도상의 잇점을 볼 수 있고, 상용 소프트웨어에 쓰이는 게임엔진만큼은 아니더라도 여러 기능을 쉽게 이용할 수 있게 됩니다.


step 1. 게임오브젝트(GameObject) 클래스


기존에 소개드린 소스에서는 적 캐릭터가 필요하면 적 캐릭터 클래스를, 총알이 필요하면 총알 클래스를 만드는 방식을 보았습니다.

이렇게 어느 정도 별개의 클래스로 만들었는데도, 실제 중요한 처리나 그리기는 상당부분 메인 스레드의 루프 내에서 해 줬었는데요,

이런 방식은 간단하게 급히 만들때는 괜찮지만, 등장하게 되는 물체(Object)의 종류가 늘어나고 프로그램의 규모가 커지면 메인 루프 부분이 스파게티 코드화 되고, 협업 개발이 어려워집니다.

그때문에 메인 루프에서는 물체-오브젝트를 생성해서 배열에 올려놓기만 하면 나머지는 해당 오브젝트를 정의한 클래스 내부에서 알아서 동작하도록,
화면에 동작하는 모든 물체(Object)를 독립적인 코드로 개발하는 것을 전제하면 개발에 편리해집니다.

우선은 이 구조를 만들려고 합니다.
이후부터 화면에 등장하는(눈으로 보이지는 않을 수도 있는) 모든 것을 게임 오브젝트-GameObject라 부르겠습니다.

샘플을 보겠습니다. 위치즈 플라이트 때 처럼 단계별로 소스가 진화(?)해가므로 github에는 적합하지 않아 중간 단계의 코드들은 압축 다운로드에 다 함께 포함되어 있습니다.

기본 프로젝트는 여기 포함된 프로젝트를 사용하고, 이후 src 폴더의 내용만 변경해주면 됩니다.

※소스는 핑백으로 연결된 포스트에 포함되어 있습니다.



1.forSlick2D 폴더의 BasicScene은, Slick2D 엔진의 BasicGameState를 기본으로 확장한 클래스입니다.

BasicGameState를 사용해 화면을 전환하기 위해서는, 각 화면 별로 ID를 지정해 줘야 하며, 화면 ID는 Slick2D에서 기본적으로 구현되어 있지 않아 추가 코딩을 해 줘야 한다- 이것은 앞서의 포스팅에서 다룬 바 있습니다.

앞서 포스팅의 내용을 BasicGameState을 상속한 별도 클래스로 만들어, 이후에 추가 코딩 없이 사용 가능하도록 한 것이 BasicScene입니다.


game은 Slick2D 엔진이 최초 수행하는 StateBasedGame을,
gc는 Slick2D 엔진이 관리하는 게임 컨테이너(게임이 실행되는 창 크기, 프레임레이트, 게임의 실행 등) 정보를 갖습니다.

id는 이 화면의 id로 BasicScene의 생성자가 불릴 때 지정해줄 수 있습니다. 지정하지 않으면 -1로 고정되도록 했습니다. ID값이 중복되면 정상적으로 화면을 전환할 수 없으니 필히 ID는 지정해줘야 합니다.

2.
이상의 기본적인 처리에 더해서, GameObject를 관리하기 위한 기반 처리를 해 줍니다.
이전에는 각 클래스 별로 Vector를 따로 선언해서 관리했는데, 이번에는 하나의 Vector로 관리하는 것입니다.
이게 총알인지 적 캐릭터인지 어떻게 구분하느냐? 이 부분에 관해서는 잠시 뒤로 밀어놓고..

우선은 GameObject라는 클래스를 정의합니다. 여기서는 추상클래스로 만들었는데 꼭 그럴 필요는 없지만, 실제 구현되는게 아닌 개념을 정의하는 상위 클래스는 추상으로 만드는걸 추천합니다.

BasicScene에 GameObject의 벡터를 준비합니다. 이후 node벡터라고 하면 이것을 의미합니다.


BasicScene은 render와 update에서 각각 GameObject의 render와 update를 호출합니다.

GameObject에는 기본적으로 다음 정보를 설정합니다.


오브젝트를 식별한다는 것은, node에서 특정 오브젝트를 얻어내기 위한 정보입니다.
하나의 벡터에 배열되기 때문에 배열의 위치나 순서로는 오브젝트가 어떤 것인지 알아낼 방법이 없습니다.

그 때문에 객체를 생성할 때, 유니크한 정보를 설정하고, 해당 정보를 통해 node에서 원하는 오브젝트를 찾아내야 합니다.
tag나 name 둘 중 하나만 사용해도 되고, 하나는 종류로, 하나는 개체 하는 식으로 복합해서 사용해도 됩니다.

프로그래머는, 이후 GameObject를 상속한 오브젝트 클래스-총알, 아이템, 적 캐릭터 등등-를 만들어 준 뒤, 그리기는 render에, 좌표 이동이나 기타 처리는 update에 코딩하여 만들어주고, BasicScene의 GameObject 벡터에 add 해주면 BasicScene에서 호출해 그리기와 처리를 수행하게 됩니다.


게임오브젝트 벡터인 node 벡터는 private로, 하위 객체들로부터 벡터에 대한 직접 접근을 금지합니다. 벡터의 내용을 임의로 가감하게 되면 객체를 처리하는 루프가 동작하는 중에 객체가 제거되어 에러가 발생할 수 있습니다.

addChild는 node 벡터에 인수로 주어진 GameObject 객체를 추가해 줍니다.
반면, removeChild는 node 벡터에서 바로 삭제하는 것이 아니라, 삭제 예정 리스트에 추가해 줍니다.
그리고, update에서 삭제 예정 리스트에 등록된 것이 있으면 이때 한꺼번에 삭제해 줍니다.

어째서 바로 삭제하지 않는가.
예를 들어 적 캐릭터를 처리하고 있는데, 각 적 캐릭터마다 플레이어의 총알과 충돌했는지 판단하여 적 캐릭터와 총알을 제거하게 됩니다.
이때 제거된 node는 총알과 적 캐릭터가 따로 관리되지 않고 하나의 node 벡터에서 관리되기 때문에 현재 처리중인 캐릭터보다 더 앞쪽의 node가 삭제되었을 수도 있습니다. 복수의 node가 삭제되면서 다음 벡터의 인덱싱이 오류를 발생하게 됩니다.

그때문에 내부적으로는 '이 node는 삭제할 것이다'라고 목록에만 올려놓고, 루프에 영향을 주지 않는 위치에서 한꺼번에 삭제하는 것입니다.

현재 BasicScene의 update와 removeChild에서는 그런 식으로 node 삭제를 구현하고 있습니다.


그럼 지금까지 만든 것을 간단하게 응용해 보겠습니다.


TitleScene은 BasicScene을 상속합니다.
마우스 이벤트를 받아들여 마우스로 클릭하는 위치에 Cloud 객체를 생성하는 내용입니다.

Cloud는 GameObject를 상속합니다.
초기화되면 구름 이미지를 읽어들여 이미지 객체를 생성하고, render에서 이 이미지를 반복적으로 그립니다.
update에서는 구름 이미지가 조금씩 아래로 내려가, 화면 밖으로 나가면 사라지도록 지시했습니다.

임의의 지점을 계속 클릭하면 구름이 생겨서 아래로 내려가 사라지고 현재 존재하는 구름의 갯수를 화면에 표시되는 Total GameObjects 수치로 확인할 수 있습니다.




이번에 다룬 내용은 실제 눈으로 보이는 건 별로 없습니다.
그에 비해 구현해야 하는 내용은 많고 은근히 추상적인 토대에 해당하는 내용이라, 설명도 장황해졌습니다.

굳이 이렇게 구현할 필요가 있는가? 싶기도 하지만, 확장과 응용이 용이하기 때문에 화면에 등장하는-혹은 등장하지는 않지만 게임을 콘트롤하는- 오브젝트들의 종류가 많을수록 편리해지는 방법이며,
이후 런타임 버그가 발생해도, 클래스 단위로 제거해 보면 문제가 생긴 부분을 찾기도 훨씬 쉬워집니다.

앞으로 스크롤되는 배경이나 플레이어 캐릭터, 적 캐릭터, 아이템, 플레이어 총알, 적 총알 등등은 모두 이것을 기본으로 만들게 됩니다.
심지어는 적 캐릭터를 출연시키는 '진행 시나리오' 마저도 게임오브젝트에 근거해서 작성할 예정입니다.


안드엔진이나 cocos2d-x도 기본적으로 이와 비슷한 방식으로-물론 실제로는 훨씬 복잡하고 다양한 기능을 포함하여- 구현되어 있습니다.




* 게임 오브젝트 관리를 위한 추가 펑션
앞에서는 게임오브젝트 추가/제거를 위한 addChild, removeChild 정도만 작성했는데,

좀 더 본격적으로 게임을 만들게 되면 무엇보다 오브젝트를 획득하는 방법이 중요해집니다.
그러기위해서 아래와 같은 함수군을 준비했습니다.

public int getNumOfChildren() : node벡터에 있는 게임오브젝트 갯수를 얻어옵니다.
public GameObject getChildByTag(int tag) : 특정 tag에 해당하는 게임오브젝트를 찾아옵니다.
public GameObject getChildByName(String name) : 특정 name을 가진 게임 오브젝트를 찾아옵니다.
public Vector getChildrenByTag(int tag) : 특정 tag를 가진 게임오브젝트들을 얻습니다.
public Vector getChildrenByName(String name) : 특정 name을 가진 게임오브젝트들을 얻습니다.

getChildByTag나 getChildByName는 식별자인 tag나 name을 동일하게 가진 것이 있다면, 가장 먼저 발견된 게임오브젝트만 얻어옵니다.
그때문에 식별자가 중복되는 경우에는 어떤 것이 얻어질지, 장담할 수 없기 때문에 식별자를 유니크하게 관리해야 합니다.

그에 비해 getChildrenByTag나 getChildrenByName은 동일한 식별자를 가진 오브젝트들을 묶어서 돌려줍니다.
예를 들어 적 총알만 처리해야 할 경우, 적 총알의 name을 모두 "ENEMY BULLET"이라고 지어주고, getChildrenByName("ENEMY BULLET") 이라고 하면 적 총알만 모아서 뽑아낼 수 있습니다.



핑백

덧글

  • 수피 2016/02/18 03:54 # 삭제 답글

    질문이 있습니다.
    BasicScene이 node 벡터의 zorder를 비교해서 각각의 render()와 update()를 호출하잖아요?
    그렇다면 바로 그 BasicScene의 render(), update() 를 호출하는 곳은 어디 인가요?
    코드 어디에도 while(){ BasicScene.render(); BasicScene.update() } 같은 부분이 안보이는데 어디서 호출되는건지 궁금하네요.

    (강의 잘보고있습니다 :p)
  • 펭귄대왕 2016/02/18 06:39 #

    render와 update는 slick2d 내에 정의되어 있습니다.

    BasicScene은 slick2d의 BasicGameState를 상속하고 있고,
    BasicGameState에서 오버라이드 함수로 render와 update를 준비해서, 이 안의 내용을 구현해두면 slick2d가 알아서 호출하도록 되어 있습니다.

    아래 포스팅도 참고삼아 같이 봐주시면 좋을 듯 합니다.
    http://icegeo.egloos.com/310587
    http://icegeo.egloos.com/310355
  • 수피 2016/02/18 17:39 # 삭제

    답변 감사합니다.
    제가 알고싶던건 slick2d내의 어느 클래스가 update와 render를 반복호출 하는가였습니다.
    다행히 소스를 열어서 자문자답 했습니다.
    이미 알고 계시겠지만 다른분들 위해 참고삼아 적어두겠습니다.

    ProjectMain클래스가

    public static void main(String[] args){
    AppGameContainer appgc = new AppGameContainer(new ProjectMain("Slick2D Game Project")); // 인자로 ProjectMain을 넣음.
    appgc.start();
    }

    위 코드를 수행하면
    AppGameContainer의 start()에서 while(){ gameLoop(); } 를 반복 수행하고
    gameLoop() 는 main()에서 인자로 받은 ProjectMain의 update(), render()를 호출하는 식으로 짜여졌습니다.

    즉, main()이 자기자신을 인자로 주어 만든 AppGameContainer가
    main클래스의 부모인 BasicScene의 HashMap에서 Scene들을 하나씩 꺼내와 렌더링과 업데이트를 수행합니다.

    강의 시리즈 끝에 말씀하신대로 Slick2D 없이 AWT만으로 만들려다 보니 구조분석이 필요해서 드린 질문이었습니다.
    다시 한 번, 강의 감사드립니다 ㅎㅎ
  • 펭귄대왕 2016/02/19 05:49 #

    아 그런 의미셨군요.. 설명 감사합니다.

    awt에서는 기존에
    http://icegeo.egloos.com/275435
    이런 걸 한게 있는데
    여기서 dblpaint와 update가 slick2d의 render와 update와 같은 역할이라서
    간단한 게임은 약간의 수정을 해 주니까 바로 slick2d 기반으로 돌릴 수 있었습니다.

    반대로 slick2d로 만든걸 awt로 할 때는, slick2d에 편리한 api가 준비되어 있는걸 재구현하거나 생략해야하니 조금 부담스러운게 있었네요.
댓글 입력 영역


Books

Geek라이프

메가 드라이브 퍼펙트 카탈로그
마에다 히로유키 저/조기현 역

미소녀 일러스트 테크닉
B-은하, pen스케, 카와이 저/정유진 역

핵심강좌! Cocos2d-x
이재환 저

피규어의 교과서 레진 키트 & 도색 입문 편
후지타 시게토시 저/김정규 역
예스24 | 애드온2
일본서적 전문사이트 NEPIC