본문 바로가기

캔버스 게임만들기 2편『멀티 이미지 넣기』

by Recstasy 2019. 11. 2.


지난 캔버스 포스팅에서는 꽃이미지 1개를 웹 브라우저에 띄웠다. 이번에는 이보다 조금 더 진보한 형태의 캔버스 기능을 생각해보자. 


먼저 캔버스에 이미지 2개를 띄우려면, 배열을 생각할 수 있다.  배열에 객체를 하나씩 담고, 시간 흐름에 따라 하나씩 랜더링 한다면 여러개의 이미지를 캔버스에 띄울 수 있다. 이와 관련된 대략적인 기능을 유스케이스로 정리하면 아래와 같다. 


1] 초단위로 시간이 흐른다

2] 자동으로 이미지가 하나씩 생성된다 


이벤트나 사용자 반응이 없기 때문에 유스케이스라 말하기 힘들지만, 최소한 위의 2가지 동작이 필요하다는 사실은 알 수 있다. 한개의 이미지를 띄웠던 지난 포스팅의 코드에서 배열과 자바스크립트 내장함수(시간)를 첨가해보자.



1 Canvas 기본코드

canvas의 기본코드는 지난번 이미지 로딩을 진행했던 것과 거의 동일하다. Game클래스를 생성하고, 속성에서 sprites배열을 추가하는 부분만 변경되었다.


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

<script>

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

                   let game = new Game();

         });

    class Game{

        constructor(){

                const game = this;

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

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

          

                this.sprites = [ ];

                this.spriteImage = new Image();

                this.spriteImage.src ="https://firebasestorage.googleapis.com/v0/b/webgame-786ab.appspot.com/o/flower.png?alt=media&token=076f5c0a-6733-466f-9c6b-4e8ccc6b29af"


                this.spriteImage.onload = function(){    

                    game.lastTime = Date.now();

                    game.spawn(this);

                    game.refresh();

               }  

          }

   }


Game클래스의 책임은 canvas영역에 여러 소스들을 띄우는 것에 있다. 이를 위해서 canvas를 받고, .getContext()를 지정한다. this.spriteImage속성에는 자바스크립트 내장객체인 new Image()가 들어가고, src(경로지정). onload(로드 이후 실행함수)메서드를 실행한다. 여기서 src와 onload와 같은 메서드는 이미 자바스크립트에서 지정된 내장객체이므로 사용자는 있는 그대로 이용만 할 뿐이다.



2 Game클래스의 기본기능

설계부터 진행한다면, 사실 game클래스의 속성보다 '기능'위주로 진행해야한다. 하지만 지금처럼 간단한 기능을 만들 때는 크게 상관없다. 이미지를 여러개 만드는 코드에서 game클래스의 역할은 4가지다.


1] spawn() :: 이미지 속성값 지정

2] refresh() :: 프레임 계산

3] update() :: 변하는 부분 체크 

4] render() :: 이미지 화면에 표현하기


spawn()을 제외한 refresh(), update(), render()메서드는 게임을 관리하는 Game 클래스의 기본 기능이다. 게임관리자(Game클래스) refresh()를 통해 프레임을 계산한다. refresh()는 재귀함수인데, 반복적으로 업데이트가 필요한 변수를 지속적으로 update()메서드에 공급해준다. 


update()메서드는 바뀌는 부분을 감시하고 있다가 특정 속성값이 변하면 즉시 업데이트를 진행한다. 가령, 사용자가 이동키를 클릭하면 update()메서드는 이벤트값을 확인해서 해당 객체의 속성값을 업데이트한다


refresh()와 update()의 역할에 혼란을 느낄 수 있는데, refresh()는 게임이 정상적으로 작동되기 위해서 자동으로 변한다. 반대로 update()는 내외부의 반응에 의해 약속된 조건이 변했을 때 작동한다. 즉, 감시가 update(), 반복은 refresh()라고 생각하면 된다.         


 class Game{

        constructor(){  ...중략... }


             spawn(){

                     var img = this.spriteImage;

                      var options = {

                          context : this.context,

                          width : img.width,

                          height : img.height,

                          image : img,

                          x : Math.random()*this.canvas.width,

                          y : Math.random()*this.canvas.height

                    }

                    let sprite = new Sprite(options);

                    this.sprites.push(sprite);

                    this.sinceLastSpawn = 0;

              }

      

              refresh(){

                   var now = Date.now();

                   var dt = (now - this.lastTime)/1000.0;

                    this.update(dt);

                    this.render();

                    this.lastTime = now;

                    const game = this;

                    requestAnimationFrame(function(){

                               game.refresh();

                    })

             }


             update(dt){

                    this.sinceLastSpawn += dt;

                    if(this.sinceLastSpawn > 1){

                          this.spawn();

                       }

             }


             render(){

                      for(let sprite of this.sprites){

                             sprite.render();

                      }

             }

     }


위의 코드에서 핵심은 spawn()이다. spawn()은 sprites배열에 저장된 각 이미지들을 불러내어 하나씩 랜더링하는 기능을 담당한다. 게임으로 말하자면, 게임을 구성하는 이미지 리소스들을 화면에 구현하는 역할을 한다고 볼 수 있다. 


그리고 spawn()메서드에 있는 sprite변수는 현재 상태에서는 다소 경직되어 있는데, 사용자의 이벤트와 연결되지 않았기 때문이다. this.spriteImage의 src에는 특정 경로가 정해져있다. 중간에 추상 클래스를 만들고, 해당 클래스를 상속받는 이미지 소스가 있다면 요구사항 추가에 유리하고, 변형에 닫혀있는 유연한 코드를 작성할 수 있다. 



3 Sprite클래스

Sprite클래스는 랜더링 기능을 수행한다. Game.spawn()에서 설정한 옵션을 Sprite클래스는 그대로 받아서 화면에 띄워야한다. 이를 위해서 Game클래스의 render()메서드와 sprite의 render()는 리스코프 치환법칙에 의해 같은 메서드명을 사용한다. 


class Sprite{

          constructor(options){

              this.context = options.context;

              this.width = options.width;

              this.height = options.height;

              this.image = options.image;

              this.x = options.x;

              this.y = options.y;

          }

          render(){

              this.context.drawImage(

                 this.image,this.x,this.y)

         }

    } 


Sprite클래스의 인자값으로는 Game클래스에서 보낸 옵션값이 들어가고, 속성값은 게임에서 사용될 이미지의 속성이 나열된다. 여기서 랜더링을 직접적으로 실행하는 코드가 Game()클래스가 아닌 Sprite()클래스에 있는 이유는 'SRP(단일책임원칙)'객체지향 원칙에 의한 약속이다. Game()클래스의 책임은 Game에서 사용되는 리소스들을 관리하는 것에 국한될 뿐, 랜더링을 해야하는 의무가 없다.  


refresh()메서드에서 계산된 프레임값(dt)은 update()메서드로 보내지고, 시간의 변화에 따라 spawn()은 반복적으로 실행된다. 그 결과 아래와 같은 다수의 이미지가 시간의 흐름에 따라 나타나게 된다.

 


댓글

최신글 전체

이미지
제목
글쓴이
등록일