본문 바로가기

캔버스 게임만들기 1편『캔버스에 이미지 띄우기』

by Recstasy 2019. 10. 24.

자바스크립트 기본기가 어느정도 된다고 생각되면 대부분 게임코드를 한번씩 찾게 된다. 자바스크립트 뿐만 아니라 모든 컴퓨터언어에서 '게임코드'는 유혹의 대상이다. 하지만 막상 게임코드를 보면 이런 생각이 든다.


"이게 이렇게 복잡했어?"


계산기 프로그램 같은 예제를 보면서 키웠던 자신감은 '대충 미사일 하나 쏘는 이벤트'에 무너져버린다. 물론 유니티나 언리얼 엔진과 같은 게임 엔진 덕분에 게임 제작은 누구나 할 수 있다. 그러나 게임제작은 결코 만만한 작업이 아니다. 게임엔진으로 빌드한 결과물만 본다면야 "이 정도는 할 수 있다"라고 생각할 수도 있겠지만, 설계부터 생각해보면 말이 달라진다.


'캐릭터', '무기 인벤토리', '유저', '스테이지' 등 엔티티(유저, 기타)에 따라 게임설계는 충분히 유연해야 하며, 이에 따라 추상화가 밥 먹듯이 사용된다. 만일 '오픈월드'를 지향하는 게임을 설계하려면, 사용자와 관련된 객체 뿐만 아니라 게임 내의 모든 객체들이 추상화 패턴을 따라야 하고, 유연하게 변해야 한다. 또, 버전업까지 항상 염두에 둬야하는 게임의 특성을 고려한다면,  깔끔한 '아키텍처'가 필요하다. 


제대로 된 게임을 제작하려면 코딩의 경계를 넘어서 설계의 차원으로 들어가야 하고, 다시 설계의 차원에서 아키텍처의 차원을 뛰어넘어야 한다. 이렇게 말하는 필자 역시 코딩과 설계의 경계선에서 허우적대고 있다. 만일 스스로 코더 수준에 만족한다면, 게임엔진(유니티,언리얼,cocos2D...)을 사용하는 편이 훨씬 효율적이다. 


하지만 아키텍처의 차원을 목표하는 개발자라면, 과감하게 가시밭길로 들어가야 한다. 늘 그렇듯 아무나 갈 수 있는 길에는 보물이 없다. 게임만들기 포스팅에서는 간단한 게임을 최대한 객체지향 설계와 아키텍처 차원에서 분석해보자.


1 Game 클래스

캔버스를 활용해서 이미지를 띄우는 코드는 구글검색만 하더라도 수백 개의 코드가 나온다. 문제는 Game 클래스가 하는 역할과 책임 그리고 협력관계에 있다. Game클래스의 기능만 이해한다거나 관계만 바라봐서는 설계나 구조를 정확히 볼 수 없다. 



아래는 게임 기본 코드다. 


