본문 바로가기

캔버스 루프 애니메이션 4편 『최종정리』

by Recstasy 2020. 2. 6.

캔버스 루프 애니메이팅 전체코드를 정리해보자.


1 index.html

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

<script>

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

             let game = new Game(); 

    })

</script>

index.html에서는 canvas를 선언하고, Game()클래스의 인스턴스(변수)를 실행한다. 


index.html 기능 : Game클래스 실행


2 Game클래스

Game클래스의 가장 중요한 역할은 이미지소스가 움직일 수 있는 정보를 제공하고 관리하는 것에 있다. Game클래스의 기능을 정리하면 대략 다음과 같다.


1] 캔버스 설정 => canvas 관련 API

2] 서버에 이미지 요청하기 => loadJSON

3] 프레임 정보 이미지 클래스에 보내기 => this.spriteImage

4] 캔버스에 이미지 띄우기 => render, update, refresh메서드


아래 코드는 캔버스로 어떤 어플을 만들건 가장 기본적인 사항이다. this.states 프로퍼티에는 이미지의 프레임, 루프여부, 위치정보(motion.x,motion.y),fps가 담겨있고, loadJSON 메서드 내부에 init()메서드를 넣음으로써 실행역할을 겸하고 있다.


class Game {

        constructor() {


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

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

            this.sprites = [];

            this.states = {

                walk: {

                    frames: [0, 1, 2, 3, 4, 5, 6, 7],

                    loop: true,

                    motion: {

                        x: 120,

                        y: 0

                    },

                    fps: 10

                }

            }

            const game = this;


            this.loadJSON("https://firebasestorage.googleapis.com/v0/b/webgame-786ab.appspot.com/o/bucket.json?alt=media&token=9642ad1e-0678-4509-ba4a-d11b5dd0c9d9", function (data, game) {

                game.spriteData = JSON.parse(data);

                game.spriteImage = new Image();

                game.spriteImage.src = game.spriteData.meta.image;

                game.spriteImage.onload = function () {

                    game.init();

                }

            });

        }


        loadJSON(json, callback) {

            var xobj = new XMLHttpRequest();

            xobj.overrideMimeType("application/json");

            xobj.open("GET", json + '.json', true);

            const game = this;

            xobj.onreadystatechange = function () {

                if (xobj.readyState == 4 && xobj.status == "200") {

                    callback(xobj.responseText, game)

                }

            }

            xobj.send(null);


        }


클래스를 보면 보통 시작부분에 엄청난 양의 프로퍼티가 등장한다. 프로퍼티는 요구사항이 던지는 메시지를 따라가다보면 자동으로 하나씩 늘어나게 되어있다특정 엔티티(사용자)의 요구사항이 던지는 메시지를 따라가자.



엔티티는 이미지가 화면에 나오기를 원한다. 그래서 Game클래스는 new Image객체를 생성해야하고, 해당 객체가 onload될 때 전체를 실행(game.init())한다. 위의 코드에서는 game.init()메서드가 game.spriteImage.onload 함수에 있는데 그 이유는 이미지가 불러온 뒤에 init()메서드가 실행되어야하기 때문이다. 


2-1 init메서드

init() {

            this.lastTime = Date.now();

            this.spawn();

            this.refresh();

        }


init메서드는 자동차로 말하자면 '시동을 켜는 역할'을 한다. 자동차 엔진은 기름을 받아서 피스톤을 움직이며 '폭발~배기' 반복운동을 한다. 게임엔진도 원리는 비슷하다. requestAnimationFrame(callback)구문에서 callback을 자기자신으로 하는 재귀(반복)구문이 곧 게임엔진의 핵심이다. 이를 위해 init()메서드는 현재 시간을 Game()클래스에 기록(this.lastTime)하고, 이미지를 띄우고(spawn), 반복(refresh)한다. 


시동걸린 자동차로 비유하자면 다음과 같다.


this.lastTime = 주행거리

this.spawn() = 연료주입

this.refresh() = 피스톤 반복운동


2-2 spawn(), refresh()메서드

시동이 걸렸다면(canvas 불러오기) 연료를 주입하고 엔진을 가동해야 한다. spawn()은 Sprite클래스를 생성하고, 해당 클래스에 필요한 정보(canvas,이미지데이터[json파일], 등..)를 전달한다. 게임엔진을 자동차엔진으로 생각해보자. spawn()메서드에서 생성되는 Sprite클래스는 마치 자동차의 연료와 같다. Sprite클래스는 이미지의 움직임과 관련된 정보가 담겨있고, Game클래스는 이를 이용해서 이미지를 움직인다. 이는 자동차 엔진이 연료에 담겨있는 성분을 폭발시켜 움직이는 원리와 같다.


