札幌 "9leap" HTML5/enchant.js ゲーム開発ハッカソンに参加しました

昨日になりますが、札幌 "9leap" HTML5/enchant.js ゲーム開発ハッカソンに参加してきました。enchant.jsというのはJavaScriptのライブラリで、HTML5を使ったゲームを作るための基盤を提供するものです。今回は、そのイベントとして札幌開催となり、ゲーム系の開発者が多く集まっていました。午前中は2本の講演で、午後からはハッカソンです。

講演

今回は、初音ミクでお馴染み、北海道の誇るクリプトンフューチャーメディアさんが協賛していた事もあり、クリプトンの杉本さんが「中国のゲーム事情」について話されました。ざっくりと言えば、中国ではまさにMMOが主流で成長市場な状況で、ユーザ数も多いですよ、という話です。自分はMMOとかはもはや興味ないわけですが、どうもビジネスモデル的に嫌いな所もあり、興味は沸きませんね…。ただ、あのパワーと勢いは流石中国って感じでした。
後半はHTML5とenchant.js について、ユビキタスエンターテインメントの清水さんのお話です。ゲーム開発をやっているワケについて最初に話されていましたが、なかなか興味深いものでした。いわゆるウェブアプリなんかに比べたらゲームアプリの開発の方が何倍も面白いという理由をあげています。

確かに、いわゆる業務アプリを作っているだけのPGには無縁のものも多くあります。オブジェクト指向とかユーザインターフェイスなどは意識すればいくらでもできますが、それでもゲームの方が面白く工夫出来る部分ですね。確かに手強い所もありますが、だからこそ面白いってのはあると思います。ちなみに自分はディスクトップアプリが好きで、よくSwingとか書いているわけですが、ゲームほどではないにせよ、同じような面白さがあると思います。そして、何より自分で作って動かして、他の人に遊んで貰ってという本質的な楽しさがゲーム開発にはあると話していました。そうですよね・・・誰も喜ばない業務システムなんて本当は作りたくないです。
あとはHTML5スマートフォンなどの話、enchant.js や 9leapについて話されていましたが、enchant.js の目指しているところは共感できました。それは「若い世代にN88 Basicのような感覚で、ゲームを作って楽しんで欲しい」という事かと思います。プログラミングは面白いのですが、それは何かを作って、それを動かして、フィードバックを得られるから面白いのです。昨今はプログラミング言語も進化し、アプリケーションも進化しています。また、家庭用ゲーム機が普及してしまい、自分でゲームを作らなくても誰かが用意してくれる時代です。でも、やっぱり作る面白さは自分で作って自分で遊ぶところが原点なんですよね。そのままゲーム業界に入らなくとも、物作りの原点という意味ではゲームって凄く良い題材なんだと思います。
今回やってみて解ったのは、HTML5/JavaScriptは、物作りに興味があってプログラムが初めての人が少しずつ楽しみながら学べるというコンテキストにおいて、非常に有効だなと思いました。ただ、それだけだとハードルが高すぎるので、最低限のフレームワークを用意しようというのがenchant.jsなワケです。これは新人教育とか夏休みの宿題的な意味での中高生向けのワークショップとかに最適かと思います。そんな機会があれば推しまくるかなー。

ハッカソン

久しぶりにJavaScriptを触りましたが、3時間弱でなんとか遊べる物はできた感じです。流石に暑すぎて2時間半くらいで集中力は尽きてしまいましたがw
自分が作ったのはこちら。ミクさんの素材が提供されていたので、4方向から現れるミクさんをカウントするだけのゲームです。時間内の出現数とカウント数が一致すれば勝ち。本当はレベルの選択やレベルアップなどの要素、変な動きをするパターンなどまでやりたかったのですが、時間も少なかったので断念。ソースはこんな感じです。久しぶりにエディタのみでプログラム書いた気がします。

enchant();
// game data
var level = 4;
var WIDTH = 320;
var HEIGHT = 320;
var MAX_MIKU_COUNT = 10 + level;
var OUT_OF_FRAME = -100;
var TIME = 600;
// Rand
var Rand = Class.create({
	getInt: function(max) {
		return Math.floor(Math.random() * (max + 1));
	},
	range: function(min, max) {
		return min + Math.floor(Math.random() * (max - min + 1));
	},
});
var rand = new Rand();