<script>

 class Game{

   constructor(){


        this.canvas = document.getElementById("game");

        this.context = this.canvas.getContext('2d');

}

</script>


<script>

      document.addEventListener("DOMContentLoaded",function(){

          const game = new Game();

})

</script>


//html 부분

<canvas id="game" width="640" height="480" style="border:1px solid #000000"></canvas>


위의 코드를 실행해보면 캔버스 구역을 표시하는 크기, 640 x 480의 구역만 화면에 표시된다. 일단 코드를 보기에 앞서, Game 클래스의 책임을 생각해보자. Game클래스가 맡고 있는 책임이 뭘까? 


위의 상황에서는 <canvas id="game"...></game>부분에 무엇을 나타낼 지를 Game클래스에서 결정하고 있다. 즉, Game클래스는 'game'이라는 canvas태그에 나타날 사항들을 관리하는 책임을 가지고 있다. 일종의 '화면관리자'인 셈이다. 이를 MVC패턴으로 굳이 표현하자면 Game클래스는 View의 역할과 책임을 맡고 있다. 단, View의 역할이 아주 큰 상태다. 


Game클래스를 '화면관리자'라 부른다면, canvas의 id를 받아서 어떤 화면에 게임을 그려야할 지 정해주는 일을 먼저 해야한다.(위의 코드) 그리고 그 다음 할일은 무엇일까?



2 구성요소 불러오기

어디에 Game요소를 표시할 지를 정했다면, 그 다음 할일은 Game에 사용될 요소를 불러오는 일이 바로 화면관리자가 할 일이다. 이를 위해서 Game클래스는 화면에 그려줄 이미지 자료(Data)들을 'Model'클래스에 요청한다. 하지만 아래 코드는 간단하기 때문에 Model부분을 직접 작성하지 않고, 바로 소스를 불러오는 방식으로 만들었다. 


<script>

 class Game{

   constructor(){


        this.canvas = document.getElementById("game");

        this.context = this.canvas.getContext('2d');


      this.spriteImg = new Image();

       this.spriteImg.src = "https://tistory2.daumcdn.net/tistory/2784544/skin/images/r_search.png"

        }

}


</script>


<script>

      document.addEventListener("DOMContentLoaded",function(){

          const game = new Game();

})

</script>


<canvas id="game" width="640" height="480" style="border:1px solid #000000"></canvas>


Game클래스의 속성 spriteImg값으로 내장객체 new Image()를 넣는다. new Image()는 웹 브라우저에서 사용할 수 있는 내장객체이며, 'src'처럼 미리 정해진 속성값을 사용할 수 있다. 


위의 코드를 실행하더라도 화면의 변화는 나타나지 않는데, 화면관리자(Game클래스)가 단순히 모델을 불러오는 것만으로는 변화가 발생할 리 없다. 화면에 뭔가 변화가 있으려면, 불러온 모델을 보여주는 기능이 있어야 한다. 이를 위해 화면관리자는 화면에 보여주는 기능(랜더링)을 가지고 있는 클래스를 만들어야 한다. 



3 랜더링 기능 만들기

화면 관리자(Game클래스)가 직접 랜더링 기능을 담당해도 되지만, 이는 객체지향 설계에서 SRP(단일책임 원칙)위반에 해당한다. 객체지향 설계에 있어 SRP는 상당히 중요한데, 클래스는 단 하나의 책임만 져야한다. 가령, 스마트폰을 생각해보자. 

대다수 스마트폰에 있는 '홈키'의 책임은 모든 작업에서 빠져나오는 것이다. 기능으로는 '배경화면으로 가기' 혹은 '재부팅'과 같은 여러 기능이 있을 수 있다. 그런데 경영진에서 다음과 같은 명령이 떨어졌다고 가정해보자. 

``이번 신제품에 적용될 홈키는 '출력 기능'에 관한 기능들을 넣으시오``

홈키를 한번 누르면 종료되고, 두번 누르면 화면의 배경설정이 달라지고, 홈키를 세번 누르면 화면의 ~~~~, 홈키를 네번 누르면 ~~~~~. 


아마도 홈키의 기능이 여러 개가 될수록 관련 책임이 복잡해지고, 사용자는 복잡함을 넘어 당황할 것이다. 홈키 클래스의 책임이 여러개가 된 스마트폰은 범용성이 떨어진다. 특정 상황에 국한된 설계는 상황이 변하면 책임간 충돌을 일으키거나 동시에 기능이 구현되는 식의 여러가지 에러를 발생시킨다. 즉, 유연한 설계가 되지 못한다. 

게임 설계 역시 SRP를 지켜야 한다. 화면관리자(Game클래스)는 말 그대로 화면을 관리할 책임만 있을 뿐, 구성요소에 대한 세부설정은 다른 클래스에게 맡겨야 한다.

<script>

 class Game{

   constructor(){

        this.canvas = document.getElementById("game");

        this.context = this.canvas.getContext('2d');


        this.spriteImg = new Image();

        this.spriteImg.src = "https://tistory2.daumcdn.net/tistory/2784544/skin/images/r_search.png"


        const game = this;


           this.spriteImg.onload = function(){

              const options = {

                    context:game.context,

                    height:this.height,

                    width:this.width,

                    image:this

                 }


              game.sprite = new Sprite(options);

              game.sprite.render();   

           }

    }

}


 class Sprite{

    constructor(options){


          this.context = options.context;

          this.width = options.width;

          this.height = options.height;

          this.image = options.image;

    }

    render(){

         this.context.drawImage(this.image,200,100);

    }

 }

</script>



<script>

      document.addEventListener("DOMContentLoaded",function(){

          const game = new Game();

})

</script>


<canvas id="game" width="640" height="480" style="border:1px solid #000000"></canvas>



4 sprite클래스

화면관리자는 단지 불러올 뿐이다. 실제 랜더링 기능은 sprite클래스에서 담당한다. sprite클래스의 책임은 '랜더링'이며, 화면관리자의 책임은 '불러오기'다. 아래코드에서는 sprite클래스는 정의하고, 파라미터에서 Game클래스가 불러온 데이터 값을 받는다. render()메소드는 sprite클래스에 정의되어 있으며, 화면 관리자(Game클래스)는 render기능을 받아서 그대로 실행시킨다.


<script>

 class Game{

   constructor(){

        this.canvas = document.getElementById("game");

        this.context = this.canvas.getContext('2d');


        this.spriteImg = new Image();

        this.spriteImg.src = "https://tistory2.daumcdn.net/tistory/2784544/skin/images/r_search.png"


        const game = this;

        this.spriteImg.onload = function(){


              const options = {

                    context:game.context,

                    height:this.height,

                    width:this.width,

                    image:this

                 }


              game.sprite = new Sprite(options);

              game.sprite.render();   

           }

    }


}

 class Sprite{

    constructor(options){


          this.context = options.context;

          this.width = options.width;

          this.height = options.height;

          this.image = options.image;

    }

    render(){

         this.context.drawImage(this.image,200,100);

    }

 }


</script>


<script>

      document.addEventListener("DOMContentLoaded",function(){

          const game = new Game();

})

</script>


<canvas id="game" width="640" height="480" style="border:1px solid #000000"></canvas>


위의 코드를 정리하면 다음과 같다.


1] Game클래스(화면관리 책임) : 게임에 필요한 데이터들을 불러와서 화면에 띄워줌

2] Image클래스 : 이미지 데이터의 기능을 제어

3] Sprite클래스 : 이미지 객체를 화면에 보여줌 


Game클래스는 Image클래스, Sprite클래스와 협력함으로써 화면을 관리하는 책임을 이수하고 있다. 


<구현>


댓글

최신글 전체

이미지
제목
글쓴이
등록일