[html5]검색결과, 63건
- [Node.js] socket.io를 활용한 웹채팅 구현 2016.07.15 735
- [HTML5 Game] Shooting Down 2013.10.16 6
- [HTML5 Game] Sound Effect 2013.10.11 16
- [HTML5 Game] Firing Bullet 2013.10.08 8
- [HTML5 Game] Moving Object Ⅱ 2013.10.02 9
- [HTML5 Game] Moving Object 2013.10.02 6
- [HTML5 Game]Calculating FPS 2013.09.30 8
- [HTML5 Game] Game State 2013.09.27 9
- [HTML5 Game] Game Loop 2013.09.25 9
- [HTML5 Game] Collision Detection 2013.09.23 15
- [HTML5 Game] Parallxing Background Animation 2013.09.21 11
- [HTML5 Game] Background Animation 2013.09.20 12
- [HTML5 Game] Character Animation Using Trident.js 2013.09.17 12
- [HTML5 Game] Character Animation Using Sprites 2013.09.17 8
- [HTML5 Game] Character Animation 2013.09.14 7
재밌네... ㅎㅎ
별도의 비표준 플러그인 없이, 웹에서 채팅을 이렇게 쉽게 구현할 수 있는 시대라니...
6년전, 웹 소켓에 대해 나름 정리한 적이 있다.
이미 그 시대는 오래전(?)에 도래했건만, 개인적으로 방치해 두다가, 어제 밤 문득...
웹 소켓과 node.js를 사용해서 채팅 샘플을 구현해 보고 싶다는 생각이 스쳐 지나갔다.
socket.io라는 자바스크립트 라이브러리가 존재한다더니, 이건 뭐.. 다른건 볼 필요도 없네.
socket.io는 Node.js 기반으로 소켓 통신이 가능하도록 해 주는 확장 라이브러리인데, HTML5의 정식표준인 WebSocket을 지원하지만, 미지원 환경에 대한 호환성을 위해 Comet 같은 기술도 하나의 API로 추상화 시킨 양방향 통신 라이브러리이다.
socket.io는 WebSocket이 지원되지 않는 환경에서 실시간 양방향 통신을 구현하기 위해 다음과 같은 fallback 기술을 사용하고 있다.
- FlashSocket, AJAX Long Polling, AJAX Multi part Streaming, IFrame, JSONP Polling
기본적으로 Node.js 설치해 주고, express와 socket.io를 npm을 통해 다음과 같이 설치한다.
- npm install express
- npm install socket.io
이제 환경 설정은 끝이다. 자바스크립트 기반 개발환경은 이렇듯 심플하다. 내가 좋아하는 이유 중 주요한 팩터이다.
* socket.io를 이용한 웹 채팅 구현하기
먼저 다음과 같은 3개의 파일을 준비한다.
1. chatServer.js
- 채팅 서버 역할을 하는 자바스크립트 파일로, Node.JS에 의해 호스팅되며 socket.io 라이브러리를 통해 소켓 서버를 구현한다. 또한 부가적으로 http 리소스 요청에도 응답할 수 있도록 한다.
2. chatClient.js
- 채팅 서버와 통신을 하는 클아이언트 자바스크립트 파일로, 역시 socket.io 라이브러리를 사용하며 채팅을 위한 간단한 DOM 조작을 수행한다.
3. chatClient.html
- chatClient.js가 정의된 html 파일이며, 채팅창과 텍스트 박스로 간단히 구성된 뷰를 제공한다.
먼저 chatServer.js 파일의 코드부터 보자.
var url = require("url");
//루트에 대한 get 요청에 응답
app.get("/", function(req, res){
console.log("get:chatClient.html");
//최초 루트 get 요청에 대해, 서버에 존재하는 chatClient.html 파일 전송
res.sendFile("chatClient.html", {root: __dirname});
});
//기타 웹 리소스 요청에 응답
app.use(function(req, res){
var fileName = url.parse(req.url).pathname.replace("/","");
res.sendFile(fileName, {root: __dirname});
console.log("use:", fileName);
});
//http 서버 생성
var server = require('http').createServer(app);
server.listen(3000);
console.log("listening at http://127.0.0.1:3000...");
//클로저를 사용해, private한 유니크 id를 만든다
var uniqueID = (function(){
var id = 0;
return function(){ return id++; };
})();
//서버 소켓 생성
var socket = require('socket.io').listen(server);
//소켓 Connection 이벤트 함수
socket.sockets.on('connection', function(client){
//클라이언트 고유값 생성
var clientID = uniqueID();
console.log('Connection: '+ clientID);
//서버 receive 이벤트 함수(클라이언트에서 호출 할 이벤트)
client.on('serverReceiver', function(value){
//클라이언트 이베트 호출
socket.sockets.emit('clientReceiver', {clientID: clientID, message: value});
});
});
다음으로 chatClient.js 소스이다.
//클라이언트 소켓 생성
var socket = io.connect('ws://127.0.0.1:3000');
//DOM 참조
var div = document.getElementById('message');
var txt = document.getElementById('txtChat');
//텍스트 박스에 포커스 주기
txt.focus();
//텍스트 박스에 이벤트 바인딩
txt.onkeydown = sendMessage.bind(this);
function sendMessage(event){
if(event.keyCode == 13){
//메세지 입력 여부 체크
var message = event.target.value;
if(message){
//소켓서버 함수 호출
socket.emit('serverReceiver', message);
//텍스트박스 초기화
txt.value = '';
}
}
};
//클라이언트 receive 이벤트 함수(서버에서 호출할 이벤트)
socket.on('clientReceiver', function(data){
//console.log('서버에서 전송:', data);
//채팅창에 메세지 출력하기
var message = '['+ data.clientID + '님의 말' + '] ' + data.message;
div.innerText += message + '\r\n';
//채팅창 스크롤바 내리기
div.scrollTop = div.scrollHeight;
});
};
마지막으로 chatClient.html 파일이다.
<script src="/socket.io/socket.io.js"></script>
<div id="message" style="background-color:#F5F5F5; width:400px; height:200px; OVERFLOW-Y:auto; word-wrap: break-word"></div>
<div id="chatInput">
메시지를 입력하세요 <input type='text' id='txtChat' name="txtChat">
</div>
* 실행흐름
1) 서버 실행
- Node의 쉘기능을 이용해 다음과 같이 chatServer.js를 실행한다. 실행하면 다음 그림과 같이 console 로그가 프롬프트 창에 찍힌다.
2) 클라이언트 실행
- 클라이언트에서는 단순히 브라우저로 지정된 url로 접근하기만 하면 된다.
브라우저로 chatServer.js 서버로 접속하면, 루트 get요청(http://127.0.0.1:3000)에 따른 chatClient.html 파일이 클라이언트로 다운로드 되고, 이 HTML파일이 브라우저에서 실행되면서 chatClient.js 파일에 대한 요청이 다시 이뤄져 자바스크립트 코드가 다운로드 되고 실행된다. 이 클라이언트 자바스크립트가 실행되어 소켓서버와 클라이언트는 서로 연결이 이뤄진다.
다음 그림은 브라우저에서 해당 url로 접근했을 때 서버측 로그화면이다.
* 결과화면
- 총 4개의 사용자가 접근하는 시나리오로 가정하여, 4개의 브라우저를 동시에 사용한다. 각 브라우저에서 해당 url로 접근하고 각자 채팅을 해 본다. 이때 각 클라이언트 구분은 서버측 코드에서처럼 임의의 순처적 고유값을 부여한 숫자로 사용한다.
다음 화면은 브라우저 4개를 띄우고, 채팅을 해본 모습니다. 혼자놀기에 좋다 ㅡ,ㅡ;
지금까지 알아본 샘플 채팅은 채팅 구현을 위한 가장 기본적이면서도 필수적으로 구현되어야 하는 것들만 다뤘다. 실제 업무에서 채팅을 구현한다면 성능과 안정성 등을 위해 다양한 방어코드, 추가기능 코드, 최적화 기법 등의 추가 작업이 필요할 것이다.
기본적인 흐름과 코드를 알았으니, 살을 붙여 나가면 될 일이다.
'모바일 > Javascript' 카테고리의 다른 글
[Node.js] CORS 설정(Cross Domain 요청 허용) (10) | 2016.07.15 |
---|---|
[AngularJS] REST API 통신(with Node.js) (10) | 2016.07.15 |
[AngularJS] Route (12) | 2016.07.14 |
[AngularJS] Directive (12) | 2016.07.13 |
[AngularJS] $scope과 controller-as 문법 (10) | 2016.07.12 |
앞서 전개한 글들에서 충돌처리, 총알발사, 오브젝트 이동 처리 등을 구현해 보았다.
이번 글에서는, 이러한 개별 개념을 조합하여 총알로 적(enemy) 비행체를 격추하는 샘플을 구현해 보자.
간략한 시나리오
- 적(enemy) 비행체는 캔버스 상단에서 좌/우로 방향을 바꿔가며 계속 이동한다.
- 키보드의 스페이스 키를 누르면 총알이 발사된다.
- 총알이 적 비행체에 명중하면 폭발한 듯한 이미지 효과를 준다.
격추라는 시나리오에만 충실하기 위해 아군 비행기는 등장시키지 않았으며, 총알의 출발 위치도 캔버스 하단 중앙에서만 출발하도록 한다. 또한 지속적인 테스트를 위하여 적 비행체가 격추되어도 충돌을 표현하는 이미지 효과만 줄 뿐 비행체는 계속 살아서 움직이도록 할 것이다.
먼저 적 비행체와 총알, 그리고 충돌 처리를 위한 객체 기반을 작성할 것인데 그전에, 주요 코드부터 간략히 살펴보자.
총알 발사
이전 글에서는, 과도한 연사 속도를 조절하기 위해 FPS를 기반의 조절값을 사용한 반면 여기서는 총알 발사의 전/후 시간을 속도 조절의 기준 값으로 사용했다. 즉 직전 총알 발사 시간과 현재 총알 발사 시간의 간격이 0.1초 이상 되어야만 새 총알이 배열에 추가되도록 처리한다. 그리고 총알의 발사 위치는 캔버스의 하단 중앙으로 설정한다.
this.bullets.push({x: (this.canvasSize.width / 2)
- (this.gameAssets.bullet.width / 2), y: this.canvasSize.height});
this.lastShootTime = Date.now();
}
오브젝트 이동
적 비행체와 총알의 이동을 위해 좌표값을 변경하는 코드이다. 게임 루프의 update() 메서드에서 매 프레임마다 호출하는 부분으로, 총발 위치는 위쪽 방향으로 이동하도록 Y 좌표값을 변경하고 적 비행체는 캔버스의 좌/우우를 계속해서 왔다갔다 하도록 방향값을 기준으로 X 좌표값을 변경시켜준다.
for(var i = 0; i < this.bullets.length; i++){
this.bullets[i].y -= this.speedBullet;
}
if(this.directionEnemy == 1
&& this.enemyPostion.x + this.gameAssets.enemy.width > this.canvasClientRect.right)
{
this.directionEnemy = -1;
}
if(this.directionEnemy == -1
&& this.enemyPostion.x + 5 < this.canvasClientRect.left)
{
this.directionEnemy = 1;
}
this.enemyPostion.x += (this.directionEnemy) * 5;
오브젝트 그리기 및 충돌처리
객체의 마지막 코드로, 게임 오브젝트들을 캔버스에 그린다. 게임 루프의 display() 메서드에서 매 프레임마다 호출하는 부분으로, 먼저 캔버스의 모든 내용을 지우고 적 비행체를 그린다. 다음으로 총알을 배열 수 만큼 그려주는데 총알이 캔버스 영역 밖으로 이동했다면 배열에서 제거하고 루프를 빠져나간다. 그리고 총알과 적 비행체와의 충돌처리를 통하여 격추되는 이미지를 그려줄지 판단한다.
this.canvasContext.drawImage(this.gameAssets.enemy, this.enemyPostion.x, this.enemyPostion.y);
for(var i = 0; i < this.bullets.length; i++){
if(this.bullets[i].y <= 0) {
this.bullets.splice(i,1); continue;
}
this.canvasContext.drawImage(this.gameAssets.bullet, this.bullets[i].x, this.bullets[i].y);
if(this.bullets[i].x + this.gameAssets.bullet.width > this.enemyPostion.x
&& this.bullets[i].x < this.enemyPostion.x + this.gameAssets.enemy.width
&& this.bullets[i].y > this.enemyPostion.y
&& this.bullets[i].y < this.enemyPostion.y + this.gameAssets.enemy.height)
{
this.canvasContext.drawImage(this.gameAssets.pow, this.enemyPostion.x, this.enemyPostion.y);
}
}
여기까지 해서 객체기반 작성이 완료되었다. 나머지 코드는 각종 초기화 및 이미지 리소스 다운로드, 위의 객체를 기반으로 게임 루프를 구현하는 것이다. 다음 코드는 객체를 포함한 전체 소스이다.
function ShootingDownObj(gameAssets,canvasElement){
this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
this.canvasContext = canvasElement.getContext('2d');
this.canvasClientRect = canvasElement.getBoundingClientRect();
this.enemyPostion = {x: 10, y:10};
this.gameAssets = gameAssets;
this.bullets = [];
this.speedBullet = 15;
this.directionEnemy = 1;
this.lastShootTime = Date.now();
this.addBullet = function(){
if(Date.now() - this.lastShootTime >= 100){
this.bullets.push({x: (this.canvasSize.width / 2)
- (this.gameAssets.bullet.width / 2), y: this.canvasSize.height});
this.lastShootTime = Date.now();
}
}
this.movePosition = function(){
for(var i = 0; i < this.bullets.length; i++){
this.bullets[i].y -= this.speedBullet;
}
if(this.directionEnemy == 1
&& this.enemyPostion.x + this.gameAssets.enemy.width > this.canvasClientRect.right){
this.directionEnemy = -1;
}
if(this.directionEnemy == -1
&& this.enemyPostion.x + 5 < this.canvasClientRect.left){
this.directionEnemy = 1;
}
this.enemyPostion.x += (this.directionEnemy) * 5;
}
this.shotBullets = function(){
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
this.canvasContext.drawImage(this.gameAssets.enemy,
this.enemyPostion.x, this.enemyPostion.y);
for(var i = 0; i < this.bullets.length; i++){
if(this.bullets[i].y <= 0) {
this.bullets.splice(i,1);
continue;
}
if(this.bullets[i].x + this.gameAssets.bullet.width > this.enemyPostion.x
&& this.bullets[i].x < this.enemyPostion.x + this.gameAssets.enemy.width
&& this.bullets[i].y > this.enemyPostion.y
&& this.bullets[i].y < this.enemyPostion.y + this.gameAssets.enemy.height){
this.canvasContext.drawImage(this.gameAssets.pow,
this.enemyPostion.x, this.enemyPostion.y);
}
this.canvasContext.drawImage(this.gameAssets.bullet, this.bullets[i].x, this.bullets[i].y);
}
}
}
var fps = 30;
var canvasElement;
var gameContext;
var shootingDownObj;
var gameAssets;
var currentAssetImageLoadCount = 0;
var isKeyDown = [];
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
var bulletImage = new Image();
bulletImage.src = 'image/bullet.png';
bulletImage.onload = onAssetImageLoadComplete;
var enemyImage = new Image();
enemyImage.src = 'image/enemy.png';
enemyImage.onload = onAssetImageLoadComplete;
var powImage = new Image();
powImage.src = 'image/pow.png';
powImage.onload = onAssetImageLoadComplete;
gameAssets = {bullet: bulletImage, enemy: enemyImage, pow: powImage};
}
function onAssetImageLoadComplete(){
if(++currentAssetImageLoadCount >= 3){
shootingDownObj = new ShootingDownObj(gameAssets,canvasElement);
setInterval(gameLoop, 1000 / fps);
}
}
function gameLoop(){
update();
display();
}
function update(){
if(isKeyDown[32]){
shootingDownObj.addBullet();
}
shootingDownObj.movePosition();
}
function display(){
shootingDownObj.shotBullets();
}
function onKeyDown(e){
isKeyDown[e.keyCode] = true;
}
function onKeyUp(e){
isKeyDown[e.keyCode] = false;
}
window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);
브라우저를 통해 확인해보면, 다음 그림과 같이 총알이 명중하면 폭발한듯한 이미지 효과를 확인할 수 있다.
|
|
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Sound Effect (16) | 2013.10.11 |
---|---|
[HTML5 Game] Firing Bullet (8) | 2013.10.08 |
[HTML5 Game] Moving Object Ⅱ (9) | 2013.10.02 |
[HTML5 Game] Moving Object (6) | 2013.10.02 |
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
이번 글에서는 게임에 사운드(Sound) 효과를 추가해 볼 것이다.
이전 글(http://m.mkexdev.net/252)에서 스페이스 키를 누르면 총알이 발사되도록 해 봤으니, 여기서는 총알이 발사되는 소리를 구현해 보도록 하겠다.
사운드 처리는 HTML5 스펙에 새로이 추가된 Audio 요소를 이용할 것이다.(참고: http://m.mkexdev.net/63)
단순히 하나의 사운드를 재생하는 것은 매우 심플한 코드로 구현이 가능하다. 하지만 게임 사운드는 한 번에 하나의 사운드만 재생되는 것이 아니라 여러 사운드가 동시에 재생되어야 하는 경우가 많고 또한 하나의 사운드라 해도 그 사운드가 종료되기 전에 같은 사운드가 다시 재생되어야 하는 경우도 있다. 이러한 대표적인 경우가 총알이 발사되는 경우이다. 예를들어 총알 사운드의 플레이 타임이 3초인 경우, 첫번째 총알이 발새되고 3초가 지나기 전에 두 번째 총알이 발사되는 경우이다. 이를 위해서는 하나의 사운드를 여러개로 분리해서 각각 재생되도록 하는 기법이 필요하다.
바로 구현해 들어가 보자. 다음과 같이 총알 사운드를 추상화환 객체 기반을 준비한다.
여러 개의 총알 소리가 거의 동시에 같이 재생될 수 있도록 사운드 배열을 관리하고 플레이 가능한 사운드를 찾으면 그 사운드를 재생하고 바로 루프를 빠져 나간다. 또한 특정 사운드의 재생이 완료되면 명시적으로 paused(일시정지)처리를 하는데 이 함수에서는 이 속성값을 검사하여 재생가능한 사운드 파일을 탐색한다.
this.sounds = sounds
this.playSound = function(){
for(var i = 0; i < this.sounds.length; i++){
if(this.sounds[i].audio.paused){
this.sounds[i].audio.play();
break;
}
}
}
}
이어지는 코드는 FireShort 객체를 이용하여 스페이스 키가 눌러졌을 때 사운드 재생을 처리하는 코드이다.
총알 소리로 사용되는 사운드 파일은 한개이지만, (앞서 설명한대로) 동시 재생을 위해 여러개의 Audio 요소를 동적으로 생성한다. 코드에서는 50개를 생성했는데 이것은 사운드 파일의 재생시간과 동시 재생 환경등을 고려하여 적절한 값을 지정해야 한다. 필자가 사용하는 (인터넷에서 다운받는) 사운드 파일의 재생시간은 대략 4초 가량 되며(너무 길다. 짧막한 총성소리에 비해 너무 긴 재생 시간이다. ㅡ,ㅡ;) 스페이스 키를 계속 누르고 있을 경우 소리가 끊임없이 나도록 해야 하기 때문에 50이라는 좀 과한(?) 값을 지정했다. (이렇게 해도 플레이타임이 길어서 매끄럽지 못하다.) 예제에서는 사운드 재생만이 목적이기 때문에 이러한 과도한 값을 지정했지만 실제 게임 개발시에는 자원의 낭비 및 성능의 저해가 없는 수준에서 적절한 선택을 해야 할 것이다.
참고로 크롬 브라우저의 경우 한번 재생한 Audio를 재사용 하기 위해서는 다시 로드해야 하는것을 주의하기 바라며 기타 코드는 HTML5의 Audio 요소를 사용하는 코드와 FireShot 객체를 호출하는 부분으로 이뤄져있다.
var fireShot;
var currentSoundLoadCount = 0;
function init(){
for(var i = 0; i < 50; i++){
var gunAudio = new Audio();
gunAudio.src = 'audio/gun.mp3';
gunAudio.addEventListener("canplaythrough", onSoundReadyComplete, false);
gunAudio.addEventListener("ended", function(){
if(window.chrome) this.load();
this.pause();
},false);
document.body.appendChild(gunAudio);
sounds.push({audio: gunAudio});
}
}
function onSoundReadyComplete(){
if(++currentSoundLoadCount >= 50){
fireShot = new FireShot(sounds);
}
}
function onKeyDown(e){
if(e.keyCode = 32){
fireShot.playSound();
}
}
window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
브라우저로 실행하여 사운드 효과를 감상(?)해 보자.
다시한번 말하지만, 사운드 파일의 재생시간과 동시 재생의 환경에 따라 적절히 수정을 가하며 테스트 해 보는 것이 좋다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Shooting Down (6) | 2013.10.16 |
---|---|
[HTML5 Game] Firing Bullet (8) | 2013.10.08 |
[HTML5 Game] Moving Object Ⅱ (9) | 2013.10.02 |
[HTML5 Game] Moving Object (6) | 2013.10.02 |
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
이전 글(http://m.mkexdev.net/247)에서 게임 루프를 기반으로 게임 오브젝트 이동에 대해 알아 보았다.
키보드의 방향키에 반응해 전투기가 이동하는 샘플을 작성해 봤는데 여기서 덧붙여 전투기에서 총알이 발사되도록 해 볼 것이다. 키보드의 스페이스 바를 누르면 총알이 발사되며 키를 계속 누르고 있으면 총알 다발이 연속해서 발사되는 시나리오이다.
총알 이미지
총알으로 사용할 이미지는 총알을 제외한 배경이 투명한 형태로 만드는 것이 좋다. 그러나 투명 이지미를 수급하기 쉽지 않아, 여기서는 캔버스 배경과 동일한 흰색 배경의 총알 이미지를 사용할 것이다. (ppt로 간단히 만들고 캡쳐해서 이미지로 변환하자 ㅎㅎ)
데모 실행 화면
실제 구현에 앞서 데모 실행화면을 먼저 확인해 보자. 이전 글의 예제와 같이 전투기는 방향키에 반응해 이동을 하며, 여기서 더해 스페이스 키를 누르면 총알이 위쪽 방향으로 발사된다.
구현 내용 정리
구현해야 할 내용을 큰 맥락에서만 정리해 보자.
- 스페이스 키를 누르면 총알 이미지가 비행기 중앙에서 출발하도록 한다
- 총알은 연속적으로 발사되기 때문에 배열로 관리한다.
- 발사된 총알이 위쪽 방향으로 계속 이동하게끔 하기 위해 Y 좌표 값을 업데이트 한다
대략 이 정도만 정리하고 자세한 건 구현하면서 살펴 보도록 하자.
이전 글에서는 전투기 객체를 Character로 명명했었는데, 맘에 들지 않아 Fighter로 변경하고 메서드 이름도 drawFighter 로 변경하였다. 그리고 총알을 그리기 위해 추가된 코드는 파란색으로 표시했다.
총알은 하나 이상 발사될 수 있기 때문에 배열로 관리하며 발사된 총알 수 만큼 배열 요소가 생성된다. drawFighter 메서드에서는 전투기를 그리기 전에 배열에 담긴 총알을 그려준다. 이때 캔버스의 맨 위쪽까지 도달했을 경우(Y 좌표가 '0'일 경우) 배열에서 제거하고 루프를 건너뛴다. 이렇게 하지 않으면 캔버스를 벗어난 총알도 배열에 계속 잔존하게 되고 이 배열은 게임 실행 중 계속 커져 버려서 성능에 좋지 못하다. 따라서 반드시 총알 제거를 해 주어야 한다.(참고로 캔버스의 위쪽 끝 Y 좌표가 오프셋 값 등으로 인해 0보다 클 수 있어나 큰 의미 없으므로 그냥 0 값을 기준으로 한다)
this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
this.canvasContext = canvasElement.getContext('2d');
this.assets = assets;
this.position = {x: x, y: y}
this.bullets = [];
this.drawFighter = function(){
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
//draw Bullet
for(var i = 0; i < this.bullets.length; i++){
if(this.bullets[i].y <= 0) {
this.bullets.splice(i,1);
continue;
}
this.canvasContext.drawImage(this.assets.bulletAsset, this.bullets[i].x, this.bullets[i].y);
}
//draw Fighter
this.canvasContext.drawImage(this.assets.fighterAsset, this.position.x, this.position.y);
}
}
이어지는 코드 역시 이전 글의 샘플 코드에서 파란색 부분의 주요 변경이 이뤄졌다. 파란색으료 표시하지 않는 부분도 일부 변경되 되었는데 이것은 일부 변수명 변경, 이미지를 두 개(전투기, 총알) 다운 받기 위한 코드, 게임 루프 메서드 분리 등 총알 발사와 관련된 로직과 크게 관련되어 있지 않아 별도 표시를 하지 않았다.
변경된 부분을 대략 설명하면, 전투기와 총알 이미지 객체를 담기 위한 assets 객체와 스페이스 바를 누를때 전투기 객체의 총알 배열에 (총알이 발사될 최초 x,y 지점을 지정하여) 하나의 총알 요소를 추가하고 있다. 그리고 매 업데이트마다 Y 값을 점점 줄여나가면서 위쪽 방향으로 이동시킨다.
var canvasElement;
var gameContext;
var fighter;
var assets;
var currentAssetLoadCount = 0;
var speedFighter = 10;
var speedBullet = 15;
var isKeyDown = [];
var bulletTime = 0;
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
var fighterImage = new Image();
fighterImage.src = 'image/fighter.png';
fighterImage.onload = onAssetLoadComplete;
var bulletImage = new Image();
bulletImage.src = 'image/bullet.png';
bulletImage.onload = onAssetLoadComplete;
assets = {fighterAsset: fighterImage, bulletAsset: bulletImage};
}
function onAssetLoadComplete(){
if(++currentAssetLoadCount >= 2){
fighter = new Fighter(assets, 200, 200, canvasElement);
fighter.drawFighter();
setInterval(gameLoop, 1000 / fps);
}
}
function gameLoop(){
update();
display();
}
function update(){
bulletTime += 1000 / fps;
if(bulletTime >= 100){
if(isKeyDown[32]){ //space
fighter.bullets.push({x: fighter.position.x + 6, y: fighter.position.y});
bulletTime = 0;
}
}
if(isKeyDown[37]){
fighter.position.x -= speedFighter;
}
if(isKeyDown[38]){
fighter.position.y -= speedFighter;
}
if(isKeyDown[39]){
fighter.position.x += speedFighter;
}
if(isKeyDown[40]){
fighter.position.y += speedFighter;
}
for(var i = 0; i < fighter.bullets.length; i++){
fighter.bullets[i].y -= speedBullet;
}
}
function display(){
fighter.drawFighter();
}
function onKeyDown(e){
isKeyDown[e.keyCode] = true;
}
function onKeyUp(e){
isKeyDown[e.keyCode] = false;
}
window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);
추가로 위의 코드에서 다음의 부분을 주의깊게 보자. 이 코드가 필요한 이유는 30프레임이라는 빠른 속도는 스페이스 키를 한번만 눌러도 두 세번 누른 것처럼 되기도 하고, 스페이스 키를 계속 누르고 있을 경우 그 속도가 너무 빨라 총알이 겹쳐서 발사되어 시각적으로 완전하기 못하게 된다. 따라서 총알이 한번 발사된 후 0.1초 정도 지나야만 다음 총알이 발사될 수 있도록 약간의 딜레이(delay) 시간을 주는 것이 좋다.
bulletTime += 1000 / fps;
if(bulletTime >= 100){
if(isKeyDown[32]){ //space
fighter.bullets.push({x: fighter.position.x + 6, y: fighter.position.y});
bulletTime = 0;
}
}
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Shooting Down (6) | 2013.10.16 |
---|---|
[HTML5 Game] Sound Effect (16) | 2013.10.11 |
[HTML5 Game] Moving Object Ⅱ (9) | 2013.10.02 |
[HTML5 Game] Moving Object (6) | 2013.10.02 |
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
이전 글(http://m.mkexdev.net/246)에서 키보드에 반응해 게임 오브젝트가 이동하는 예를 살펴 보았다.
당시 제기했던 문제점을 해결해서 완성도를 높여 보자.
이전 글에서 제기한 문제점을 다시 가져와 본다.
먼저 제일 문제가 되는 것은 '동시 키 입력'이 되지 않는다는 것이다.
예를 들어 대각선 방향으로 이동하기 위해 '→, ↑' 두 키를 동시 누르고 있더라도 뒤에 눌러진 키에만 반응하여 한쪽 방향으로만 이동하게 된다. 이것은 PC에서 Sift, Alt 키와 같은 보조키를 제외하고는 동시키 입력을 지원하지 않기 때문에 발생하는 현상이다. 따라서 이에 대한 처리를 별도로 해 줘야 한다.
두 번째 문제는, 전투기 이동의 시작이 매끄럽지 못하다는 것이다. 전투기를 끊김 없이 이동 시키기위해 키를 떼지 않고 계속 누르고 있을 경우 처음 이동이 끊기는 현상을 볼 수 있다. 이것은 키 입력이 지속될 때 발생하는 최초 지연 현상 때문인데 실제로 키 입력의 초당 실행수를 측정해 보면 최초 시작시점에 1~5사의 값이 나오고 이후부터는 최대 30정도로 유지되는 것을 확인할 수 있었다.
두 가지 문제의 해결책은 바로 게임 루프이다.
키보드의 키 입력 이벤트가 발생할 때 UI를 갱신하는 것이 아니라 게임 루프 상에서 빠르고도 지속적으로 UI를 갱신하도록 하며 키 이벤트는 키 입력 상태 값을 업데이트 해 주는 용도로만 그치게 하는 것이 핵심이다.
이렇게 하면 동시 키 입력이나 초기 지연 현상을 극복할 수 있게 되는 것이다.
Character 객체는 이전 코드와 동일하다.
this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
this.canvasContext = canvasElement.getContext('2d');
this.asset = asset;
this.position = {x: x, y: y}
this.drawCharacter = function(){
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
this.canvasContext.drawImage(asset, this.position.x, this.position.y);
}
}
이어지는 코드는 이전 코드이 문제점을 개선한 것인데, 먼저 UI를 갱신시키는 부분을 키 이벤트가 아닌 게임 루프에서 처리하는 것을 주의깊게 보자. 키 이벤트에서는 isKeyDown이라는 배열에 키 입력 여부를 삽입하고 게임루프에서 이 값을 기반으로 오브젝트 이동을 처리한다. 키 입력을 해제하기 위해 keyup 이벤트도 사용하고 있다.
var fps = 30;
var canvasElement;
var gameContext;
var character;
var asset;
var speed = 10;
var isKeyDown = [];
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
asset = new Image();
asset.src = 'image/fighter.png';
asset.onload = onAssetLoadComplete;
}
function onAssetLoadComplete(){
character = new Character(asset, 200, 200, canvasElement);
character.drawCharacter();
setInterval(gameLoop, 1000/fps);
}
function gameLoop(){
if(isKeyDown[37]){
character.position.x -= speed;
}
if(isKeyDown[38]){
character.position.y -= speed;
}
if(isKeyDown[39]){
character.position.x += speed;
}
if(isKeyDown[40]){
character.position.y += speed;
}
character.drawCharacter();
}
function onKeyDown(e){
isKeyDown[e.keyCode] = true;
}
function onKeyUp(e){
isKeyDown[e.keyCode] = false;
}
window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);
브라우저로 결과를 확인해 보면 이전 샘플에서 발생했던 동시키 입력 문제나 초반 지연 현상이 개선된 것을 확인할 수 있다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Sound Effect (16) | 2013.10.11 |
---|---|
[HTML5 Game] Firing Bullet (8) | 2013.10.08 |
[HTML5 Game] Moving Object (6) | 2013.10.02 |
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
[HTML5 Game] Game State (9) | 2013.09.27 |
이번 글에서는 게임 오브젝트를 움직여 이동시키는 방법에 대해 알아보자.
키보드의 방향키에 반응하여 상,하,좌,우로 움직여 볼텐데 뻔한 개념이니 바로 코드를 살표보자.
먼저 다음과 같이 Character 객체 기반을 작성한다. 지금까지 작성해오던 코딩 패턴과 유사하기에 별도의 설명은 생략하며, 위치 정보를 위한 position 객체 변수와 이 값을 기준으로 이미지를 그리고 있는 것을 확인하자.
this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
this.canvasContext = canvasElement.getContext('2d');
this.asset = asset;
this.position = {x: x, y: y};
this.drawCharacter = function(){
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
this.canvasContext.drawImage(asset, this.position.x, this.position.y);
}
}
이어지는 코드에서는 키보드의 방향키에 반응하여 앞서 생성한 Character 객체의 위치 값(position)을 변경하면서 그리기 작업을 호출하고 있다. speed라는 전역변수는 이동 속도 즉 간견을 위한 값이다. 여기서 사용한 캐릭터는 전투기 이미지인데 구글에서 다운 받아서 무단으로 사용한 것이다 ㅡ,ㅡ;
var canvasElement;
var gameContext;
var character;
var asset;
var speed = 10;
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
asset = new Image();
asset.src = 'image/fighter.png';
asset.onload = onAssetLoadComplete;
}
function onAssetLoadComplete(){
var frameCounter = new FrameCounter();
character = new Character(asset, 200, 200, canvasElement,frameCounter);
character.drawCharacter();
}
function onKeyDown(e){
if(e.keyCode == 37){ //left
character.position.x -= speed;
}
if(e.keyCode == 38){ //up
character.position.y -= speed;
}
if(e.keyCode == 39){ //right
character.position.x += speed;
}
if(e.keyCode == 40){ //down
character.position.y += speed;
}
character.drawCharacter();
}
window.addEventListener("load", init, false);
window.addEventListener("keydown", onKeyDown, false);
코드는, 전형적인 자바스크립트 키 이벤트 처리 로직이며 브라우저로 실행해 보면 전투기가 방향키대로 움직이는 것을 확인할 수 있다.
문제점
브라우저로 결과를 확인해 봤다면 뭔가 완전하지 않다는 것을 느꼈을 것이다. 전투기가 방향키에 반응해 원하는 곳으로 이동 하는 것은 분명하지만 완성도 있는 게임으로써는 뭔가 부족하다.
먼저 제일 문제가 되는 것은 '동시 키 입력'이 되지 않는다는 것이다.
예를 들어 대각선 방향으로 이동하기 위해 '→, ↑' 두 키를 동시 누르고 있더라도 뒤에 눌러진 키에만 반응하여 한쪽 방향으로만 이동하게 된다. 이것은 PC에서 Sift, Alt 키와 같은 보조키를 제외하고는 동시키 입력을 지원하지 않기 때문에 발생하는 현상이다. 따라서 이에 대한 처리를 별도로 해 줘야 한다.
두 번째 문제는, 전투기 이동의 시작이 매끄럽지 못하다는 것이다. 전투기를 끊김 없이 이동 시키기위해 키를 떼지 않고 계속 누르고 있을 경우 처음 이동이 끊기는 현상을 볼 수 있다. 이것은 키 입력이 지속될 때 발생하는 최초 지연 현상 때문인데 실제로 키 입력의 초당 실행수를 측정해 보면 최초 시작시점에 1~5사의 값이 나오고 이후부터는 최대 30정도로 유지되는 것을 확인할 수 있었다.
결국 이 두가지 문제를 해결해야 정상적인 이동 처리가 가능하며 다음 글에서 해결된 내용을 살펴볼 것이다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Firing Bullet (8) | 2013.10.08 |
---|---|
[HTML5 Game] Moving Object Ⅱ (9) | 2013.10.02 |
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
[HTML5 Game] Game State (9) | 2013.09.27 |
[HTML5 Game] Game Loop (9) | 2013.09.25 |
게임 루프(http://m.mkexdev.net/242)를 다루는 글에서 초당 프레임 수인 FPS의 개념과 샘플을 작성해 보았다. 이번 글에서는 실제 초당 얼마의 프레임률을 보이는지 파악할 수 있는 초당 프레임 수 계산기를 작성해 보자.
초당 프레임 수는 게임 실행환경이나 가변적인 연산처리 등으로 일정하지 않거나, 목표한 프레임 률(framerate) 보장하지 못하는 등의 문제가 발생하기 때문에 게임 개발과 디버깅 시 관심있게 살펴봐야 하는 데이터이다.
특히 가변 프레임 방식이라면 초당 몇 프레임이 나오는지에 대한 실제적 자료가 필요하며 고정 프레임 방식일지라도 항상 고정값을 보장하지 않는 환경이 될 수도 있으니 실제 프레임 수를 살펴보는 것이 여전히 도움이 된다.
또한 만들고자 하는 게임이, 초당 몇 프레임에서 가장 원활한 동작을 보이는 지를 확인할 때도 유용하다 하겠다.
아래 사이트는 tree.js의 3D 샘플인데, 이 샘플에서도 좌측 상단에 초당 프레임 수를 보여주고 있다.
http://threejs.org/examples/ (이 사이트의 예제는 날 감탄스럽게 만들어 버렸다)
초당 프레임 수 계산 방식
초당 프레임수를 계산하기 위해서는 크게 두 가지 접근방식을 취할 수 있다.
먼저 밀리세컨드 단위의 이전 프레임의 실행 시간과 현재 프레임의 실행 시간을 비교해서 그 간격을 보고 프레임 수를 역으로 계산하는 방식과 실제 프레임 실행시마다 카운팅하는 방식이 그것이다. 두 방식 모두 큰 의미에서는 동일하다 할 수 있으나, 두 번째 방식이 좀 더 직관적이고 명료한 듯 하여 이 글에서도 두번째 방식으로 샘플을 작성해 볼 것이다.
예제가 복잡하지 않으니 바로 작성해 보자. 더 이상 HTML 파일은 언급하지 않겠다.(너무 심플하뉘...)
다음과 같이 초당 프레임 수를 계산하기 위한 객체 기반을 작성한다. 코드의 핵심은 현재 시간과 직전 시간을 (밀리세컨드 단위로) 비교하여 1000 즉, 1초가 지났을 경우 계속 증가시켜오던 callCount 값을 framePerSecond에 대입하는 부분이다. 참고로 자바스크립트의 Date.now()는 1970년 1월 1일 자정으로부터 현재 시간 사이의 밀리초를 반환하는 함수인데 new Date().getTime()으로도 동일한 결과를 받을 수 있으나 검색한 자료에 의하면 성능상 Date.now()가 더 좋은 효율을 보인다고 한다.
this.callCount = 0;
this.framePerSecond = 0;
this.beforeTime = 0;
this.countFps = function(){
//(Date.now() is returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC)
var nowTime = Date.now(); //1970년 1월 1일 자정과 현재 날짜 및 시간 사이의 밀리초 값입니다
//If one second has passed
if(nowTime - this.beforeTime >= 1000){
this.framePerSecond = this.callCount;
this.beforeTime = nowTime;
this.callCount = 0;
}
//Increase frame count per second
this.callCount++;
}
}
이어지는 코드는 앞서 생성한 FrameCounter 인스턴스를 생성해서 게임루프마다 fps를 증가시키기 위해
countFps 함수를 호출하는 것이다. 초당 프레임 수는 좌측 상단에 표시하며 전체 누적 프레임 수는 중앙에 표시하도록 한다.
var canvasElement;
var gameContext;
var totalFrameCount = 0;
var frameCounter;
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
frameCounter = new FrameCounter();
setInterval(gameLoop, 1000/fps);
}
function gameLoop(){
update();
display();
}
function update(){
frameCounter.countFps(); //Call countFps function
++totalFrameCount; //Increase total frame count
}
function display(){
gameContext.clearRect(0, 0, canvasElement.width, canvasElement.height);
gameContext.textBaseline = 'top';
gameContext.font = '10pt Arial';
gameContext.fillText(frameCounter.framePerSecond + '/second' ,5, 5);
gameContext.font = '18pt Arial';
gameContext.fillText(totalFrameCount ,130, 80);
}
window.addEventListener('load', init, false);
브라우저로 실행하면 가운데 누적 프레임 수와 좌측 상단의 초당 프레임 수를 확인할 수 있다
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Moving Object Ⅱ (9) | 2013.10.02 |
---|---|
[HTML5 Game] Moving Object (6) | 2013.10.02 |
[HTML5 Game] Game State (9) | 2013.09.27 |
[HTML5 Game] Game Loop (9) | 2013.09.25 |
[HTML5 Game] Collision Detection (15) | 2013.09.23 |
게임은 상태의 집합이라 할 수 있다. 보통 우리가 게임을 할 때 대략 다음과 같은 수순의 단계를 만나게 된다.
게임 준비 -> 게임 실행 -> 게임 종료
물론 복잡한 게임에서는 더 다양한 단계가 있을 수 있지만, 개념을 이해하는 수준에서 위와 같이 간단히 세 가지 단계이 있고 한 번에 하나의 단계가 활성화되어 게임이 실행된다는 것을 이해하는게 중요하다.
이러한 게임 흐름의 각 단계를 '게임 상태'라 하며 HTML5 Game 개발에서도 이러한 상태의 전이를 통해 전반적인 게임 흐름을 관리하게 된다.
다음 그림은 게임 루프상에서 실행되는 게임 상태를 표현한 것이다. 게임 상태는 특정한 이벤트에 의해 서로 교체되며 한 번에 하나의 상태가 게임 루프 상에서 동작하여 게임이 진행된다.
게임 상태를 잘 분리해서 독립화시켜 적절히 모듈화한다면 개발과 유지보수에 좋은 영향을 미치게 된다. 따라서 게임 개발 시 각각의상태를 객체화 시켜서 관리하는 것이 권장된다.
그럼 이제 게임 상태와 상태의 변경을 통한 게임 흐름을 체험해 볼만한 간단한 샘플을 작성해 보자.
총 3가지 게임상태(ready, running, end)를 객체로 관리하고 키보드의 스페이스 바를 누르면 다음 상태로 이동하는 예인데, running 상태에서는 캔버스에 랜덤한 사격형을 그리는 이전 글(Game Loop, http://m.mkexdev.net/242)의 샘플을 그대로 사용할 것이다.
먼저 다음과 같이 HTML 파일을 준비한다.
<html lang="en">
<head>
<script type="text/javascript" src="js/gameState.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="500" height="400" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
</body>
</html>
다음으로 '준비/실행/종료'에 해당하는 각각의 게임상태를 객체로 관리하기 위한 3개의 생성자 함수를 정의하는데 공통 속성을 재사용하기 위해 프로토타입의 상속을 활용한다. 3가지 상태는 모두 게임 루프상에서 실행되기 위한 update(), display() 함수를 가지고 있으며 각 상태에 맞는 업데이트와 화면 갱신을 처리하도록 한다.
function GameState(canvasElement){
if(canvasElement != undefined){
this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
this.canvasContext = canvasElement.getContext('2d');
}
}
/* Define Ready State Class */
function Ready(canvasElement){
//Call Parent Constract Function
GameState.call(this,canvasElement);
}
Ready.prototype = new GameState(); //inherit
Ready.prototype.update = function(){
this.canvasContext.fillStyle = '#000000';
}
Ready.prototype.display = function(){
this.canvasContext.font = '18pt Arial';
this.canvasContext.textBaseline = 'top';
this.canvasContext.fillText("Ready..." ,200, 150);
}
/* Define Running State Class */
function Running(canvasElement){
//Call Parent Constract Function
GameState.call(this,canvasElement);
this.position = {};
}
Running.prototype = new GameState(); //inherit
Running.prototype.update = function(){
this.position.x = Math.floor(Math.random() * (canvasElement.width - 20));
this.position.y = Math.floor(Math.random() * (canvasElement.height - 20));
this.canvasContext.fillStyle = 'rgb(' + Math.floor(Math.random() * 255) + ','
+ Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';
}
Running.prototype.display = function(){
this.canvasContext.fillRect(this.position.x, this.position.y, 20, 20);
}
/* Define End Class */
function End(canvasElement){
//Call Parent Constract Function
GameState.call(this,canvasElement);
}
End.prototype = new GameState(); //inherit
End.prototype.update = function(){
this.canvasContext.fillStyle = '#000000';
}
End.prototype.display = function(){
this.canvasContext.font = '18pt Arial';
this.canvasContext.textBaseline = 'top';
this.canvasContext.fillText("End!!!" ,200, 150);
}
게임 상태가 준비되었으니, 각 상태가 실행되도록 다음과 같이 작성한다. 게임상태를 저장하기 위한 gameState 배열과 이 배열에서 특정 상태를 선택하기 위한 인덱스 값을 저장하는 currentGameStateIndex 변수를 선언한다. 키보드의 스페이스 바를 누르면 이 변수 값을 차례대로(0~2) 변경되도록 하여 게임 상태가 순환되도록 한다. 나머지 코드는 지금까지 학습했던 내용과 동일하므로 설명을 생략한다.
var canvasElement;
var gameContext;
var gameState = [];
var currentGameStateIndex = 0;
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
//Create Game State Instance & Push in gameState Array
gameState = [new Ready(canvasElement),new Running(canvasElement),new End(canvasElement)];
setInterval(gameLoop, 1000/fps);
}
function gameLoop(){
gameState[currentGameStateIndex].update();
gameState[currentGameStateIndex].display();
}
function ChangeGameState(e){
if(e.keyCode == 32){ //32: Space Key
gameContext.clearRect(0, 0, canvasElement.width, canvasElement.height);
//Change Game State Index(0 ~ 2)
currentGameStateIndex = (currentGameStateIndex + 1) % 3;
}
}
window.addEventListener('load', init, false);
window.addEventListener("keydown", ChangeGameState, false);
샘플을 브라우저로 확인해 보면 스페이스 키로 인해 다음의 3 단계가 순환되는 것을 확인할 수 있다.
|
|
|
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Moving Object (6) | 2013.10.02 |
---|---|
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
[HTML5 Game] Game Loop (9) | 2013.09.25 |
[HTML5 Game] Collision Detection (15) | 2013.09.23 |
[HTML5 Game] Parallxing Background Animation (11) | 2013.09.21 |
모든 마법은 게임루프(Game Loop)에서 일어난다!!!
처음 HTML5 Game 개발에 대한 학습을 시작할 때 가장 충격적(?)이었던 것이 바로 게임루프라는 개념이었다. 좀 과장된 표현이긴 하지만, 서버 측 어플리케이션 개발에 대부분의 시간을 보낸 나에게는 꽤나 신선한 느낌을 주었다. 물론 서버 측 어플리케이션에도 무한 반복 실행을 위한 루프 로직이 없는 것은 아니다. TCP 서버에서 입력 소켓을 지속적으로 리스닝 하는 것도 일종의 루프라 할 수 있고 주기적인 폴링을 위한 루프 로직도 심심찮게 구현해왔다. 하지만 초당 프레임 수에 기반한 매우 빠른 속도의 루프와 이벤트 처리, 상태 업데이트, 랜더링 등 거의 모든 로직이 스스로 살아 움직이는 게임 루프상에서 이뤄지는 개념은 적어도 나에게는 입가에 미소를 짓게 만들만큼 신선한 느낌이었다.
게임루프는 비단 HTML5 게임개발에만 적용되는 개념이 아니다. 어떤 프로그래밍 환경의 게임이라도 게임루프가 구현되며 큰 맥락에서는 동일한 개념과 로직이라고 할 수 있다.
혹자는 게임루프를 게임의 심장박동이라고 표현한다. 심장이 계속 뛰어야 하는 것처럼 게임이 살아있기 위해서는 게임루프가 지속적으로 실행되어야 하며 분당 심박 수처럼 게임 루프도 초당 프레임 수에 기반하므로 꽤 그럴싸한 비유라 하겠다.
업데이트와 디스플레이
게임 루프에서 모든 마법이 일어나지만 개념을 단순화 시키면 크게 두 가지 마법(?)이 일어난다고 할 수 있다.
바로 업데이트와 디스플레이다. 게임은 실행 중에 캐릭터가 이동하거나 레벨이 업데이트 되거나 하는 여러가지 상태 값의 업데이트가 필요하며 이렇게 업데이트 된 데이터를 기반으로 화면을 갱신해 줘야 한다. 이러한 게임 루프의 로직을 의사코드로 표현하면 다음과 같다.
update()
display()
}
FPS
FPS는 Frame Per Second의 약자로 초당 프레임 수를 말한다. 1초 동안 보여주는 화면의 수를 일컫는데 보통 영상을 상영할 때 필름의 프레임이 교체되는 속도 즉 프레임률(frame rate)과 동일한 개념이다. 보통 인간은 초당 15프레임 이상이면 깜빡임 현상을 거의 느끼지 못한다고 하며 25프레임 이상이면 비교적 자연스러운 영상으로 인식한다고 한다. 60프레임이상이면 잔상을 느끼지 못하게 된다.
프레임 수가 높을수록 더 부드러운 처리가 가능하지만 과도하게 높은 프레임 수는 게임 실행에 지장을 주게 되므로 성능과 하드웨어 환경 및 게임 상황을 고려해 적절한 프레임 수를 지정할 필요가 있다.
1) 고정 프레임 방식
초당 프레임 수를 일정하게 고정시키는 것을 고정 프레임 방식이라 한다. 이 방식은 한 프레임이 수행되는 시간이 아니라 초당 프레임 수행 횟수에 의존하는 방식이며 이 프레임 수행 횟수를 매 초 마다 고정시키는 방식이다.자료에 의하면 고정 프레임 방식은 하드웨어 환경을 미리 파악할 수 있어서, 하나의 프레임에 걸리는 시간을 미리 결정할 수 있는 콘솔 게임들에서 흔히 사용되어 왔다고 한다. HTML5 게임 개발에서 고정 프레임 방식을 적용할 경우 자바스크립트의 setInterval() 함수를 사용할 수 있다.
2) 가변 프레임 방식
초당 프레임 수가 일정하지 않고 유동적으로 변하는 것을 가변 프레임 방식이라 한다. 가변 프레임 환경에서 한 프레임의 수행 시간은 그 프레임에서 그려야 할 화면의 디스플레이 시간에 의존적이기 때문에 게임 상황에 따라 달라질 수 있다. 즉 초당 프레임 횟수가 아니라 프레임 수행 시간에 의존적인 방식이다. 따라서 하드웨어의 성능이나 게임 상황에 따라 초당 프레임 수가 달라질 수 있다는 것이다. 대부분의 PC게임이 이 방식으로 구현된다고 한다. HTML5 게임 개발에서 고정 프레임 방식을 적용할 경우 자바스크립트의 setTimeout() 함수를 사용할 수 있다.
고정 프레임 방식의 게임 루프를 코드로 표현하면 대략 다음과 같다.
setInterval(gameLoop, 1000/fps);
자바스크립트의 setInterval() 함수는, 두 번째 매개변수로 지정된 시간 간격으로 함수를 반복해서 실행한다. 이때 시간 단위는 1/1000초 즉, 밀리세컨드 단위로 지정하게 된다.
코드에서는 1000/fps = 1000/10은 1초에 10번 gameLoop() 함수를 실행하라는 의미가 된다. 다시말해 1000/10 = 100 즉, 100ms (0.1초) 마다 함수를 호출하도록 한다.
게임 루프 구현
게임 루프와 FPS의 개념을 알아봤으니 이 개념을 기반으로 간단한 게임 루프를 HTML5 기반으로 구현해보자.
여기서 구현해 볼 예제는 게임 루프 상에서 Canvas 영역안에 임의의 색상의 사각형을 랜덤하게 반복적으로 그려주는 예이다.
먼저 HTML 파일을 다음과 같이 작성한다.
<html lang="en">
<head>
<script type="text/javascript" src="js/gameLoop.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="500" height="400" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
</body>
</html>
그리고 다음과 같이 자바스크립트를 작성한다. 게임 루프가 어떤 식으로 구현되는지 자세히 살펴보기 바라며 사각형의 위치와 색상이 랜덤하게 지정되도록 했다.
var canvasElement;
var gameContext;
var position = {};
function init(){
canvasElement = document.getElementById('GameCanvas');
gameContext = canvasElement.getContext('2d');
setInterval(gameLoop, 1000/fps);
}
function gameLoop(){
update();
display();
}
function update(){
//Set Rectangle Position(Random Positioning In Canvas)
position.x = Math.floor(Math.random() * (canvasElement.width - 20)); //0~480
position.y = Math.floor(Math.random() * (canvasElement.height - 20)); //0~380
//Set Random Coloring
gameContext.fillStyle = 'rgb(' + Math.floor(Math.random() * 255) + ','
+ Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';
}
function display(){
gameContext.fillRect(position.x, position.y, 20, 20);
}
window.addEventListener('load', init, false);
이제 예제를 브라우저로 실행해보면 다음과 같은 화면을 볼 수 있을 것이다.
fps를 10으로 지정했기 때문에 1초에 10번 사각형이 그려질 것이며 이를 무한 반복하게 되는 것을 확인할 수 있다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game]Calculating FPS (8) | 2013.09.30 |
---|---|
[HTML5 Game] Game State (9) | 2013.09.27 |
[HTML5 Game] Collision Detection (15) | 2013.09.23 |
[HTML5 Game] Parallxing Background Animation (11) | 2013.09.21 |
[HTML5 Game] Background Animation (12) | 2013.09.20 |
거의 대부분의 게임은 충돌감지를 필요로 한다.
플레이어를 향해 달려오는 적(enemy), 적을 향해 쏘는 총알 등 게임 요소들간 충돌을 구현하기 위해서는 개별 요소들의 시각적 겹침을 처리해야 한다.
2D 게임에서의 충돌 감지는, 충돌 감지 대상이 되는 게임 요소의 형태에 따라 충돌박스, 충돌구, 충돌점 등 상황에 맞는 감지 로직을 구현해야 한다.
여기서는 가장 기본이 되는 충돌 박스에 대해 알아 보겠다.
* 간단한 개념
앞서 언급했듯이, 충돌 감지는 두 그래픽 요소의 시각적 겹침을 처리하면 된다. 겹침은 그래픽 요소의 X, Y 좌표와 요소의 크기(너비/높이) 값이 기준이 되는데 다음과 같이 크게 두 영역의 겹첨을 감지하면 된다.
1) X 좌표 겹침
player 와 enemy 라는 두 그래픽 요소가 있다고 가정하자.
player를 기준으로 보면, enemy가 player의 X좌표 영역 안에 있을 경우 충돌되었다고 할 수 있다.
아래 그림을 보면 이해가 쉬울 것이다.
먼저 enemy의 우측 끝 X좌표(enemy.x + enemy.width)가 player의 좌측 끝 X좌표(player.x)보다 크고,
enemy의 좌측 끝 X좌표(enemy.x)가 player의 우측 끝 X좌표(player.x + player.width)보다 작으면 서로 겹침 즉, 충돌되었다고 할 수 있다.
2) Y 좌표 겹침
같은 개념으로 Y 좌표 겹침을 감지할 수 있다.
먼저 enemy의 하단 끝 Y좌표(enemy.y + enemy.height)가 player의 상단 끝 Y좌표(player.y)보다 크고,
enemy의 상단 끝 Y좌표(enemy.y)가 player의 하단 끝 Y 좌표(player.y + player.height)보다 작으면 서로 겹침 즉, 충돌되었다고 할 수 있다.
결론적으로 두 요소의 X 좌표 겹침과 Y 좌표 겹침이 둘다 만족한다면 서로 충돌되었다고 감지하면 되는 것이다.이 개념을 코드화 하면 다음과 같다.
function IsCollision(enemy, player) {
return enemy.x < player.x + player.width &&
enemy.x + enemy.width > player.x &&
enemy.y < player.y + player.height &&
enemy.y + enemy.height > player.y;
}
* HTML
충돌 박스에 근거한 간단한 예제를 만들어 보겠다. 먼저 다음과 같이 HTML 파일을 구성한다. 캔버스와 마우스 좌표를 표시하기 위한 span 요소를 정의한다.
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript" src="js/collisionBox.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="400" height="300" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
<br />
<span id="mousePositionDisplay"></span>
</body>
</html>
* Javascript
먼저 Player 객체를 위한 Box 생성자 함수를 정의하는데, Player가 위치할 X,Y 좌표와 크기(너비/높이)를 매개변수로 전달받도록 한다.
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
다음으로 문서의 로딩 완료와 마우스 움직임을 처리하기 위한 이벤트 리스너를 등록하고 필요한 전역 변수를 정의한다. 예제의 단순함을 위해 enemy 요소는 따로 정의하지 않고 마우스 포인트를 사용할 것이다. 즉 Player 박스와 마우스 포인터의 겹침(충돌)을 감지하게 되므로 마우스 이벤트가 필요하다.
window.addEventListener("mousemove", onMousemove, false);
var canvasClientRect; //Canvas Client Bounding Rect
var gameContext; //Canvas Context
var box; //Player Box
그리고 초기화 함수와 Player 박스를 그리는 함수를 정의한다. 여기서 주의깊게 봐야 할 것은 Canvas의 getBoundingClientRect() 함수이다. Canvas가 문서에 표시될 때 div와 같은 부모요소에 포함될 수도 있고 문서 자체의 margin, padding 값에 의해 문서의 좌측 상단에서 떨어져 있을 수 있다.
다시말해, Canvas의 좌측 상단의 좌표가(0,0)이 아닐 수 있다는 것이다. Canvas에 포함된 게임 요소들의 충돌 감지를 위해서는 Canvas의 좌측 상단의 좌표가 (0,0)이어야 하므로 문서와 Canvas의 간격을 좌표계산에서 제거해야 한다. 이를 위해 문서 자체의 margin값을 0으로 설정하거나 Canvas의 offSet값을 계산에 포함할 수도 있지만 좀더 범용적으로 적용하기 위해서 getBoundingClientRect()함수를 사용하는 것이 좋다. 이 함수를 이용하면 Canvas가 문서로부터 얼마나 떨어져 있는지 알 수 있으므로 쉽게 계산할 수 있게 된다.
var canvasElement = document.getElementById("GameCanvas");
//Get Canvas Bounding ClientRect
canvasClientRect = canvasElement.getBoundingClientRect();
gameContext = canvasElement.getContext('2d');
//Draw Player Box
drawBox();
}
function drawBox(){
//Create Player Box Instance
box = new Box(150,100,100,100);
gameContext.fillStyle = '#000000';
//Draw Player Box
gameContext.fillRect(box.x,box.y,box.width,box.height);
}
이제 남은건 마우스 무브 이벤트와 충돌감지 로직이다. 이 코드가 핵심인데 다음과 같이 구현한다.
앞서 설명한대로 Player 요소와 마우스 포인터의 겹침을 감지하고 충돌 시 이를 표시해 준다.
function onMousemove(e){
//Clear Canvas
gameContext.clearRect(0, 0, canvasClientRect.width, canvasClientRect.height);
//Draw Player Box
drawBox();
//Calculate Mouse Position based on Canvas
mouseXinCanvas = e.clientX - canvasClientRect.left;
mouseYinCanvas = e.clientY- canvasClientRect.top;
//Collision Detection
if(IsCollision(box,mouseXinCanvas,mouseYinCanvas)){
gameContext.fillStyle = '#ffffff';
gameContext.font = '18pt Arial';
gameContext.textBaseline = 'top';
gameContext.fillText("collision!", box.x, box.y);
}
//Display Mouse Position
document.getElementById("mousePositionDisplay").innerText =
"x:" + mouseXinCanvas + ", y:" + mouseYinCanvas;
}
function IsCollision(box, x, y){
return x > box.x && x < box.x + box.width &&
y > box.y && y < box.y + box.height
}
* 실행 화면
예제를 브라우저로 실행해 보면 마우스 포인터가 Player 박스 안에 들어가면 충돌되었다고 표시하는 것을 확인할 수 있을 것이다. 물론 이 예제에서는 마우스 포인트가 enemy를 대신하기 때문에 enemy의 박스 계산(너비/높이 계산)이 필요없게 되지만 개념은 동일하다.
|
|
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Game State (9) | 2013.09.27 |
---|---|
[HTML5 Game] Game Loop (9) | 2013.09.25 |
[HTML5 Game] Parallxing Background Animation (11) | 2013.09.21 |
[HTML5 Game] Background Animation (12) | 2013.09.20 |
[HTML5 Game] Character Animation Using Trident.js (12) | 2013.09.17 |
지난 포스팅에서 백그라운드 애니메이션 처리 기법을 알아 보았다.
이 글에서는 하나의 배경이미지를 사용해서 애니메이션을 구현했는데,
이번 글에서는 더욱 현실감있게 하기 위해 몇 개의 배경을 중첩시켜서 서로 다른 속도로 애니메이션이 동작하도록 구현해 볼 것이다.
여러개의 배경이 서로 중첩되어 각기 다른 속도로 움직이도록 처리하면 게임을 더욱 실감나게 만들 수 있는데, 이를 시차 스크롤 방식이라 한다. 이에 대한 깔끔한 설명은 'LEARNING HTML5 온라인 게임 개발 프로그래밍'의 설명을 인용한다.
...
자동차를 타고 다리를 건너가면서 차창 밖으로 지나가는 풍경을 바라보는 것과 같다고 생각하면 쉽게 이해될 것이다. 저 멀리 보이는 산이라든가 도시의 스카이라인에 비해 자동차에서 가까운 다리의 기둥들은 훨씬 빠른 속도로 시야를 스쳐 지나간다.
즉 가까운 거리에 있는 배경은 상대적으로 빠르게 움직이고 멀리 있을 수록 천천히 움직이도록 배경에 시차 스크롤링 효과를 주면 2D게임의 3차원 효과를 줄 수 있게 된다.
* 간단한 개념
시차효과를 구현하기 위해서는 각각의 배경을 별도의 이미지로 만들고 이를 중첩시켜서 서로 다른 속도로 애니메이션이 동작하도록 구현해야 한다. 즉 Canvas에 배경 이미지가 계층을 이루어 그려져야 하는데 이때 다른 이미지를 덮게 되는 이미지는 투명효과를 지원하도록 하여 밑에 깔린 이미지를 가리지 않도록 해야한다. 이것은 이미지를 제작할 때 고려되어야 하는 부분으로 디자이너의 몫이라 하겠다. 이번 예제에서는 개인적으로 투명이미지를 만들 수 있는 디자인 역량이 없는 관계로 앞에서 언급한 책의 이미지를 사용할 것이다.
총 4개의 배경 이미지가 사용되는데 이 이미지들은 자신의 표현부분을 제외하고는 모두 투명효과를 지원해서 차례대로 중첩시키면 다음과 같은 모양이 나온다.
이 4개의 배경이미지를 서로 다른 속도로 동작하도록 구현하면 시차 효과가 적용된 배경 애니메이션이 완성되는 것이다. 이때 서로 다른 속도를 구현하기 위한 다음과 같은 방법이 사용될 수 있다.
1) fps 조절
초당 프레임수를 조정하여 가까이 있는 배경일수록 더 빠른 fps를 사용한다. 다만 그다지 권장하고 싶지는 않다. 배경 이미지 애니메이션을 위해 fps가 따로 관리되어야 한다면 배보다 배꼽이 더 크지는 느낌이다.
2) 이미지 자르기 위치 조절
이전 글에서 살펴본대로 배경 애니메이션은 게임 루프상에서 이미지 자르기와 두 번의 이미지 그리기 작업을 반복함으로써 구현할 수 있었다. 즉 배경 이미지의 이동 간격을 서로 다르게 하여 시차 효과를 구현할 수 있다.
이 방법도 다음과 같이 두 가지로 나눠볼 수 있겠다.
- 이미지 크기 조절
앞에서 언급한 책인, 'LEARNING HTML5 온라인 게임 개발 프로그래밍'에서 구현한 방식이다. 이미지 크기를 배수로 조절하여 계층간 이미지 이동 간격을 별도로 계산하지 않아도 되도록 한다. 즉 첫번째 계층은 320px, 두 번째는 640px, 세 번째는 960px 너비를 갖게 함으로써 동일 시간 내에 점점 더 많은 부분을 보여주도록 한다.
- 이동 간격 조절
앞서, 이미지 크기 조절과 개념적으로는 동일하지만 이미지 크기에 제약을 두지는 않는다. 이미지 크기 조절 방식이 편리한 측면이 있으나 예제의 포괄성(?)을 위해 이미지 크기를 직접 손대지 않고 이동 간격을 조절할 수 있도록 한다. 이번 글의 예제에서 사용하는 방식이다.
* HTML
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript" src="js/animationParallxingBackground.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="320" height="200" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
</body>
</html>
* Javascript
'LEARNING HTML5 온라인 게임 개발 프로그래밍' 책의 예제 이미지를 사용했지만 코드는 이전 글들과의 맥락을 위해 사용하지 않겠다. 이 책에서는 Trident.js를 사용해 시차 효과를 구현하고 있다. 여기서는 이전 글들의 코드 구조를 유지하면서 라이브러리 없이 직접 구현하도록 한다.
1. 백그라운드, 생성자 함수와 메서드
function Background(assets,canvasElement){
this.assets = assets;
this.canvasSize = {width: canvasElement.width, height: canvasElement.height}; //Canvas Size
this.canvasContext = canvasElement.getContext('2d'); //Canvas Context
this.spritesX = [];
for(var i = 0; i < assets.length; i++){
this.spritesX.push(0);
}
}
Background.prototype.startAnimation = function(){
//Clear Canvas
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
for(var i = 0; i < this.assets.length; i++){
//Draw Background Image
var drawX = this.spritesX[i] * this.assets[i].bgImage.width;
var drawWidth = this.assets[i].bgImage.width - drawX;
this.canvasContext.drawImage(this.assets[i].bgImage,
drawX, 0, drawWidth, this.assets[i].bgImage.height,
0, 0, drawWidth, this.assets[i].bgImage.height);
//Fill Cut Out area
if(drawWidth < this.assets[i].bgImage.width) {
var fillDrawWidth = this.assets[i].bgImage.width - drawWidth;
this.canvasContext.drawImage(this.assets[i].bgImage,
0, 0, fillDrawWidth, this.assets[i].bgImage.height,
drawWidth, 0, fillDrawWidth, this.assets[i].bgImage.height);
}
this.spritesX[i] = (this.spritesX[i] + this.assets[i].spritesRate) % 1;
}
}
배경 이미지를 다뤘던 이전 글의 흐름과 거의 동일하다. 다만 이번 글에서는 중첩된 배경 이미지들이 사용될 것이기에 생성자 함수에 커스텀 객체 배열(assets)을 받는 부분과 이 배열을 기반으로 그리기 좌표 등이 계산되는 부분만 변경되었다. assets 배열의 개별 객체에는 배경 이미지 이외에도 이동 간격을 나타내는 속성인 spritesRate가 정의되어 있는데 이 값을 기준으로 자르기 X좌표(spritesX) 값의 수정이 이뤄진다. 생성자 함수에서는 spritesX를 배경 이미지 수 만큼 지정할 수 있도록 하였으며 처음에는 모두 0으로 초기화한다.
2. 기타 프로그램 로직
역시 이전 포스팅의 예제와 거의 동일하다. 중요한 것은 assets 배열에 담기는 객체의 spritesRate 속성과 그 값이다. 이 값은 이미지 자르기 x좌표를 계산하기 위한 값인데, 이 값의 크기를 배경 이미지마다 다르게 지정하여 애니메이션 속도를 조절하게 된다. 가장 밑에 깔리는 배경은 움직임이 없게 하기 위해 값을 '0'으로 지정하고 이어지는 배경이미지들은 직전 이미지보다 2배 빠른 속도로 애니에미션이 될 수 있도록 값을 지정했다.
var background; //Character Instance
var canvasElement; //Canvas Element
var assetfiles; //Asset Image File Array
var assets = []; //Custom Asset Object Array
var currentAssetLoadCount = 0; //Asset Image File Load Count
function init(){
canvasElement = document.getElementById("GameCanvas");
//Define Asset Image File Array
assetfiles = ['image/Parallax0.gif', 'image/Parallax1.gif', 'image/Parallax2.gif', 'image/Parallax3.gif'];
//Create Custom Literal Object(Define spritesRate Property) & Insert into Asset Image Array
assets.push({spritesRate: 0});
assets.push({spritesRate: 0.001});
assets.push({spritesRate: 0.002});
assets.push({spritesRate: 0.004});
for (var i = 0; i < assetfiles.length; i++) {
//Create Asset Image Ojbect
var asset = new Image();
asset.src = assetfiles[i];
//Assign Asset Image Object to the bgImage property that newly created
assets[i].bgImage = asset;
//Assign Imgae Load Event
asset.onload = onAssetLoadComplete;
}
}
function onAssetLoadComplete(){
//Check Load Complete of All Images
if(++currentAssetLoadCount >= assetfiles.length){
//Create Character Instance
background = new Background(assets,canvasElement);
//Run Game Loop
setInterval(animationLoop, 1000 / fps);
}
}
function animationLoop(){
background.startAnimation();
}
window.addEventListener("load", init, false);
* 실행화면
* 마무리하며...
두 번의 포스팅을 통해 배경 이미지 애니메이션과 시차 효과가 적용된 애니메이션 구현 기법에 대해 알아보았다. 캐릭터 애니메이션이나 배경 애니메이션 모두 게임 루프상에서 이미지 그리기를 지속적으로 업데이트 하는, 즉 궁극적으로는 동일한 매커니즘이 사용됨을 알 수 있었다. 이 지식은 어떠한 2차원 배경 이미지 애니메이션도 구현할 수 있는 초석이 될 것이다. 개념이 정립되었으면 각자 취향에 맞는 배경 애니메이션을 살을 붙여 가며 구현해 보기 바란다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Game Loop (9) | 2013.09.25 |
---|---|
[HTML5 Game] Collision Detection (15) | 2013.09.23 |
[HTML5 Game] Background Animation (12) | 2013.09.20 |
[HTML5 Game] Character Animation Using Trident.js (12) | 2013.09.17 |
[HTML5 Game] Character Animation Using Sprites (8) | 2013.09.17 |
이번 글에서는 HTML5 게임의 백그라운드 애니메이션 구현 기법을 알아보자.
백그라운드 애니메이션은 배경이 고정되어 있지 않고 지속적으로 움직이는 것으로, 여기서는 마치 자동차를 타고 지나가는 것과 같이 배경이 우측에서 좌측으로 계속 움직이는 애니메이션 효과를 구현해 볼 것이다.
실제 게임에서는 캐릭터와 배경 애니메이션이 적절히 조화된 상태로 구현되지만 간단하게는 캐릭터는 가만히 있어도 배경이 계속 흐르듯 움직이면 마치 캐릭터가 앞으로 나아가는 것같은 느낌을 줄수도 있다.
* 간단한 개념
배경이 움직인다고 해서 여러개의 이미지가 사용되는 것은 아니다.(물론 여러개의 배경이라면 그 만큼 이미지가 필요하다). 여기서는 하나의 배경이 우측에서 좌측으로 움직이는 것이므로 하나의 배경 이미지만 사용하면 된다. 이전 글, Image Sprites의 개념에서 설명했듯이 HTML5 Canvas에는 원본 이미지에서 원하는 부분만 잘라내어 그릴 수 있다고 했다. 배경 애니메이션 역시 Canvas의 이러한 특징을 이용하는 것으로 총 두 번의 그리기 작업을 지속적으로 수행함으로써 배경이 움직이도록 할 수 있다.
먼저 배경이 좌측으로 움직인만큼 X좌표를 이동시켜서 첫 번째 그리기를 수행하고, 우측에 남은 공간만큼 원본에서 다시 잘라내어 두 번째 그리기를 수행하면 된다. 이것은 마치 회전목마(carousel)를 연상시킨다. 만일 회전목마의 한 단면만 볼 수 있다면 그 단면은 지나간 말의 꼬리와 새로운 말의 머리가 만나는 것의 연속이라 할 수 있다.
결국 배경이 움직인다는 것은 시간의 흐름에 따라 배경의 그리기 좌표를 조정하고 이를 빠르게 전환시키면서 그려주면 자연스러운 애니메이션 처리가 된다는 것이다.
하나의 이미지를 사용하여 그림을 붙여 나가는 방식이기 때문에 배경 이미지는 시작과 끝이 자연스럽게 연결되는 모습이어야 한다. 그렇지 않을 경우, 애니메이션에는 지장이 없으나 배경이 끝나고 시작되는 지점의 시각적 연결이 부자연스러워 게임의 퀄리티가 손상될 것이다. 이렇게 이미지의 시작과 끝 부분을 자연스럽게 연결해서 반복하는 것을 타일링 방식이라 한다.
이제 본격적으로 Background Animation 구현방법을 알아보자.
* HTML
HTML 코드는 설명할 것도 없이 간단하다. 캔버스가 있고 대체 텍스트와 스크립트 참조가 전부이다.
<html lang="en">
<head>
<script type="text/javascript" src="js/animationBackground.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="909" height="566" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
</body>
</html>
* Javascript
늘 그래왔듯이 모든 마법(?)은 자바스크립트에서 이뤄진다. 전체적인 구조는 지금까지 구현해 왔던 것과 유사하다. 다만 캐릭터 객체가 아닌 백그라운드 객체로 변화가 있으며 두 번의 그리기 작업이 필요하다는 것이다.
애니메이션을 위하 Trident.js를 사용할 수도 있으나 이번에는 라이브러리 의존없이 구현해 보도록 한다.
1. 백그라운드, 생성자 함수와 메서드
function Background(assetObj,canvasElement){
this.assetObj = assetObj;
this.canvasSize = {width: canvasElement.width, height: canvasElement.height}; //Canvas Size
this.canvasContext = canvasElement.getContext('2d'); //Canvas Context
this.spritesX = 0; //Image X Position
}
Background.prototype.startAnimation = function(){
//Clear Canvas
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
//Draw Background Image
var drawX = this.spritesX * this.assetObj.bgImage.width;
var drawWidth = this.assetObj.bgImage.width - drawX;
this.canvasContext.drawImage(this.assetObj.bgImage,
drawX, 0, drawWidth, this.assetObj.bgImage.height,
0, 0, drawWidth, this.assetObj.bgImage.height);
//Fill Cut Out area
if(drawWidth < this.assetObj.bgImage.width) {
var fillDrawWidth = this.assetObj.bgImage.width - drawWidth;
this.canvasContext.drawImage(this.assetObj.bgImage,
0, 0, fillDrawWidth, this.assetObj.bgImage.height,
drawWidth, 0, fillDrawWidth, this.assetObj.bgImage.height);
}
this.spritesX = (this.spritesX + this.assetObj.spritesRate) % 1;
}
먼저 Background 생성자 함수에서는 배경이미지를 표현하는 에셋 인스턴스와 Canvas 요소를 전달받아서 초기화 작업을 수행한다. spritesX 변수는 배경이미지의 이동을 처리하기 위해 원본 이미지에서 이동할 크기를 저장하는 변수이다.
다음으로 실제 애니메이션 처리가 이뤄지는 startAnimation 메서드에서는 캔버스를 다 지우면서 시작한다.
drawX 변수는 원본 이미지 크기에서 spritesX를 곱한 값으로, 원본 이미지에서 원한는 부분을 자르게 될 X좌표 값이 된다. spritesX의 값은 이후 나오게 될 spritesRate값을 계속 더하여 원본 이미지의 x 좌표를 spritesRate 간격만큼 이동시키는데, 예를 들어 원본 이미지 너비가 100px이고 spritesRate가 0.1일 경우 spritesX와 drawX값의 변화는 다음과 같다.
spretesX |
0 |
0.1 |
0.2 |
0.3 |
drawX |
0 |
10 |
20 |
30 |
즉, X좌표가 10px 만큼 이동하게 되는 것이다.
이어지는 drawWidth는 원본 이미지에서 drawX르 뺀 값으로 drawX부터 이미지 나머지 부분의 크기를 나타낸다. 이렇게 첫번째 그리기 작업이 수행된다.
이후 두번째 그리기 작업이 수행되는데, 이 시점에 브라우저로 확인해 보는 것도 프로그램 로직 이해에 도움이 될 것이다. 두 번째 그리기 작업은 앞서 첫번째 그리기 작업에서 비워진 우측 공간을 메우는 작업으로 역시 동일한 원본 이미지의 첫 부분에서 비워진 공간만큼 잘라내어 그리는 것이다.
두번째 그리기 작업에서 중요한 것은 fillDrawWidth 변수인데, 이 변수에는 비워진 공간의 너비를 계산해서 그 값을 저장하게 된다. 그리고 비워진 공간의 X좌표에 두번째 그리기 작업을 수행하면 두 개의 그리기 작업이 자연스럽게 연결됨으로써 애니메이션 효과가 구현되는 것이다.
마지막으로 spritesX 값을 spritesRate만큼 계속 더해가는데, 최대 크기 비율인 1이 될 시점에 다시 0으로 만들어 줌으로써 이미지가 처음부터 다시 그려지도록 한다.
2. 기타 프로그램 로직
나머지 코드는 이전에 알아봤던 캐릭터 애니메이션의 그것과 거의 유사하다.
var background; //Character Instance
var canvasElement; //Canvas Element
var asset; //Asset Image Ojbect
function init(){
canvasElement = document.getElementById("GameCanvas");
//Create Asset Image Ojbect
asset = new Image();
asset.src = 'image/background.png';
//Assign Imgae Load Event
asset.onload = onAssetLoadComplete;
}
function onAssetLoadComplete(){
//Create Custom Asset Object
var assetObj = {bgImage:asset, spritesRate:0.01};
//Create Character Instance
background = new Background(assetObj,canvasElement);
//Run Game Loop
setInterval(animationLoop, 1000 / fps);
}
function animationLoop(){
background.startAnimation();
}
window.addEventListener("load", init, false);
fps 값을 60 즉, 1초에 60번 프레임을 교체하도록 하여 캐릭터 애니메이션 보다 조금 더 빠른 속도를 지정했다.
나머지 코드는 캐릭터 애니메이션 예제와 동일하므로 이전 글을 참고하면 되고 다만 spritesRate가 추가되었는데 이것은 배경 이미지의 이동 간격을 나타낸다. 코드에서는 0.01을 지정했는데 이것의 의미는 프레임이 이동할 때마다 원본 이미지에서 0.01px 이동한다는 의미가 된다. fps와 함께 이 값의 변화는 배경 애니메이션의 속도에 영향을 준다. 이 값이 클수록 한 프레임에 이동하는 배경 이미지의 간격이 크지므로 더욱 빠른 애니메이션 효과를 낼 수 있다. 즉 이 값의 조정을 통해 동일한 fps 상에서 캐릭터의 속도를 증가시킬수도 있다는 것이다.
실행화면
인터넷에서 background image를 검색해서 이미지의 좌측 시작부분과 우측 끝 부분이 매끄럽게 연결되는 이미지를 다운로드 해서 예제를 실행해 보자.
마무리하며...
이 글에서는 횡스크롤 형태의 게임에서 사용할법한 배경 애니메이션을 구현해 봤다.
원본 이미지의 좌표를 이동해 가며 두 번의 그리기 작업을 반복함으로써 배경이 우측에서 좌측으로 이동하는 효과를 구현했는데 이는 유명한 고전 오락실 게임인 1945와 같이 위에서 아래로 흐르는 배경 애니메이션 처리에도 동일한 개념으로 접근할 수 있을 것이다. 애니메이션의 방향을 바꿔서 직접 구현해 보면 더욱 빠른 이해가 될 것이다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Collision Detection (15) | 2013.09.23 |
---|---|
[HTML5 Game] Parallxing Background Animation (11) | 2013.09.21 |
[HTML5 Game] Character Animation Using Trident.js (12) | 2013.09.17 |
[HTML5 Game] Character Animation Using Sprites (8) | 2013.09.17 |
[HTML5 Game] Character Animation (7) | 2013.09.14 |
[HTML5 Game] Character Animation Using Trident.js
Posted in 모바일/HTML5 // Posted at 2013. 9. 17. 20:00앞서 두 개의 글을 통해 HTML5 상에서 애니메이션 구현을 알아봤다.
이 두 글에서는 자바스크립트의 setInterval() 함수를 통해 애니메이션 처리를 직접 구현했었다.
이번에는 잘 알려진 오픈소스 라이브러리를 이용해 애니메이션을 구현해 보도록 한다.
이 글에서 사용할 라이브러리는 Kirill Grouchnikov라는 스마트한 개발자에 의해 개발된,
Trident.js 라는 자바스크립트 애니메이션 라이브러리로 ProcessingJS 처럼 기존 JAVA 라이브러리를 자바스크립트 버전으로 포팅한 것으로 타임라인과 키프레임에 기반한 애니메이션 처리를 지원하고 Easing 함수를 통해 비선형 타임라인을 지원하는 등 애니메이션 처리와 관련된 안정되고 다양한 기능을 제공한다.
이 라이브러리는 github에 공개되어 있다.
> https://github.com/kirillcool/trident-js
공개된 라이브러리를 사용한다는 것은 개발의 생산성과 안정성을 높이고 유용하고 다양한 기능을 큰 시행착오 없이 적용할 수 있는 등 많은 장점을 가져다 준다. 따라서 대부분의 개발환경에서 이미 구현된 라이브러리 사용을 권장하고 있다.
다만 기본 개념이 전혀 갖춰져 있지 않은 상태에서 무작정 라이브러리에만 의존하다보면 프로그램의 원리적 동작의 이해가 부족하게 되고 복잡한 문제를 대응하는 능력이 떨어져 전반적인 프로그램 개발 역량을 성장시키지 못하게 되는 원인이 되기도 한다. 따라서 이전 두 글에서와 같이 라이브러리 의존 없이 기본적인 구현을 직접 해 봄으로써 애니메이션 처리의 기본 구현 원리를 숙지하고 난후 유용한 라이브러리에 관심을 가지는 수순을 따르는게 좋겠다.
그럼. 본격적으로 Trident.js를 사용해보게 될텐데, 캐릭터 애니메이션 구현을 해 보기 전에 기본적인 라이브러리 사용법을 간단히 알아보자.
* Trident.js 기본
Trident.js 사용해서 HTML 이미지요소의 투명도를 타임라인에 기반해 서서히 변경해 보는 간단한 예를 살펴봄으로써 이 라이브러리의 기본 사용법을 익혀보자.
먼저 다음과 같이 github에서 다운받은 trident.js와 이미지요소가 정의된 간단한 HTML파일을 만들자.
<head>
<script src="js/trident.js"></script>
<script type="text/javascript" src="js/tridentBasic.js"></script>
</head>
<body>
<img id="myImg" src="image/iu.png" style="opacity:0.3" />
</body>
</html>
그리고 자바스크립트에서 다음과 같은 로직을 구현한다.
//Create TimeLine Instance
var rolloverTimeline = new Timeline(myImg.style);
//Add Interpolator
rolloverTimeline.addPropertiesToInterpolate([
{
property: "opacity", from:0.3, to:1.0, interpolator: new FloatPropertyInterpolator()
}
]);
//Define Animation Duration
rolloverTimeline.duration = 500;
//Add Event Listener of Image
myImg.addEventListener("mouseover", function(){rolloverTimeline.play()}, false);
myImg.addEventListener("mouseout", function(){rolloverTimeline.playReverse()}, false);
}
window.addEventListener("load", init, false);
Trident.js는 그래픽 편집 툴의 그것과 동일한 개념의 타임라인 객체를 제공한다. 이 객체에 설정한 각종 정보를 기반으로 애니메이션 처리가 이뤄지는데 먼저 생성자 함수로 애니메이션할 요소와 대상 속성을 설정한다.
여기서는 이미지 요소의 style속성을 애니메이션 대상 속성으로 지정했다.
var rolloverTimeline = new Timeline(myImg.style);
그리고 애니메이션할 속성과 시작-끝 값 그리고 시작과 끝 값 사이에 변경해야 할 값(보간값)의 형태를 지정한다. 여기서는 이미지의 투명도에 해당하는 opacity 속성을 지정하고 0.3 ~ 1.0 사이의 값을 실수 형태의 보간(FloatPropertyInterpolator)이 이뤄지도록 지정했다.
rolloverTimeline.addPropertiesToInterpolate([
{
property: "opacity", from:0.3, to:1.0, interpolator: new FloatPropertyInterpolator()
}
]);
참고로 보간형태는 FloatPropertyInterpolator외에도 정수값 보간인 IntPropertyInterpolator과 RGB값 보간인 RGBPropertyInterpolator가 제공된다.
마지막으로 애니메이션이 동작할 시간을 밀리초단위로 지정하는데 여기서는 0.5초로 지정했다
rolloverTimeline.duration = 500;
결국 0.5초 동안 이미지 투명도가 0.3에서 1.0으로 점점 밝아 지도록 하는 것이다. 여기까지해서 Trident.js의 애니메이션 설정은 모두 끝이 났다.
이어지는 코드는 실제 애니메이션 처리를 하기 위한 이벤트를 등록하는 것이다.
코드에서는 이미지에 마우스를 올리면 애니메이션이 시작하도록 하고 마우스를 떼면 역방향으로 애니메이션이 진행하도록 구현했다.
myImg.addEventListener("mouseover", function(){rolloverTimeline.play()}, false);
myImg.addEventListener("mouseout", function(){rolloverTimeline.playReverse()}, false);
브라우저로 샘플을 실행해서 애니메이션을 확인해 보자.(눈의 즐거움을 위해 국민 여동생 등장시킴!)
=>
* Trdent.js를 활용한 캐릭터 애니메이션 구현
이제 이 라이브러리를 이용해서 캐릭터 애니메이션을 구현해 보자. 전체적인 흐름은 이전과 유사하며 이미지는 Image Sprites 기법으로 처리한다.
1. 캐릭터, 생성자 함수와 메서드
/* Define Character Class */
function Character(assetObj,canvasElement){
this.assetObj = assetObj; //Custom Asset Object
this.canvasSize = {width: canvasElement.width, height: canvasElement.height}; //Canvas Size
this.canvasContext = canvasElement.getContext('2d'); //Canvas Context
this.spritesX = 0; //Sprite Image X Position
}
Character.prototype.startAnimation = function()
{
//Assign This Instance Context('this' keyword Changed according to the Effective Range)
var self = this;
//Create TimeLine
var spriteTimeline = new Timeline(self);
//Add Interpolate
spriteTimeline.addPropertiesToInterpolate([
{
//Interpolate Property: spritesX, Interpolate Value: 0 ~ 36, Interpolate Type: Int
property: "spritesX", from:0, to: 36, interpolator: new IntPropertyInterpolator()
}
]);
//Define Animation Duration
spriteTimeline.duration = 2500;
//Add onpulse Event Listener
spriteTimeline.addEventListener("onpulse",
function (timeline, durationFraction, timelinePosition) {
self.drawCanvas();
}
);
//Play Animation(Infinite Looping)
spriteTimeline.playInfiniteLoop();
}
Character.prototype.drawCanvas = function(){
//Clear Canvas
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
//Draw Image on Canvas
this.canvasContext.drawImage(this.assetObj.assetImage,
this.spritesX * this.assetObj.spritesWidth, this.assetObj.spritesY,
this.assetObj.spritesWidth, this.assetObj.spritesHeight,
100, 100,
this.assetObj.spritesWidth, this.assetObj.spritesHeight);
//Update Sprite Image X Position
//this.spritesX = ++this.spritesX % this.assetObj.spritesCount;
}
캐릭터 메서드의 구조가 이전과는 조금 달라졌다.
startAnimation() 메서드에서 Trident.js의 타임라인을 생성하고 애니메이션을 시작하도록 구현했으며 캔버스에 그리는 작업인 drawCanvas() 메서드를 분리시켰다.
타임라인 인스턴스를 생성할 때 앞서 기본 예제와는 달리 HTML 요소가 아니라 객체 자체를 매개변수로 전달하고 있다. 이렇게 하면 객체의 특정 속성을 기준으로 애니메이션 처리를 할 수 있게 된다.
var self = this;
var spriteTimeline = new Timeline(self);
또한 여기서 캐릭터 인스턴스의 문맥을 나타내는 'this' 값을 self라는 지역변수에 할당하고 있는데 이렇게 하는 이유는 this 키워드가 다른 유효범위에서는 다른 문맥을 가르키기 때문에 프로그램이 정상 동작하지 않을 수 있다. 이 예제에서는 타임라인에 이벤트 리스너를 등록할 때 문제가 발생한다. 따라서 self라는 변수를 통해 캐릭터 인스턴스의 문맥이 유효할 수 있도록 하며 타임라인 위치가 변경할 때마다 동작할 이벤트 리스너를 다음과 같이 등록한다.
spriteTimeline.addEventListener("onpulse", function (timeline, durationFraction, timelinePosition) {
self.drawCanvas();
});
그리고 원본 이미지의 자르기 x좌표인 spritesX 값은 더이상 직접 업데이트 해 줄 필요가 없게 되었다. Trident.js의 보간값 설정에서 다음과 같이 이 변수의 변경될 값 범위(0~36)를 지정했기 때문에 타임라인이 실행되면서 자동으로 spritesX값이 업데이트 되기 때문이다.
spriteTimeline.addPropertiesToInterpolate([
{
//Interpolate Property: spritesX, Interpolate Value: 0 ~ 36, Interpolate Type: Int
property: "spritesX", from:0, to: 36, interpolator: new IntPropertyInterpolator()
}
]);
마지막으로 애니메이션을 2.5초간 동작하도록 하여 이전 두 글의 fps와 값을 유사하게 맞췄다. 더불어 애니메이션이 무한 반복되도록 다음과 같이 playInfiniteLoop() 메서드를 호출한다.
spriteTimeline.playInfiniteLoop();
2. 프로그램 로직
나머지는 이전 코드와 모두 동일하며 onAssetLoadComplete() 함수에 약간의 변화가 있다.
이전 코드에서는 자바스크립트의 setInterval() 메서드를 이용해 직접 루프를 동작시켰지만 Trideng.js에서는 자체 타임라인을 기반으로 루프가 동작하기 때문에 더 이상 사용할 필요가 없다.
//Create Custom Asset Object
var assetObj =
{assetImage:asset, spritesCount:36, spritesWidth:128, spritesHeight:128, spritesY: 128 * 0};
//Create Character Instance
character = new Character(assetObj,canvasElement);
//setInterval(animationLoop, 1000 / fps);
character.startAnimation();
}
//function animationLoop(){
//character.startAnimation();
//}
실행화면
브라우저로 실행해 보면 이전 예제와 동일한 애니메이션을 확인할 수 있다.
마무리하며...
지금까지 총 3차례에 걸쳐 캐릭터 애니메이션을 구현해 보았다. 첫번째는 동작별로 분리된 이미지들을 이용해 애니메이션을 구현해 봤으며 두번째는 하나의 이미지를 기반으로 Image Sprites 기법을 활용했으며 마지막으로 이 글에서는 Image Sprites 기법과 더불어 Trident.js 라는 오픈소스 라이브러리를 활용해서 동일한 애니메이션을 구현해 보았다. 이 중에서 제일 권장되는 방법은 1 < 2 < 3 즉, 이 Trident.js를 활용하는 것이 여러모로 좋을 것이다. 라이브러리의 안정성이 곧 프로그램의 안정성으로 직결되며 반복 카운트 및 되감기 애니메이션 지원과 타임라인과 키프레임의 조합, 비선형 타임라인 지원 등 더 강력한 기능을 공짜로(?) 얻을 수 있기 때문이다. 하지만 자바스크립트의 애니메이션 처리를 위한 기본 원리를 알기 위해서는 이전 두 과정을 필수로 거쳐보기 바란다. 기본기 없이 라이브러리에만 의존한다면 언젠가 더 높은 벽을 만나게 될 것이다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Parallxing Background Animation (11) | 2013.09.21 |
---|---|
[HTML5 Game] Background Animation (12) | 2013.09.20 |
[HTML5 Game] Character Animation Using Sprites (8) | 2013.09.17 |
[HTML5 Game] Character Animation (7) | 2013.09.14 |
CSS3 3D Effect (4) | 2013.08.02 |
이전 글에서 캐릭터의 움직임을 위한 애니메이션 기법을 알아봤다.
이전 글: http://m.mkexdev.net/235
이 글에서는 캐릭터 애니메이션을 처리하기 위해 각각의 동작을 분리된 개별 이미지로 처리했었다.
하지만 이런 방식은 많은 이미지 리소스를 다운로드 해야 하기 때문에 비효율적이다.
이번 글에서는 Image Sprites 기법을 이용해 애니메이션을 구현하는 예를 살펴 보겠다.
Image Sprites 모든 캐릭터의 모든 동작을 하나의 이미지로 만들어 필요한 부분만 잘라내서 사용하는 방식으로 리소스의 다운로드 비용을 줄이고 효율성을 높이는 기법으로 실제 게임 개발에 많이 사용되고 있기도 하다.
아래 그림은 http://opengameart.org 의 샘플 이미지로 8 * 36 즉, 총 8가지 패턴의 서로 다른 동작 36개를 하나의 이미지로 모아놓은 것이다. 이번 예에서는 이 이미지를 Sprites 해서 애니메이션을 구현해 볼 것이다.
* 간단한 개념
HTML5 Canvas에 이미지를 그릴 때, 이미지 원본의 특정 좌표에서 원하는 너비,높이 만큼 이미지를 그릴 수 있다. 예를 들어 아래 그림과 같이 4개의 서로 다른 모습이 하나의 이미지에 포함되어 있을 때 개별 이미지의 너비/높이 값으로 특정 이미지 좌표와 크기를 산출할 수 있으며 이렇게 산출된 값으로 원하는 영역의 이미지를 Canvas에 그릴 수 있게 된다. 즉 아래 그림과 같이 4개의 동작일 경우, (0,0) ~ (384,0)까지 128*128 크기로 차례대로 그려주면 되는 것이다.
* HTML
HTML은 이전 글과 동일한 구조다.
<html lang="en">
<head>
<script type="text/javascript" src="js/animationCharacterUsingSprites.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="300" height="300" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
</body>
</html>
* JavaScript
자바스크립트 역시 전체적인 흐름은 이전 글의 내용과 동일하다.
다만 이전 글에서는 개별 이미지를 담는 배열과 이 배열의 인덱스를 탐색하기 위한 프레임 값의 업데이트가 있었던 것에 반해, 여기서는 하나의 이미지에서 원하는 부분을 잘라내기 위한 이미지 X좌표 값의 업데이트가 구현된다.
1. 캐릭터, 생성자 함수와 메서드
/* Define Character Class */
function Character(assetObj,canvasElement){
this.assetObj = assetObj; //Custom Asset Object
this.canvasSize = {width: canvasElement.width, height: canvasElement.height}; //Canvas Size
this.canvasContext = canvasElement.getContext('2d'); //Canvas Context
this.spritesX = 0; //Image X Position
}
Character.prototype.startAnimation = function(){
//Clear Canvas
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
//Draw Image on Canvas
this.canvasContext.drawImage(this.assetObj.assetImage,
this.spritesX * this.assetObj.spritesWidth, this.assetObj.spritesY,
this.assetObj.spritesWidth, this.assetObj.spritesHeight,
100, 100,
this.assetObj.spritesWidth, this.assetObj.spritesHeight);
//Update Sprite Image X Position
this.spritesX = ++this.spritesX % this.assetObj.spritesCount;
}
이번 코드의 생성자 함수에서는 이미지 배열이 아닌 하나의 이미지 정보를 담고 있는 커스텀 객체를 매개변수로 전달받는다. 그리고 원본 이미지에서 잘라 낼 X 좌표 값을 위한 spritesX 변수를 선언하였다. 이 변수는 총 36개의 동작의 X좌표 계산을 위한 변수이며 '0 에서 35'의 값을 가지게 된다. 이 값에 개별 동작의 너비 값인 128px를 곱해 주면 0 ~ 4480 즉, 실제 잘라 낼 이미지의 X좌표를 계산할 수 있게 된다.
HTML5의 Canvas는 원본 이미지에서 원하는 부분을 잘라내어 그릴 수 있도록 아래와 같은 매개변수를 가진 drawImage()함수를 제공한다.
drawImage 매개변수>
- image, in float sx, in float sy, in float sw, in float sh, in float dx, in float dy, in float dw, in float dh
drawImage 매개변수 설명>
- 이미지 객체, 자르기 X 값, 자르기 Y 값, 자를 너비, 자를 높이, 그릴 위치 X 값, 그릴 위치 Y 값, 그릴 너비, 그릴 높이
(실제 제대로 된 해석은 원본 X값, 대상 X값.. 이런 식이지만 원본 이미지를 자른다는 개념을 명확히 하기 위해 임의대로 설명함)
2. 프로그램 로직
하나의 이미지만을 사용하기 때문에 이전 코드에 비해 전역변수가 줄어 들었다. 그리고 초당 프레임 수인 fps도 15로 작게 설정되었다. 샘플을 실행해 보면 알겠지만 초당 30 프레임으로 동작시키면 캐릭터가 너무 빠르게 움직이므로 적절한 값으로 변경해 주었다.
var character; //Character Instance
var canvasElement; //Canvas Element
var asset; //Asset Image Ojbect
그리고 초기화 함수를 보면 단 한개의 이미지만을 로딩하는 것을 확인할 수 있으며 이미지 객체와 개별 동작의 수, 개별 이미지의 너비/높이 및 Y 좌표 값을 담은 커스텀 객체를 생성해서 캐릭터 객체의 생성자 함수로 전달해 주고 있다. 이 값들을 이용해 원본 이미지에서 어떤 부분의 이미지들을 애니메이션 할 것인가가 결정된다.
canvasElement = document.getElementById("GameCanvas");
//Create Asset Image Ojbect
asset = new Image();
asset.src = 'image/zombie.png';
//Assign Imgae Load Event
asset.onload = onAssetLoadComplete;
}
function onAssetLoadComplete(){
//Create Custom Asset Object
var assetObj =
{assetImage:asset, spritesCount:36, spritesWidth:128, spritesHeight:128,
spritesY: 128 * 0};
//Create Character Instance
character = new Character(assetObj,canvasElement);
//Run Game Loop
setInterval(animationLoop, 1000 / fps);
}
function animationLoop(){
character.startAnimation();
}
window.addEventListener("load", init, false);
실행화면
코드를 실행하면 좀비가 걸어가다 머리가 터지며 죽는 모습의 애니메이션을 확인할 수 있다.
마무리 하며...
지금까지 캐릭터 이미지를 구현하기 위한 두 가지 방법을 알아 보았다. 사실 두 가지 방법은 이미지를 몇 개 사용하는지만 다를 뿐 동일한 프로그램 로직을 가지고 있다. 실제 게임 개발에서는 Image Sprites 기법을 많이 사용하므로 이 기법으로 여러가지 테스트 프로그램을 작성해 보는 것이 도움이 될 것이다.
두개의 글을 통해 애니메이션 구현 기법을 충분히 숙지하였으니, 다음 글에서는 공개된 라이브러리를 사용해서 애니메이션을 구현해 보도록 하겠다. 라이브러리를 활용하면 좀 더 안정적으로 그리고 생산성 높게 개발할 수 있으므로 자신의 개발 무기로 장착해 두는 것이 좋을 것이다.
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Background Animation (12) | 2013.09.20 |
---|---|
[HTML5 Game] Character Animation Using Trident.js (12) | 2013.09.17 |
[HTML5 Game] Character Animation (7) | 2013.09.14 |
CSS3 3D Effect (4) | 2013.08.02 |
... (4) | 2013.07.23 |
HTML5의 Canvas 요소를 활용하여 그럴싸한 2D 게임을 개발할 수 있다.
UDACITY에서도 HTML5 기반의 게임 개발 강의를 무료로 제공하고 있다
=> https://www.udacity.com/course/cs255
여기서 제공하는 캐릭터 애니메이션 구현 기법을 살펴보자. 코드는 내 입맛에 맞게 수정을 가했지만 전체적인 맥락은 동일하다.
대략적은 구현 흐름을 보면, 캐릭터의 움직임을 위해서 각각의 컷을 개별 이미지로 준비하고 이 이미지들을 Canvas에 차례대로 반복해서 그려주는 것이다. 이미지들은 매우 빠른 속도로 Canvas에 그려지는데 이때 적용되는 개념이 초당 프레임(FPS, frame per second)이다.
FPS는 게임 개발 영역에서는 자연스러운 개념으로 인간이 느끼지 못하는 속도로 프레임을 교체하여 자연스러운 움직임이 가능하도록 하는 것으로 영화 필름의 재생을 생각해 보면 이해하기 쉽다.
아래 그림은 캐릭터 움직임을 19개 이미지로 분할한 모습을 보여준다.
* HTML
애니메이션 처리는 모두 자바스크립트에서 구현한다. HTML 파일은 단지 Canvas 요소와 자바스크립트을 참조하는 코드만 존재한다.
<html lang="en">
<head>
<script type="text/javascript" src="js/animationCharacter.js"></script>
</head>
<body>
<canvas id="GameCanvas" width="300" height="300" style="border: 1px solid #000;">
HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
</canvas>
</body>
</html>
* JavaScript
자바스크립트로 이미지를 다운로드하여 애니메이션을 구현한다. 애니메이션 처리를 위한 자바스크립트의 핵심 함수는 setInterval()이다. 이 함수는 주어진 간격으로 함수를 반복(loop) 실행시켜주는 함수로 setTimeout() 함수와 함께 HTML5 게임 개발에 필수적으로 사용되는 함수이다. 먼저 캐릭터 객체를 위한 코드부터 살펴보자
1. 캐릭터, 생성자 함수와 메서드
this.assets = assets; //Asset Image Array
this.canvasSize = {width: canvasElement.width, height: canvasElement.height}; //Canvas Size
this.canvasContext = canvasElement.getContext('2d'); //Canvas Context
this.currentFrame= 0; //Current Frame
}
Character.prototype.startAnimation = function(){
//Clear Canvas
this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
//Draw Image on Canvas
this.canvasContext.drawImage(this.assets[this.currentFrame], 100, 100);
//Update Current Frame
this.currentFrame = ++this.currentFrame % this.assets.length;
}
생성자 함수에서는 게임 캐릭터의 개별 이미지를 담을 assets 배열과, 이 이미지를 그려줄 Canvas 요소를 매개변수로 받아서 초기화 작업을 수행한다. CurrentFrame는 개별 이미지를 담고 있는 assets 배열의 인덱스로 사용될 변수로 총 19개의 이미지를 순서대로 교체하기 위해 0~18까지의 값을 가지게 된다.
그리고 실제 애니메이션 동작을 처리하는 startAnimation 메서드에서는 먼저 Canvas를 초기화시킨다. Canvas를 초기화하지 않으면 이전에 그린 그림과 새로 그리는 그림이 서로 겹쳐지게 되어 잔상이 남게 되기 때문에 초기화 하는 작업을 제일 먼저 수행한다.
다음으로 19개의 캐릭터 이미지를 담고 있는 assets 배열에서 CurrentFrame에 해당하는 이미지를 선택해서 Canvas에 그려준다. 마지막으로 19개의 이미지를 순회하기 위해 CurrentFrame 값을 업데이트 하는데 0~18까지 순회하도록 나머지 연산을 수행한다. 결국 이 startAnimation 메서드가 끊임없이 반복 수행됨으로써 캐릭터의 움직임을 구현할 수 있게 되는 것이다.
2. 프로그램 로직
이제 이미지를 다운로드해서 setInterval()함수와 캐릭터 객체를 활용하는 코드를 살펴보자.
먼저 전역변수를 정의한다.
var character; //Character Instance
var canvasElement; //Canvas Element
var assetfiles; //Asset Image File Array
var assets = []; //Asset Image Ojbect Array
var currentAssetLoadCount = 0; //Asset Image File Load Count
fps는 frame per count 약자로 초당 프레임 수를 나타내는데 30이라는 값은 1초에 30번 함수를 호출하겠다는 의미이다. 즉 1초에 30번 프레임이 이동 시키겠다는 의미이다.
다음으로 초기화함수를 정의한다.
canvasElement = document.getElementById("GameCanvas");
//Define Asset Image File Array
assetfiles =
[ 'image/robowalk/robowalk00.png', 'image/robowalk/robowalk01.png',
'image/robowalk/robowalk02.png', 'image/robowalk/robowalk03.png', 'image/robowalk/robowalk04.png', 'image/robowalk/robowalk05.png',
'image/robowalk/robowalk06.png', 'image/robowalk/robowalk07.png', 'image/robowalk/robowalk08.png', 'image/robowalk/robowalk09.png', 'image/robowalk/robowalk10.png', 'image/robowalk/robowalk11.png',
'image/robowalk/robowalk12.png', 'image/robowalk/robowalk13.png', 'image/robowalk/robowalk14.png', 'image/robowalk/robowalk15.png', 'image/robowalk/robowalk16.png','image/robowalk/robowalk17.png',
'image/robowalk/robowalk18.png'
];
for (var i = 0; i < assetfiles.length; i++) {
//Create Asset Image Ojbect
var asset = new Image();
asset.src = assetfiles[i];
//Insert Asset Image in Asset Image Array
assets.push(asset);
//Assign Imgae Load Event
asset.onload = onAssetLoadComplete;
}
}
페이지가 로딩되면 제일 처음 실행될 초기화 함수에서는 HTML DOM에서 Canvas 요소를 선택하고 총 19개의 캐릭터 이미지를 파일을 불러와서 이미지 객체를 생성하여 assets 배열에 담는다. 실제 웹 환경에서는 이미지 다운로드 시간에 지연이 있을 수 있으므로 이미지 로드가 모두 완료된 후 다음 로직을 수행해야 한다. 이미지 로딩이 완료되면 발생하는 onload 이벤트에 onAssetLoadComplete 함수를 할당한다.
onAssetLoadComplete 함수 코드는 다음과 같다.
//Check Load Complete of All Images
if(++currentAssetLoadCount >= assetfiles.length){
//Create Character Instance
character = new Character(assets,canvasElement);
//Run Game Loop
setInterval(animationLoop, 1000 / fps);
}
}
function animationLoop(){
character.startAnimation();
}
이 함수에서는 모든 캐릭터 이미지가 로딩 완료되었는지 체크하고 완료되었다면 캐릭터 객체를 생성하고 setInterval() 함수로 반복 루프를 시작시킨다. setInterval() 함수는 반속 실행을 위한 시간 간격을 밀리세컨트 단위로 설정하게 되는데 1000/fps 즉 1000/30은 0.33초 마다 함수를 수행하라는 의미가 되므로 결과적으로 1초에 30번 Character 객체의 startAnimation() 함수가 수행시키게 된다.
그리고 마지막으로 페이지가 로딩되면 초기화 함수가 수행될 수 있도록 이벤트를 연결시킨다.
실행화면
코드를 모두 작성하고 HTML5를 지원하는 브라우저(크롬 or 사파리)로 확인하면 로봇 캐릭터가 움직이는 애니메이션을 확인할 수 있다. 다음 그림은 화면을 캡쳐한 것이다.
마무리 하며...
이번 글에서는 UDACITY 강의를 기반으로 HTML5 기반의 애니메이션 처리를 살펴봤다. 여기서는 총 19개에 달하는 각각의 캐릭터 이미지를 사용했지만 실제 게임 개발에서는 이렇게 많은 이미지를 사용한다는 것은 매우 비효율적인 것이 된다. 그래서 대부분 Image Sprite 라는 기법을 이용해 모든 에셋이 포함된 하나의 이미지를 기반으로 필요한 에셋만 잘라내서 활용하는 방식이 사용된다. 또한 이러한 애니메이션 처리를 위한 오픈소스 라이브러리도 많이 존재한다. 다음 글에서는 Image Sprite 기법과 애니메이션 라이브러리를 사용하여 애니메이션을 구현하는 기법을 알아보자.
참고> 이 글에서 사용된 게임 에셋 이미지(UDACITY 이미지) 다운로드 경로
=> https://www.udacity.com/media/js/standalone/libs/gamedev_assets/robowalk/robowalk01.png
'모바일 > HTML5' 카테고리의 다른 글
[HTML5 Game] Character Animation Using Trident.js (12) | 2013.09.17 |
---|---|
[HTML5 Game] Character Animation Using Sprites (8) | 2013.09.17 |
CSS3 3D Effect (4) | 2013.08.02 |
... (4) | 2013.07.23 |
HTML5 개발을 도와주는 도구들 (12) | 2010.11.01 |