// Strategy
var NullStrategy = Class.create({
	initialize: function() {
	},
	move: function(sprite) {
		sprite.x = OUT_OF_FRAME;
	}
});
var LinerMoveStrategy = Class.create({
	initialize: function(dx, dy, initX, initY) {
		this.dx = dx;
		this.dy = dy;
		this.initX = initX;
		this.initY = initY;
	},
	move: function(sprite) {
		sprite.x += this.dx;
		sprite.y += this.dy;
	}
});
var MoveAndReturnStrategy = Class.create({
	initialize: function(dx, dy, initX, initY) {
		this.dx = dx;
		this.dy = dy;
		this.initX = initX;
		this.initY = initY;
	},
	move: function(sprite) {
		if (this.initX + WIDTH / 2 < sprite.x) {
			this.dx = -1 * this.dx;
		}
		sprite.x += this.dx;
		sprite.y += this.dy;
	}
});
var createStrategy = function() {
	var dx = rand.range(2, 5);
	var dy = rand.range(2, 5);
	switch (rand.getInt(level) + 1) {
		case 1:
			return new LinerMoveStrategy(dx, dy, 0, 0);
		case 2:
			return new LinerMoveStrategy(-1 * dx, dy, WIDTH, 0);
		case 3:
			return new LinerMoveStrategy(dx, -1 * dy, 0, HEIGHT);
		case 4:
			return new LinerMoveStrategy(-1 * dx, -1 * dy, WIDTH, HEIGHT);
		default:
			return new MoveAndReturnStrategy(dx, dy, 0, 0);
	}
};

// Character class
var Miku = Class.create(Sprite, {
	initialize: function(game) {
		Sprite.call(this, 44, 32);
		this.image = game.assets['miku.gif'];
		this.x = OUT_OF_FRAME;
		this.strategy = new NullStrategy();
		this.frame = 1;
		this.addEventListener('enterframe', function(evt){
			this.frame += 1;
			if (this.frame == 3) this.frame = 1;
			this.strategy.move(this);
		});
	},
	isActive: function() {
		if (this.x < 0 || WIDTH < this.x) return false;
		if (this.y < 0 || HEIGHT < this.y) return false;
		return true;
	}
 });
window.onload = function() {
	// setup
	var game = new Game(WIDTH, HEIGHT);
	game.preload('miku.gif');
	// 
	game.onload = function() {
		var userCount = 0;
		var count = 0;
		var mikus = new Array(MAX_MIKU_COUNT);
		for (i = 0; i < MAX_MIKU_COUNT; i++) {
			mikus[i] = new Miku(game);
			game.rootScene.addChild(mikus[i]);
		}
		// Time Label
		var timeLabel = new Label("みくさん1人に付き1回Downボタンを押してね!");
		timeLabel.x = 20;
		timeLabel.y = HEIGHT - 20;
		game.rootScene.addChild(timeLabel);
		// Count Label
		var countLabel = new Label("0 miku");
		countLabel.x = WIDTH - 60;
		countLabel.y = 0;
		game.rootScene.addChild(countLabel);
		var end = function() {
			var message = new Label();
			if (userCount == count) {
				message.text = "すばらしい!";
			} else if(userCount < count) {
				message.text = (count - userCount) + "miku足りませんよ。";
			} else {
				message.text = (userCount - count) + "miku多すぎます><";
			}
			message.x = 50;
			message.y = HEIGHT / 2;
			game.rootScene.addChild(message);
			timeLabel.text = "";
			game.stop();
		};
		game.rootScene.addEventListener('enterframe', function(evt) {
			var time = (TIME - game.frame);
			if (time < 0) { // Game end.
				end();
				return;
			}
			timeLabel.text = "Time: " + time;
			if (time < 50) return;
			for (i = 0; i < MAX_MIKU_COUNT; i++) {
				if (!mikus[i].isActive() && rand.getInt(100) == 0) {
					mikus[i].strategy = createStrategy();
					mikus[i].x = mikus[i].strategy.initX;
					mikus[i].y = mikus[i].strategy.initY;
					count++;
				}
			}
		});
		game.rootScene.addEventListener('downbuttondown', function(evt) {
			userCount += 1;
			countLabel.text = userCount + " miku";
		});
	}
	game.start();
};