사례 연구 - Wordico를 Flash에서 HTML5로 변환

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

소개

Wordico 십자말풀이 게임을 플래시에서 HTML5로 변환하면서 가장 먼저 해야 할 일은 브라우저에서 풍부한 사용자 환경을 만드는 데 알고 있던 모든 것을 아는 것이었습니다. Flash가 애플리케이션 개발의 모든 측면(벡터 그리기에서 다각형 적중 감지, XML 파싱에 이르기까지)을 위한 포괄적인 단일 API를 제공했지만 HTML5는 다양한 브라우저 지원과 함께 다양한 사양을 제공했습니다. 또한, 우리는 HTML(문서별 언어)과 박스 중심 언어인 CSS가 게임 빌드에 적합한지도 궁금했습니다. 게임이 플래시에서처럼 모든 브라우저에서 균일하게 표시되고 제대로 보이고 동작할까요? Wordico의 답변은 였습니다.

빅터님, 어떤 벡터가 있나요?

우리는 선, 곡선, 채우기, 그라데이션 등 벡터 그래픽만을 사용하여 Wordico의 최초 버전을 개발했습니다. 그 결과, 매우 작고 무한한 확장성을 얻을 수 있었습니다.

Wordico 와이어프레임
플래시에서 모든 표시 객체는 벡터 도형으로 이루어져 있습니다.

또한 플래시 타임라인을 활용하여 여러 상태를 가진 객체를 만들었습니다. 예를 들어 Space 객체에 이름이 지정된 9개의 키프레임을 사용했습니다.

플래시의 3자리 문자 공간입니다.
플래시의 3글자 공백

하지만 HTML5에서는 비트맵 스프라이트를 사용합니다.

9개의 공간이 모두 표시된 PNG 스프라이트
9개의 공백을 모두 보여주는 PNG 스프라이트

개별 스페이스에서 15x15 게임보드를 만들기 위해 225자 문자열 표기법을 반복합니다. 문자열 표기법에서는 각 공백을 서로 다른 문자로 표시합니다 (예: 3자리 문자를 't', 삼중 단어를 나타내는 'T'). 플래시에서는 간단한 작업이었습니다. 공백을 스탬프 처리한 다음 그리드 형식으로 정렬했습니다.

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 그림자가 있습니다.

HTML5에서 게임보드는 단일 캔버스 요소입니다.
HTML5에서 게임보드는 단일 캔버스 요소입니다.

타일 객체 변환도 비슷한 연습이었습니다. 플래시에서는 텍스트 필드와 벡터 도형을 사용했습니다.

플래시 타일은 텍스트 필드와 벡터 도형의 조합이었습니다.
플래시 타일은 텍스트 필드와 벡터 도형의 조합이었습니다.

HTML5에서는 런타임 시 단일 <canvas> 요소에 세 개의 이미지 스프라이트를 결합합니다.

HTML 타일은 3개의 이미지가 합성된 것입니다.
HTML 타일은 3개의 이미지로 구성되어 있습니다.

이제 100개의 캔버스 (각 타일당 1개)와 게임보드용 캔버스가 있습니다. 다음은 '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에서 수행할 수 있습니다.

단점은 무엇인가요? Google은 이미지를 사용하여 텍스트 입력란에 프로그래밍 방식으로 액세스하는 것을 허용합니다. 플래시에서 유형의 색상이나 기타 속성을 변경하는 것은 간단한 작업이었습니다. HTML5에서는 이러한 속성이 이미지 자체에 결합됩니다. HTML 텍스트를 사용해 봤지만 많은 추가 마크업과 CSS가 필요했습니다. 캔버스 텍스트도 사용해 보았지만 브라우저 간에 결과가 일관되지 않았습니다.)

퍼지 로직

모든 크기에서 브라우저 창을 최대한 활용하고 스크롤을 피하고자 했습니다. 이는 플래시에서 비교적 간단한 작업이었습니다. 전체 게임이 벡터로 그려졌고, 충실도를 유지하면서 확장하거나 축소할 수 있었기 때문입니다. 하지만 HTML에서는 더 까다로웠습니다. CSS 크기 조정을 사용해 보았지만 캔버스가 흐리게 처리되었습니다.

CSS 크기 조정 (왼쪽)과 다시 그리기 (오른쪽) 비교
CSS 크기 조정 (왼쪽)과 다시 그리기 (오른쪽) 비교

해결 방법은 사용자가 브라우저의 크기를 변경할 때마다 게임보드, 랙, 타일을 다시 그리는 것입니다.

window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);

...
rack.setConstraints(rackWidth, rackHeight);

...
tileManager.resizeTiles(tileSize);
});

모든 화면 크기에서 선명한 이미지와 멋진 레이아웃을 제공합니다.

