소개
Wordico 십자말풀이 게임을 플래시에서 HTML5로 변환할 때 가장 먼저 브라우저에서 풍부한 사용자 환경을 만드는 방법에 관해 알고 있던 모든 것을 잊어버려야 했습니다. Flash는 벡터 그리기에서 다각형 히트 감지, XML 파싱에 이르기까지 애플리케이션 개발의 모든 측면에 대해 단일의 포괄적인 API를 제공했지만 HTML5는 다양한 브라우저 지원과 함께 뒤죽박죽된 사양을 제공했습니다. 또한 문서별 언어인 HTML과 상자 중심 언어인 CSS가 게임 빌드에 적합한지 궁금했습니다. 게임이 Flash에서와 마찬가지로 브라우저에서 일관되게 표시되고 멋지게 보이고 작동하나요? Wordico의 경우 예였습니다.
빅터님, 벡터가 어떻게 되나요?
Wordico의 원래 버전은 선, 곡선, 채우기, 그라데이션과 같은 벡터 그래픽만을 사용하여 개발되었습니다. 그 결과 매우 컴팩트하면서도 무한 확장이 가능했습니다.
또한 Flash 타임라인을 활용하여 여러 상태를 가진 객체를 만들었습니다. 예를 들어 Space
객체에 이름이 지정된 키프레임 9개를 사용했습니다.
그러나 HTML5에서는 비트맵 스프라이트를 사용합니다.
개별 공간에서 15x15 게임 보드를 만들기 위해 각 공간이 다른 문자로 표시되는 225자 문자열 표기법을 반복합니다 (예: 3글자 단어의 경우 't', 3단어 단어의 경우 'T'). Flash에서는 간단하게 공백을 스탬프하고 그리드에 정렬하면 됩니다.
var spaces:Array = new Array();
for (var i:int = 0; i < 225; i++) {
var space:Space = new Space(i, layout.charAt(i));
...
spaces.push(addChild(space));
}
LayoutUtil.grid(spaces, 15);
HTML5에서는 좀 더 복잡합니다. 비트맵 그리기 영역인 <canvas>
요소를 사용하여 게임 보드를 한 번에 한 정사각형씩 칠합니다. 첫 번째 단계는 이미지 스프라이트를 로드하는 것입니다. 로드되면 레이아웃 표기법을 반복하여 각 반복마다 이미지의 다른 부분을 그립니다.
var x = 0; // x coordinate
var y = 0; // y coordinate
var w = 35; // width and height of a space
for (var i = 0; i < 225; i++) {
if (i && i % 15 == 0) {
x = 0;
y += w;
}
var imageX = "_dDFtTqQxm".indexOf(layout.charAt(i)) * 70;
canvas.drawImage("spaces.png", imageX, 0, 70, 70, x, y, w, w);
x += w;
}
웹브라우저의 결과입니다. 캔버스 자체에 CSS 그림자가 있습니다.
카드 객체를 변환하는 작업도 비슷했습니다. Flash에서는 텍스트 필드와 벡터 도형을 사용했습니다.
HTML5에서는 런타임 시 단일 <canvas>
요소에 세 개의 이미지 스프라이트를 결합합니다.
이제 100개의 캔버스 (타일마다 하나씩)와 게임 보드용 캔버스가 있습니다. 다음은 'H' 카드의 마크업입니다.
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
다음은 해당 CSS입니다.
.tile {
width: 35px;
height: 35px;
position: absolute;
cursor: pointer;
z-index: 1000;
}
.tile-drag {
-moz-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
-webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
-moz-transform: scale(1.10);
-webkit-transform: scale(1.10);
-webkit-box-reflect: 0px;
opacity: 0.85;
}
.tile-locked {
cursor: default;
}
.tile-racked {
-webkit-box-reflect: below 0px -webkit-gradient(linear, 0% 0%, 0% 100%,
from(transparent), color-stop(0.70, transparent), to(white));
}
카드가 드래그될 때 (그림자, 불투명도, 크기 조정)와 카드가 랙에 있을 때 (반사) CSS3 효과를 적용합니다.
래스터 이미지를 사용하면 몇 가지 명백한 이점이 있습니다. 첫째, 결과는 픽셀 단위로 정확합니다. 둘째, 이러한 이미지는 브라우저에 의해 캐시될 수 있습니다. 세 번째로, 약간의 추가 작업으로 이미지를 교체하여 금속 타일과 같은 새로운 카드 디자인을 만들 수 있으며, 이 디자인 작업은 Flash 대신 Photoshop에서 할 수 있습니다.
단점은 무엇인가요? 이미지를 사용하면 텍스트 필드에 대한 프로그래매틱 액세스 권한을 포기하게 됩니다. 플래시에서는 유형의 색상이나 기타 속성을 변경하는 것이 간단한 작업이었습니다. HTML5에서는 이러한 속성이 이미지 자체에 베이킹됩니다. HTML 텍스트를 시도했지만 추가 마크업과 CSS가 많이 필요했습니다. 캔버스 텍스트도 시도했지만 브라우저마다 결과가 일관되지 않았습니다.)
퍼지 논리
어떤 크기의 브라우저 창이든 최대한 활용하고 스크롤을 피하고자 했습니다. 전체 게임이 벡터로 그려지고 화질을 손상시키지 않고도 크기를 조절할 수 있으므로 Flash에서는 비교적 간단한 작업이었습니다. 하지만 HTML에서는 더 까다로웠습니다. CSS 크기 조정을 사용해 보았지만 캔버스가 흐리게 표시되었습니다.
해결 방법은 사용자가 브라우저 크기를 조절할 때마다 게임 보드, 랙, 카드를 다시 그리는 것입니다.
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
어떤 화면 크기에서도 선명한 이미지와 만족스러운 레이아웃을 얻을 수 있습니다.
요점을 전달하세요
각 타일은 절대적으로 배치되며 게임 보드 및 랙과 정확하게 정렬되어야 하므로 안정적인 위치 지정 시스템이 필요합니다. 전역 공간(HTML 페이지)에서 요소의 위치를 관리하는 데 도움이 되는 두 가지 함수(Bounds
및 Point
)를 사용합니다. Bounds
는 페이지의 직사각형 영역을 나타내고 Point
는 페이지의 왼쪽 상단 모서리(0,0)를 기준으로 한 x,y 좌표를 나타냅니다(등록 지점이라고도 함).
Bounds
를 사용하면 두 직사각형 요소의 교차 (예: 카드가 랙을 지나는 경우) 또는 직사각형 영역 (예: 이중 문자 공백)에 임의의 점 (예: 카드의 중심점)이 포함되는지 여부를 감지할 수 있습니다. 다음은 Bounds의 구현입니다.
// bounds.js
function Bounds(element) {
var x = element.offsetLeft;
var y = element.offsetTop;
var w = element.offsetWidth;
var h = element.offsetHeight;
this.left = x;
this.right = x + w;
this.top = y;
this.bottom = y + h;
this.width = w;
this.height = h;
this.x = x;
this.y = y;
this.midx = x + (w / 2);
this.midy = y + (h / 2);
this.topleft = new Point(x, y);
this.topright = new Point(x + w, y);
this.bottomleft = new Point(x, y + h);
this.bottomright = new Point(x + w, y + h);
this.middle = new Point(x + (w / 2), y + (h / 2));
}
Bounds.prototype.contains = function (point) {
return point.x > this.left &&
point.x < this.right &&
point.y > this.top &&
point.y < this.bottom;
}
Bounds.prototype.intersects = function (bounds) {
return this.contains(bounds.topleft) ||
this.contains(bounds.topright) ||
this.contains(bounds.bottomleft) ||
this.contains(bounds.bottomright) ||
bounds.contains(this.topleft) ||
bounds.contains(this.topright) ||
bounds.contains(this.bottomleft) ||
bounds.contains(this.bottomright);
}
Bounds.prototype.toString = function () {
return [this.x, this.y, this.width, this.height].join(",");
}
Point
를 사용하여 페이지의 요소 또는 마우스 이벤트의 절대 좌표 (왼쪽 상단)를 결정합니다. Point
에는 애니메이션 효과를 만드는 데 필요한 거리 및 방향을 계산하는 메서드도 포함되어 있습니다. 다음은 Point
의 구현입니다.
// point.js
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.distance = function (point) {
var a = point.x - this.x;
var b = point.y - this.y;
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
Point.prototype.distanceX = function (point) {
return Math.abs(this.x - point.x);
}
Point.prototype.distanceY = function (point) {
return Math.abs(this.y - point.y);
}
Point.prototype.interpolate = function (point, pct) {
var x = this.x + ((point.x - this.x) * pct);
var y = this.y + ((point.y - this.y) * pct);
return new Point(x, y);
}
Point.prototype.offset = function (x, y) {
return new Point(this.x + x, this.y + y);
}
Point.prototype.vector = function (point) {
return new Point(point.x - this.x, point.y - this.y);
}
Point.prototype.toString = function () {
return this.x + "," + this.y;
}
// static
Point.fromElement = function (element) {
return new Point(element.offsetLeft, element.offsetTop);
}
// static
Point.fromEvent = function (evt) {
return new Point(evt.x || evt.clientX, evt.y || evt.clientY);
}
이러한 함수는 드래그 앤 드롭 및 애니메이션 기능의 기반을 형성합니다. 예를 들어 Bounds.intersects()
를 사용하여 카드가 게임보드의 공간과 겹치는지 확인하고, Point.vector()
를 사용하여 드래그된 카드의 방향을 확인하고, 타이머와 함께 Point.interpolate()
를 사용하여 모션 트윈 또는 이중 효과를 만듭니다.
흐름을 따르는 순응자
고정 크기 레이아웃은 Flash에서 더 쉽게 생성할 수 있지만 유동 레이아웃은 HTML 및 CSS 박스 모델을 사용하여 훨씬 더 쉽게 생성할 수 있습니다. 너비와 높이가 가변적인 다음 그리드 뷰를 생각해 보세요.
또는 채팅 패널을 사용해 보세요. Flash 버전에서는 마우스 작업에 응답하는 여러 이벤트 핸들러, 스크롤 가능한 영역의 마스크, 스크롤 위치를 계산하는 수학, 이를 함께 연결하는 기타 많은 코드가 필요했습니다.
반면 HTML 버전은 높이가 고정되고 오버플로 속성이 hidden으로 설정된 <div>
에 불과합니다. 스크롤에는 비용이 들지 않습니다.
이러한 경우(일반적인 레이아웃 작업) HTML과 CSS가 Flash보다 우수합니다.
지금 내 목소리 들리나요?
<audio>
태그가 문제가 되었습니다. 특정 브라우저에서 짧은 음향 효과를 반복해서 재생할 수 없었기 때문입니다. 두 가지 해결 방법을 시도했습니다. 먼저 사운드 파일을 더 길게 만들기 위해 무음 시간을 추가했습니다. 그런 다음 여러 오디오 채널에서 재생을 번갈아 시도했습니다. 두 기법 모두 완전히 효과적이거나 우아하지는 않았습니다.
결국 자체 Flash 오디오 플레이어를 출시하고 HTML5 오디오를 대체로 사용하기로 결정했습니다. 다음은 Flash의 기본 코드입니다.
var sounds = new Array();
function playSound(path:String):void {
var sound:Sound = sounds[path];
if (sound == null) {
sound = new Sound();
sound.addEventListener(Event.COMPLETE, function (evt:Event) {
sound.play();
});
sound.load(new URLRequest(path));
sounds[path] = sound;
}
else {
sound.play();
}
}
ExternalInterface.addCallback("playSound", playSound);
JavaScript에서 삽입된 Flash 플레이어를 감지하려고 시도합니다. 실패하면 각 사운드 파일에 대해 <audio>
노드를 만듭니다.
function play(String soundId) {
var src = "/audio/" + soundId + ".mp3";
// Flash
try {
var swf = window["swfplayer"] || document["swfplayer"];
swf.playSound(src);
}
// or HTML5 audio
catch (e) {
var sound = document.getElementById(soundId);
if (sound == null || sound == undefined) {
var sound = document.createElement("audio");
sound.id = soundId;
sound.src = src;
document.body.appendChild(sound);
}
sound.play();
}
}
이 방법은 MP3 파일에서만 작동합니다. YouTube는 OGG를 지원하지 않았습니다. 업계에서 조만간 단일 형식을 정할 수 있기를 바랍니다.
설문조사 위치
HTML5에서는 Flash에서와 동일한 기법을 사용하여 게임 상태를 새로고침합니다. 10초마다 클라이언트가 서버에 업데이트를 요청합니다. 마지막 폴 이후 게임 상태가 변경된 경우 클라이언트는 변경사항을 수신하고 처리합니다. 그렇지 않으면 아무 일도 일어나지 않습니다. 이 전통적인 폴링 기법은 그다지 우아하지는 않지만 허용됩니다. 하지만 게임이 발전하고 사용자가 네트워크를 통한 실시간 상호작용을 기대하게 되면 롱 폴링 또는 WebSockets로 전환하고자 합니다. 특히 WebSockets를 사용하면 게임 플레이를 개선할 수 있는 많은 기회가 있습니다.
정말 유용한 도구입니다.
Google 웹 도구 키트 (GWT)를 사용하여 프런트엔드 사용자 인터페이스와 백엔드 제어 로직 (인증, 유효성 검사, 지속성 등)을 모두 개발했습니다. JavaScript 자체는 Java 소스 코드에서 컴파일됩니다. 예를 들어 Point 함수는 Point.java
에서 조정됩니다.
package com.wordico.client.view.layout;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;
public class Point {
public double x;
public double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double distance(Point point) {
double a = point.x - this.x;
double b = point.y - this.y;
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
...
}
일부 UI 클래스에는 페이지 요소가 클래스 멤버에 '바인딩'된 상응하는 템플릿 파일이 있습니다. 예를 들어 ChatPanel.ui.xml
는 ChatPanel.java
에 상응합니다.
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder
xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
xmlns:w="urn:import:com.wordico.client.view.widget">
<g:HTMLPanel>
<div class="palette">
<g:ScrollPanel ui:field="messagesScroll">
<g:FlowPanel ui:field="messagesFlow"></g:FlowPanel>
</g:ScrollPanel>
<g:TextBox ui:field="chatInput"></g:TextBox>
</div>
</g:HTMLPanel>
</ui:UiBinder>
자세한 내용은 이 도움말의 범위를 벗어나지만 다음 HTML5 프로젝트에서는 GWT를 사용해 보시기 바랍니다.
Java를 사용하는 이유 먼저 엄격한 유형 지정을 살펴보겠습니다. 동적 유형 지정은 JavaScript에서 유용합니다(예: 배열이 다양한 유형의 값을 보유하는 기능). 하지만 크고 복잡한 프로젝트에서는 골칫거리가 될 수 있습니다. 두 번째는 리팩터링 기능입니다. 수천 줄의 코드에서 JavaScript 메서드 서명을 변경하는 방법을 생각해 보세요. 쉽지 않습니다. 하지만 우수한 Java IDE를 사용하면 간단합니다. 마지막으로 테스트 목적으로 Java 클래스의 단위 테스트를 작성하는 것이 오래된 '저장 및 새로고침' 기법보다 낫습니다.
요약
오디오 문제를 제외하고 HTML5는 기대치를 크게 뛰어넘었습니다. Wordico는 플래시에서와 마찬가지로 보기 좋을 뿐만 아니라 유연하고 반응도 빠릅니다. Canvas와 CSS3가 없었다면 불가능했을 것입니다. 다음 과제는 Wordico를 모바일용으로 조정하는 것입니다.