  spawn() {

            const frameData = this.spriteData.frames[0];

            const sprite = new Sprite({

                context: this.context,

                image: this.spriteImage,

                x: 100,

                y: 150,

                states: this.states,

                state: 'walk',

                json: this.spriteData

            })


            this.bucket = sprite;

            this.sprites.push(sprite);

            this.spawnLife = 0;

        }


       refresh() {

            const now = Date.now();

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

            this.update(dt);

            this.render();

            this.lastTime = now;

            const game = this;

            requestAnimationFrame(function () {

                game.refresh();

            })

        }


refresh()메서드는 delta값(시간차)을 계산하여 프레임 계산에서 사용되는 "시간변화량" 정보를 전달한다. refresh()는 엔진의 핵심동작이며, 반복을 통해 화면에 이미지를 지속적으로 표현한다. 만일 사용자의 입력을 처리해야 한다면 input(사용자 입력)정보를 지속적으로 감시하고, 변화가 있을 경우 update()메서드에 전달하는 중요한 역할을 한다. 이는 자동차엔진에서 중요한 사건이 발생할 경우, 엔진경고등을 ecu센서가 전달하는 것과 같다.

2-3 update(), render()메서드

update()메서드는 현재 생성된 Sprite클래스의 인스턴스 객체에 '시간변화량(delta)'을 전달한다. 만일 update()메서드가 정지한다면 화면이 멈춰버린 것처럼 보인다.(refresh는 실행되고 있음) 


  update(dt) {

            for (let sprite of this.sprites) {

                if (sprite == null) continue;

                sprite.update(dt);

            }

        }


        render() {

            this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

            for (let sprite of this.sprites) {

                sprite.render();

            }

        }


    }


render()메서드는 복사된 Sprite객체들이 canvas문법에 맞춰 화면에 나타나도록 명령을 내린다. Game클래스는 .getContext(), clearRect()외의 어떠한 캔버스 API도 실행하지 않는다. 그 이유는 이미지 자체의 세부적인 사항은 Sprite클래스의 render메서드에서 처리하기 때문이다. 


따라서 이미지의 형태가 네모건 세모건 Game클래스는 render()명령만 실행할 뿐, 이미지 고유의 형태에 관해서 관여하지 않는다. 이는 유연한 설계의 기본이다.


만일 자동차 엔진이 특정 주유소의 휘발유에만 돌아간다면 엄청나게 불편할 것이다. 자동차 엔진은 모든 기름에 대해서 똑같은 행위(연소)를 반복하는 명령을 실행한다. 이는 Game클래스가 render()명령만 내릴뿐 어떤 이미지 소스인지 판단하지 않는 것과 같다.


3 Sprite클래스

Sprite클래스는 자동차의 '연료'라고 생각하자. 자동차 연료에는 '발화점', '옥탄가', '세탄가' 등.. 자동차가 움직이는 에너지를 얻기 위한 여러 정보를 포함한다. Sprite클래스 역시 이미지가 캔버스에서 움직이는 데에 필요한 여러 정보(캔버스정보, 시간, 위치, 크기, 이미지정보, 등)를 담고 있다. 


class Sprite {