게임보드가 세로 공간을 채우고 게임보드 주위에 다른 페이지 요소가 흐릅니다.
게임보드가 세로 공간을 채우고 게임보드 주변에 다른 페이지 요소가 흐릅니다.

핵심 내용 전달

각 타일은 절대적으로 배치되고 게임보드 및 랙과 정확하게 정렬되어야 하므로 안정적인 위치 지정 시스템이 필요합니다. BoundsPoint, 두 가지 함수를 사용하여 전역 공간 (HTML 페이지)에서 요소의 위치를 관리합니다. Bounds은 페이지의 직사각형 영역을 나타내고 Point는 페이지 왼쪽 상단(0,0)을 기준으로 한 x,y 좌표를 나타냅니다(등록 지점이라고도 함).

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 &amp;&amp;
point.x < this.right &amp;&amp;
point.y > this.top &amp;&amp;
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()를 타이머와 함께 사용하여 모션 트윈 또는 이징 효과를 만듭니다.

흐름을 따르는 순응자

플래시에서는 고정 크기 레이아웃을 만들기가 더 쉽지만, HTML 및 CSS 상자 모델을 사용하면 유동 레이아웃을 만들기가 훨씬 더 쉽습니다. 가변 너비와 높이가 있는 다음과 같은 그리드 뷰를 고려하세요.

이 레이아웃에는 고정된 크기가 없습니다. 썸네일은 왼쪽에서 오른쪽, 위에서 아래로 흐릅니다.
이 레이아웃에는 고정된 크기가 없습니다. 썸네일은 왼쪽에서 오른쪽, 위에서 아래로 흐릅니다.

또는 채팅 패널을 고려해 보세요. Flash 버전에서는 마우스 동작에 응답하는 여러 이벤트 핸들러, 스크롤 가능한 영역의 마스크, 스크롤 위치 계산을 위한 수학, 그리고 이를 함께 결합하기 위한 많은 다른 코드가 필요했습니다.

플래시의 채팅 패널은 예쁘지만 복잡했습니다.
플래시의 채팅 패널은 꽤 복잡했습니다.

이에 비해 HTML 버전은 높이가 고정되고 오버플로 속성이 hidden으로 설정된 <div>에 불과합니다. 스크롤에는 비용이 들지 않습니다.

작동하는 CSS 상자 모델입니다.
CSS 박스 모델

일반 레이아웃 작업 - HTML 및 CSS가 플래시를 돋보이게 하는 경우

이제 내 말이 들리나요?

<audio> 태그 문제가 발생했습니다. 특정 브라우저에서 짧은 사운드 효과를 반복적으로 재생할 수 없었을 뿐입니다. 두 가지 해결 방법을 시도했습니다. 먼저, 사운드 파일을 길게 만들기 위해 사운드 파일을 공기로 패딩 처리했습니다. 그런 다음 여러 오디오 채널을 번갈아 재생해 보았습니다. 두 기법 모두 완전히 효과적이거나 우아하지는 않았습니다.

결국 Google은 자체 Flash 오디오 플레이어를 출시하고 HTML5 오디오를 대체 수단으로 사용하기로 결정했습니다. 플래시의 기본 코드는 다음과 같습니다.

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 Player를 감지하려고 시도합니다. 이 작업이 실패하면 각 사운드 파일마다 <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 파일에서만 작동하며 OGG는 지원하지 않습니다. 머지않아 업계가 하나의 형식에 안착하게 되기를 바랍니다.

설문조사 위치

플래시에서와 동일한 기술을 사용하여 HTML5에서도 게임 상태를 새로고침합니다. 10초마다 클라이언트가 서버에 업데이트를 요청합니다. 마지막 폴 이후 게임 상태가 변경되었다면 클라이언트가 변경사항을 수신하여 처리합니다. 그 외의 경우에는 아무 일도 일어나지 않습니다. 이러한 전통적인 폴링 기법은 사용 가능하지만 매우 우아하지 않습니다. 하지만 게임이 발전하고 사용자가 네트워크를 통한 실시간 상호작용을 기대하게 됨에 따라 긴 폴링 또는 WebSockets로 전환하려고 합니다. 특히 WebSocket은 게임 플레이를 개선할 많은 기회를 제공합니다.

정말 유용한 도구입니다.

Google에서는 Google 웹 툴킷 (GWT)을 사용하여 프런트엔드 사용자 인터페이스와 백엔드 제어 로직 (인증, 유효성 검사, 지속성 등)을 모두 개발했습니다. 자바스크립트 자체는 자바 소스 코드에서 컴파일됩니다. 예를 들어 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.xmlChatPanel.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를 모바일 사용에 맞게 조정하는 것입니다.