JS로 간단한 Windows 게임 만들어보기 – part 1. 2D 개발 준비
JS로 간단한 Windows 게임 만들어보기 – part 1. 2D 개발 준비
개요
Windows 8 이후로 마소에선 HTML 앱을 브라우저 없이 네이티브 앱으로 지원합니다. 이 방법은 개발 자체가 편하다는 엄청난 장점이 있죠.
게임 개발에서 js 의 단점이라면 단연코 속도 문제가 되겠지만, 요즘같은 세상에 간단한 게임 정도는 js 로도 충분히 돌릴 수 있습니다.
이 시리즈에서는 (아마도) js 와 html5 의 캔버스, 오디오 등의 요소를 가지고 간단한 디펜스 게임을 만들어볼 예정입니다. 먼저 브라우저에서 동작하는 형태의 웹 앱으로 만든 뒤에 이를 윈도용 앱으로 바꾸는 과정을 거치도록 하겠습니다.
보통 이런 건 다 만든 다음에 쓰는 게 순서지만 진짜로 이제 시작하는 거기 때문에 정말 다 될 지 장담할 수가 없네여…
먼저 이번 시간에는 화면에 간단한 그림을 표시해주는 부분까지 만들어보도록 하겠습니다.
앱 틀
Windows(이하 윈도) 용 앱을 만들려면 일단 Visual studio 를 설치해야 하지만, 현재 제 윈도 데탑이 메이플스토리2 플레이어에게 점유당한 관계로 맥북에서 시작해보도록 하겠습니다.
아래 작업들의 상당수는 Visual studio 의 새 프로젝트 기능을 쓰면 자동으로 처리해주는 부분들입니다. 이 점 유념하시고 대충 봐 주세요.
윈도용 앱에는 기본적으로 default.html 파일이 필요합니다. 웹앱의 index.html과 같은 역할이라고 보면 됩니다.
이 파일의 내용을 보면, 사실 평범한 html 파일입니다.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Bug defense!</title>
<link rel="stylesheet" href="style/default.css" />
<script src="js/default.js"></script>
</head>
<body>
<canvas id="gameCanvas"></canvas>
</body>
</html>
별로 중요한 내용도 없구요. 게임에 사용할 캔버스 하나가 덩그러니 있는 html이죠.
여기서 링크된 파일중에 default.css 는 아직 비어있습니다.
default.js 파일을 먼저 살펴보면..
(function(){
'use strict';
var app = (typeof WinJS != 'undefined')?WinJS.Application:window;
if( Window.ApplicationModel && Window.ApplicationModel.Activation){
app.onactivated = function(args){
if( args.detail.previousExcutionState !== activation.AppliactionExcutionState.terminated){
//new launched
initialize();
} else {
//restore from suspend
}
}
args.setPromise(WinJS.UI.processAll());
}else{
window.addEventListener("load", initialize);
}
})();
'use strict'는 뭐 그냥 좀 더 엄격한 js 문법을 사용하겠다는 선언이구요.. 없어도 상관은 없는 부분입니다.
WinJS 라는 부분이 눈에 띄실텐데, 이 부분은 윈도용 어플리케이션을 개발할 때 제공해주는 라이브러리라고 보시면 될 것 같아요. 그 아래쪽으로는 일반적인 아이폰이나 안드로이드 앱을 개발해보신 분이라면 뭔지 아실만한 부분인데요, 앱이 처음 실행된건지, 아니면 잠시 백그라운드로 돌려졌다가 다시 활성화된 건지에 따른 분기문이구요.
이 파일을 웹브라우저에서 돌렸을 경우에는 WinJS 와 그 아래 관련된 내용이 모두 없어서 에러를 유발하기 때문에 전체를 조건으로 묶어두었습니다. 아무래도 웹브라우저에서 바로 디버깅을 하는 게 편하거든요. 이건 제가 당장 vs를 쓸 수 없는 상태인 이유가 크지만..
윈도 로드 이벤트에 initialize 라는 함수를 등록하는 부분이 보이는데, 아직 작성하지 않은 함수네요. 이 부분은 뒤에 다시 작성하도록 하겠습니다.
화면에 이미지를 찍어보자
스프라이트
아시는 분은 아시겠지만 2D 게임에서 가장 중요한 키워드는 바로 스프라이트(sprite)입니다. 알파채널이 없던 시절부터 기본적으로 투명한 부분이 있는 이미지를 스프라이트라고 불러왔었는데요. 지금은 뭐 대충 2D 화면에 보이는, ‘변하지 않는 배경’을 제외한 모든 이미지를 스프라이트라고 보면 될 것 같습니다. 스프라이트에 대한 자세한 개념은 게임 개발 서적들을 참고하시구요, 여기서는 간단히 구현해보도록 하겠습니다.
먼저 lib.js 라는 파일을 새로 만들었습니다.
이 파일에 sprite 라는 클래스~~(이 표현이 저는 참 맘에 안드는데 뭐 대체할 것도 애매하니까 일단 쓰겠습니다)~~를 선언해봅시다.
디펜스 게임을 탑뷰 형식으로 만들기 위해서는 원점을 지정하고 회전시킬 수 있는 기능과, 파일 하나에 여러개의 스프라라이트를 그려서 분리해 사용할 수 있는 기능 정도가 있으면 좋을 것 같습니다. 그 정도를 염두에 두고 속성을 작성해봅시다.
"use strict";
var app = window;
// sprite class
function Sprite(opt){
// basic properties
this.x = opt.x||0;
this.y = opt.y||0;
this.width = opt.width||0;
this.height = opt.height||0;
// 4 properties in below are for separated sprites from an sprite sheet image.
this.sx = opt.sx||0; // x position in the source
this.sy = opt.sy||0; // y position in the source
this.sw = opt.sw||0; // width in the source image
this.sh = opt.sh||0; // height in the source image
this.image = new Image();
this.rotation = opt.rotation||0;
this.scale = opt.scale||1;
this.originX = opt.originX||0.5;
this.originY = opt.originY||0.5;
this.offsetX = 0;
this.offsetY = 0;
// event handlers
this.ev = {};
this.ev.loaded = opt.loaded || null;
var self = this;
this.image.addEventListener('load', function(){
self.width = self.sw || self.image.width;
self.height = self.sh || self.image.height;
if( self.originX !== undefined ) {
self.offsetX = self.width * self.originX;
self.offsetY = self.height * self.originY;
}
console.log("Image " + self.image.src + " has loaded");
if(self.ev.loaded){
self.ev.loaded();
}
});
this.image.addEventListener('error', function(err){
console.log(err);
});
if( typeof opt.image == "string" ){
this.image.src = opt.image;
}else if(typeof opt.image == "object" && opt.image.src ){
this.image.src = opt.image.src;
}
}
// methods
Sprite.prototype.render = function(ctx){
ctx.save();
ctx.translate(this.x, this.y);
ctx.scale(this.scale, this.scale);
ctx.rotate(this.rotation *Math.PI / 180);
if( this.sw && this.sh ){
ctx.drawImage(this.image, this.sx, this.sy, this.sw, this.sh, -this.offsetX, -this.offsetY, this.sw, this.sh);
}else{
ctx.drawImage(this.image, -this.offsetX, -this.offsetY);
}
ctx.restore();
}
이 중에 originX, originY 의 경우는 원점을 지정하는 0.0 ~ 1.0의 비율을 지정합니다. scale 도 그렇고, rotation 은 라디안 값을 사용하기는 귀찮으니까 일반 각도를 받기로 합시다.
위 코드는 하나의 이미지파일도 각 스프라이트가 별개의 개체로 들고 있다는 점에서 메모리를 쓸데없이 더 처묵처묵하는 게 아닌가 하는 의구심이 들 수 있지만, 괜찮습니다. 문제가 되는 시점이 오면 그 때 고치면 돼요. 사실 안 올 거 같구요. 이런걸 기술 부채라고 하던가요?
이렇게 많은 인자를 받아서 이미지를 로드했으면, 이 이미지를 화면에 찍는 메소드가 있어야겠죠. js 니까 프로토타입에다가 메소드를 정의해줍시다.
Sprite.prototype.render = function(ctx){
ctx.save();
ctx.translate(this.x, this.y);
ctx.scale(this.scale, this.scale);
ctx.rotate(this.rotation *Math.PI / 180);
if( this.sw && this.sh ){
ctx.drawImage(this.image,
this.sx, this.sy, this.sw, this.sh,
-this.originX, -this.originY, this.sw, this.sh);
}else{
ctx.drawImage(this.image, -this.originX, -this.originY);
}
ctx.restore();
}
위 메소드가 인자로 받는 ctx 는 html의 canvas 객체가 제공하는 2dcontext입니다. 이걸 받아서 여기다가 찍고 돌려주는거죠. 다른 객체들의 렌더링 루틴에 영향을 주지 않기 위해 먼저 현재 상태를 저장하고, 위치/크기/회전을 적용한 뒤에 스프라이트를 찍고 다시 컨텍스트를 되돌리는 작업을 하는 내용입니다. 뭔 말인지 모르겠으면 HTML5 의 캔버스 관련 문서를 찾아보시기 바랍니다.
원래 윈도 유니버셜 앱을 위한 js 파일은 전체를 클로저로 감싸줘야 합니다…만 일단 브라우저에서 동작을 확인해보기 위해 이렇게 작성하도록 하겠습니다.
HTML 파일에 추가
새로운 라이브러리를 작성했으니 사용하려면 레퍼런스를 추가해야겠죠? default.html 파일에 아래 내용을 추가합니다.
<script src="js/lib.js"></script>
추가하실때는 default.js를 불러오는 부분보다 위에 넣어주세요.
이미지 파일 준비
미리 말을 안 했는데, 이 프로젝트는 위와 같은 디렉토리 구조를 가지고 있습니다. 행여나 따라하시는 분들이 있으면 위와 같은 형태로 트리를 만들어서 작업해주세요.
assets/images 디렉토리에 적당한 이미지 파일을 넣고 불러와봅시다. 제 경우에는 sprites.png라는 파일을 그려서 넣었습니다.
이 파일은 가로세로 600 x 600 크기에 각 60 x 60 영역마다 캐릭터를 하나씩 그려둔 파일입니다. 이걸 가로세로 60씩 자르면 하나 하나의 스프라이트가 되겠죠. 아직 다 그린게 아니라서 이미지를 올려드리긴 미묘하지만 나중에 완성단계에 이르면 함께 올려드릴 수 있을겁니다. 대충 아무 이미지나 넣어서 테스트 해 주세요.
화면에 찍어보기
캔버스 초기화
default.js 파일로 돌아와서, 먼저 캔버스를 초기화해보도록 합시다. 이후에는 각 게임 장면(scene)별로 클래스를 나누고 추상화하고 하는 귀찮은 작업들이 필요하지만, 오늘은 일단 그림을 찍는게 목표니까 간단하게…
function initialize(){
app.canv = document.getElementById("gameCanvas");
app.ctx = app.canv.getContext("2d");
testRender();
}
testRender()라는 함수를 호출하고 있지만, 해당 함수는 아직 없죠? 이것도 만들어줍시다.
스프라이트 생성 및 찍기
function testRender(){
var sprite = new Sprite({
image:'assets/images/sprites.png',
sw:60, sh:60,
x:100, y:100,
loaded:function(){
console.log("Render Image");
sprite.render(app.ctx);
}
});
}
여기까지 작업한 뒤, 브라우저에서 default.html 파일을 열어보면 화면에 이미지가 찍히는 걸 볼 수 있습니다.
다음편 예고
다음 포스트에서는 게임의 기본 구조, 즉 업데이트와 화면 렌더링 루프를 만들고 각 장면 클래스를 정의해 보도록 하겠습니다.
물론 예고는 다 믿을 수는 없지만 일단 계획은 그렇습니다.