        constructor(opt) {

            this.context = opt.context;

            this.image = opt.image;

            this.x = opt.x;

            this.y = opt.y;

            this.json = opt.json;

            this.currentTime = 0;

            this.scale = (opt.scale == null) ? 1.0 : opt.scale;

            this.opacity = (opt.opacity == null) ? 1.0 : opt.opacity;

            this.anchor = (opt.anchor == null) ? {

                x: 0.5,

                y: 0.5

            } : opt.anchor;

            this.states = opt.states;

            this.state = this.states[opt.state];

            this.state.duration = this.state.frames.length * (1.0 / this.state.fps);

        } 


Sprite클래스의 프로퍼티를 크게 정리하면 '속도', '시간', '거리'계산에 필요한 정보들이다. this.context, this.image, this.opacity를 제외한 나머지 프로퍼티는 모두 '프레임(시간)', '이동거리'와 연관되어 있다.


처음에는 opt에서 받아온 정보들(this.context, this.image, 등)을 프로퍼티에 입력하고, update()메서드에 집중하자.


3-1 update()메서드

Game클래스에서 update()메서드에 delta값을 전달했다. delta값은 init()메서드가 실행될 때 기록한 시간에서 refresh가 실행될 때의 시간을 뺀 값에 1000을 나눈 값이다.(밀리초) 프레임 계산과 관련된 설명은 『캔버스 루프 애니메이션 3편』을 참고하자. 


update(dt) {

       this.currentTime += dt;

            if (this.currentTime > this.state.duration) {

                if (this.state.loop) {

                    this.currentTime -= this.state.duration;

                }

            }

      this.x += this.state.motion.x * dt;

      this.y += this.state.motion.y * dt;


            if (this.x > 500) this.x = -100;


     const index = Math.floor((this.currentTime / this.state.duration) * this.state.frames.length);

     this.frameData = this.json.frames[this.state.frames[index]];

        }


이미지의 정보가 담긴 json과 이미지 파일은 『캔버스 루프 애니메이션 1편』에서 볼 수 있다. update()메서드는 프레임을 계산하고, 이미지가 움직이는 속도와 거리에 따른 루프정보를 update한다. 즉, update()는 Sprite클래스의 움직임을 관할한다.


3-2 offset()메서드

게임에서 offset값은 이미지와 상관없는 여백을 의미한다. 해당 설명은 『캔버스 애니메이팅 2편. 좌표축 이해』편에서 볼 수 있다. offset값은 json파일의 frameData key값을 통해 접근할 수 있다.


 get offset() {

            const scale = this.scale;

            const w = this.frameData.sourceSize.w;

            const h = this.frameData.sourceSize.h;

            const x = this.frameData.spriteSourceSize.x;

            const y = this.frameData.spriteSourceSize.y;

            return {

                x: (w - x) * scale * this.anchor.x,

                y: (h - y) * scale * this.anchor.y

            }

        }


3-3 render()메서드

render()메서드는 Game클래스의 render()메서드에서 sprites배열속의 각각의 sprite(Spirte객체 인스턴스)의 랜더링을 실행할 때 구현된다. Sprite클래스는 canvas API, drawImage()를 통해 프로퍼티에 담긴 정보를 움직임으로 변환한다.


render() {

            const alpha = this.context.globalAlpha;

            this.context.globalAlpha = this.opacity;

            const frame = this.frameData.frame;

            const offset = this.offset;

            this.context.drawImage(this.image, frame.x, frame.y, frame.w, frame.h, this.x - offset.x, this.y - offset.y, frame.w * this.scale, frame.h * this.scale)

            this.context.globalAlpha = alpha;

        }

    }

 

캔버스 루프 애니메이팅은 콘텐츠 웹앱제작의 필수적인 과정이다. 게임에서는 배경과 적의 움직임을 만들어야하고, 캐릭터 관련 어플에는 사용자 반응에 따른 이미지의 모션을 구현해야 한다. 현재 코드는 다양한 웹앱 제작을 위한 관문 정도로 익혀두면 좋을 듯하다.


//루프 애니메이팅 구현


댓글

최신글 전체

이미지
제목
글쓴이
등록일