Unityで開発したマリオ風ゲーム「THE HALSTAR ACTION」
前回・前々回の記事でも紹介した、今回のキャンペーン問題「HTML5+JSで落ちものゲームを作ってみよう」と、「きゃんちのジグソーパズルアプリを作ってみよう」にチャレンジするために、日本マイクロソフトを久しぶりに訪問したシュン君。
▲ジグソーパズルアプリを作ってみよう⇒ ▲HTML5+JSで落ちものゲームを作ってみよう⇒
実はプログラマとして成長した姿を物江修さんや高橋忍さんはもちろん、もし会うことができれば砂金信一郎さんや澤円さんに見てもらえたらと、自作のアプリを持参してきたのだ。
シュン:マリオみたいなゲームを作ったんです。これは自信作。見た目はかわいくないんですけど。
そう言って、シュン君が披露したのは「THE HALSTAR ACTION(ハルスターアクション)」。主人公がジャンプして障害物を避けたり、次のルートに進んだり、エネルギーを受け取ったりしてゴールと得点を目指していくゲームだ。
披露されたゲームを観た一同からは「おっ、すごい」という声が…。
物江:これはUnityで開発したの?
シュン:そう。開発環境はUnity、言語はC#で開発しました。すごくこだわっているところがあるんです。
シュン君がこだわったのは、一つひとつのシーンをUnityで作るのではなく、ステージをWeb上からダウンロードするようにしているところ。
シュン:プログラミングをしたことがない友達とも一緒にステージ作りを楽しめるように、ステージ生成装置も作ったんです。
それがUnity2D向けのステージ自動生成言語「PLANET2.0.0(プラネット)」だ。
物江:それはすごいね。編集機能も持っているんだ。
と、感心した様子の物江さん。
これには理由がある。このゲームは友達2人と一緒に作っているのだが、プログラミングができるのはシュン君のみ。
だからこのようなステージ生成装置を使って、プログラミングをすることなくコースを編集できるようにシュン君はPLANETを使って友達2人と「こういう風にするとさらに面白くなるのでは」とアイデアを出し合いながら、夢中になってゲーム作りを楽しんでいるのだそう。
シュン:ぼくたち3人は中学から違う学校に通っているので、学校でゲームのことを話せないんですよね。だから週1回の割合でぼくの家に集まってゲーム作りをしているんです。一人は、ステージ構成のアイデアがすごくて、もうひとりは、イラストのデザインと音楽作りがすごいんです。それを出し合って、しゃべりまくって作るのが、楽しすぎます!
その説明を聞いて高橋さんは、シュン君の成長ぶりに頼もしそうな顔を向けた。
高橋:自分でカスタマイズしたゲームで遊べるんだ。これはもうプロの手法だよね。例えば最近の有名なRPGゲームなども自社でそれ開発ツールを作って、フィールドを作る人、キャラを作る人という様な分業を可能にしました。シュン君の手法はまさにそれ。こういう作り方ができるのはプログラマとして2歩も3歩も先に進んでいる。格段にステップアップしているね。
すでに本格的な作り方をしている「THE HALSTAR ACTION」。キャラクターデザインや背景の絵などはどうやって作っているのかも気になるところ。
物江:デザインはどうやって作っているの?
シュン:PowerPoint(パワーポイント)です。
物江:パワポの発想はなかったな。自社にいながら、パワポでこんなところまでできるなんて知らなかったよ。
と、シュン君のパワーポイントの活用ぶりに驚きを隠せないでいた。
それから、WebGL(Three.js)を使った3Dゲームも物江さんや高橋さんに披露した。
物江:Oculus Riftで観たらすごいんじゃない。持ってくればよかったな~。
その言葉を聞いたシュン君はすかさず、
シュン:Google Cardboardを持ってきたので、ぜひ、のぞいてください。
と、持参したGoogle Cardboardの内部にスマートフォンをセットし、Chromeリモート・デスクトップでスマートフォン(スマホ)の画面に動画を映し出した。
物江:スマホを使うのはすごくいいアイデアだよね。スマホにはジャイロセンサーや電子コンパスが搭載されているので、振り向いたときの風景なども観ることができるようになるからね。
高橋:そう。ちょっと上を向いているとか下を向いているとか、そういうことがわかるので面白いよね。
物江:Windows10に搭載される新ブラウザ(開発コードネーム:Spartan)では、asm.js(JavaScriptを高速で実行するサブセット)をサポートするから、Unityからそのまま出せるよ。
シュン:本当!?そっか、Windows10が出るのが楽しみです。
とにっこりするシュン君。
マイクロソフトのデータセンターのセキュアさにビックリ
シュン君が作ったゲームでかなり盛り上がり、訪問から約2時間近くが経過。そろそろお別れの時間が近づいてきた。
物江さん、高橋さんからは、「プログラマとしてかなり成長したシュン君にとって、今回のキャンペーン問題は簡単かもしれないけど、いろいろアレンジできるので、ぜひ、自分なりに工夫してみて応募してね」と伝えられる。
最後に澤さんが、「シュン君も使っているAzureなどのクラウドサービスの鍵を握るのがデータセンター。マイクロソフトはソフトウェアの会社と思われているけど、クラウドサービスをする上で非常に高いセキュリティを保っているんだよ」と、シュン君をデータセンターの技術が紹介されているコーナーへと誘った。
そして目的のマイクロソフトのデータセンターのコーナーへ。澤さんはシュン君にマイクロソフトのデータセンターがどれだけセキュアにできているかを解説。
建物の免震性や耐震性はもちろんのこと、物理的なテロに対しても万全の仕組みを強いていることなど、あらゆる脅威に対して考えられる対策を施していることをシュン君に説明した。
また通常、データセンターの場所は明らかにされていないことが多いが、マイクロソフトでは顧客が要望した場合、現地を案内するという。
澤:実は案内する人にはある条件があるんだよ。何だと思う?
シュン:うーん、技術を知っていることかな。
澤:もちろん、技術的に解説できることもそうなんだけど、必要に応じて人を取り押さえられることなんだよね。
それにはシュン君もビックリした様子。
シュン:そっか。万一、見学者がテロリストだったら、捕まえないといけないからか。
と納得し、このような数々のセキュアな仕組みに「未来のことまでも考えているのがすごいですね」と感心することしきりだった。
「ゲームクリア」とクリア時間が表示されるアレンジを追加
このようにマイクロソフトのエバンジェリストたちと楽しい再会をしたシュン君は、自宅に戻り、早速高橋さんと物江さんの問題にもチャレンジしたのだそう。そして送ってくれたのが次の作品だ。シュン君なりのアレンジが施された作品となっている。
まず高橋さん出題の「ジグゾーパズルアプリ」。本来は画像をきゃんち画像にして、スタートボタンが機能するようになれば出来上がりだが、シュン君はすべてパーツをつなぐと、「ゲームクリア!」となり、クリア時間が表示されるようにアレンジ。
{
/// <summary>
/// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
/// </summary>
///
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
static int piecesize = 200; //ピースの1辺の長さ
int xpieces, ypieces; //元絵からの分割数
piece[,] pieces;
Random rnd = new Random();
DispatcherTimer timer = new DispatcherTimer(); // 時間計測
private async void Button_Click(object sender, RoutedEventArgs e)
{
// ここに await StartPuzzle(); と入力します。
await StartPuzzle();
}
private async System.Threading.Tasks.Task StartPuzzle()
{
// ピースを分割する数を決定し、正解となる位置関係を保存しておく配列を作成
//////////////////////////////////////////////////////////////////////////////
xpieces = (int)imgGrid.ActualWidth / piecesize;
ypieces = (int)imgGrid.ActualHeight / piecesize;
pieces = new piece[xpieces, ypieces];
//目隠し表示
waitpanel.Visibility = Windows.UI.Xaml.Visibility.Visible;
imgGrid.Visibility = Windows.UI.Xaml.Visibility.Visible;
grid1.Children.Clear();
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// ⓪ ピースの作成と配置 (初期化処理)
// 元画像を分割して、ピースを動的生成して描画し、最後に画面にランダムに配置する
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
for (int x = 0; x < xpieces; x++)
for (int y = 0; y < ypieces; y++)
{
//ピースオブジェクトを動的に作成して、設定をする
////////////////////////////////////////////////////////////////////////////////
piece _piece = new piece(x, y)
{
// 切り取ったピース画像を貼り付け
//imgsrc = renderTargetBitmap,
Width = piecesize,
Height = piecesize,
VerticalAlignment = Windows.UI.Xaml.VerticalAlignment.Top,
HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Left,
//ランダムに配置
Margin = getrandamposition()
};
//ピースの形を設定する
Rect r = _piece.setshape(xpieces - 1, ypieces - 1);
//元の絵をピースに合わせて切り取り、イメージブラシにしておく
///////////////////////////////////////////////////////////////////////////////////
RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap();
//RectangleGeometry rect = new RectangleGeometry() { Rect = new Rect(x * piecesize -28, y * piecesize -28, piecesize + 34, piecesize + 34) };
RectangleGeometry rect = new RectangleGeometry() { Rect = new Rect(x * piecesize + r.Left, y * piecesize + r.Top, r.Width, r.Height) };
SourceImage.Clip = rect;
await renderTargetBitmap.RenderAsync(imgGrid);
_piece.Background = new ImageBrush() { ImageSource = renderTargetBitmap };
// 動的生成したピースのイベント処理を設定しておく
/////////////////////////////////////////////////////////////////////////
_piece.PointerPressed += _piece_PointerPressed;
_piece.PointerMoved += _piece_PointerMoved;
_piece.PointerReleased += _piece_PointerReleased;
_piece.PointerExited += _piece_PointerExited;
// 動的生成したピースを画面上に追加する
//////////////////////////////////////////////////
grid1.Children.Add(_piece);
pieces[x, y] = _piece;
}
//目隠し終了
startpanel.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
waitpanel.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
// タイマー
timer.Tick += timer_Tick;
timer.Interval = TimeSpan.FromSeconds(1);
timer.Start();
}
int Sec = 0;
void timer_Tick(object sender, object e)
{
Sec++;
time.Text = "Time: " + Sec.ToString();
}
Point start; //タッチ開始地点
Boolean bmove; //移動中フラグ
//ピースのランダム位置を生成する
private Thickness getrandamposition()
{
return new Thickness(
rnd.Next((int)(grid1.ActualWidth - piecesize)),
rnd.Next((int)(grid1.ActualHeight - piecesize)),
0, 0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// ① 移動ピースの選択
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void _piece_PointerPressed(object sender, PointerRoutedEventArgs e)
{
bmove = true; //タッチによる移動開始フラグ
piece ps = (piece)sender; //移動ピース
ps.CapturePointer(e.Pointer); //ピースがマウスの追従から外れないようにする処理
//最前面に移動させる
if (!ps.bGroup)
{
// タッチしたピースが単一ピースの場合はピースを最前面に出す
///////////////////////////////////////////////////////////////
grid1.Children.Remove(ps);
grid1.Children.Add(ps);
//ちょっと浮かせるアニメーション
//((piece)sender).up();
}
else
{
// タッチしたピースがグループ化されている場合は、グループを最前面に移動させる
//////////////////////////////////////////////////////////////////////////////////
Grid g = (Grid)(ps.Parent);
grid1.Children.Remove(g);
grid1.UpdateLayout();
grid1.Children.Add(g);
}
//タッチした時点での位置を覚えておく。移動したときの移動量を計測するため
//////////////////////////////////////////////////////////////////////////////
start = e.GetCurrentPoint(grid1).Position;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// ② ピースの移動
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void _piece_PointerMoved(object sender, PointerRoutedEventArgs e)
{
piece ps = (piece)sender; //移動ピース
//移動中ではない(マウスホバー)時は何も処理をしない
///////////////////////////////////////////////////////////////////////////
if (bmove == false) return;
// まずは移動量を計算
Point p = e.GetCurrentPoint(grid1).Position;
double dx = (p.X - start.X);
double dy = (p.Y - start.Y);
//現在の位置(Margin)に初期位置からの移動分を追加する=移動させる
///////////////////////////////////////////////////////////////////////////
if (!ps.bGroup)
{
// 移動中のピースが単一ピースの場合
/////////////////////////////////////////////////////
Thickness margin = ps.Margin;
margin.Left += dx;
margin.Top += dy;
ps.Margin = margin;
}
else
{
// 移動中のピースがグループ化されている場合は、グループ内の全ピースを移動させる
//////////////////////////////////////////////////////////////////////////////////
Grid g = (Grid)(ps.Parent);
foreach (piece item in g.Children)
{
Thickness margin = (item).Margin;
margin.Left += dx;
margin.Top += dy;
item.Margin = margin;
}
}
//現在の位置を初期位置に設定
start = p;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ③a ピースがマウスから外れたとき
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void _piece_PointerExited(object sender, PointerRoutedEventArgs e)
{
bmove = false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// ③b ピースの移動後の処理(接続判定)
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void _piece_PointerReleased(object sender, PointerRoutedEventArgs e)
{
bmove = false; //移動中フラグOFF
piece ps = (piece)sender; //移動ピース
ps.ReleasePointerCapture(e.Pointer); //ピースがマウスの追従から外れないようにする処理の解除
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ピースの接続判定
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//本来左に来るべきピースが近くにあるかどうかの判定
if (ps.x != 0)
if (CheckAndCombine(ps, -1, 0)) return;
//本来右に来るべきピースが近くにあるかどうかの判定
if (ps.x < xpieces - 1)
if (CheckAndCombine(ps, 1, 0)) return;
//本来上に来るべきピースが近くにあるかどうかの判定
if (ps.y != 0 )
if (CheckAndCombine(ps, 0, -1)) return;
//本来下に来るべきピースが近くにあるかどうかの判定
if (ps.y < ypieces - 1)
if (CheckAndCombine(ps, 0, 1)) return;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ピースの隣接判定と隣接している場合は接続する
////////////////////////////////////////////////////////////////////////////////////////////////////////////
private Boolean CheckAndCombine(piece ps, int cx, int cy)
{
Boolean bSnap = false; //グループ化したかどうかの判定(戻り値)
piece pd = pieces[ps.x + cx, ps.y + cy]; //接続相手候補
//いまう誤解しているピース(ps)の位置と、比較するピース(pd)の位置の差を計測
double dx = pd.px - ps.px;
double dy = pd.py - ps.py;
//ピースのはまる正解場所近く(ピースの半分サイズのずれ)であれば正解として、ピースをはめる
if ((dx > piecesize * (-0.5 + cx) && dx < piecesize * (0.5 + cx)) &&
(dy > piecesize * (-0.5 + cy) && dy < piecesize * (0.5 + cy))
&& (!ps.bGroup || pd.bGroup) && (pd.Parent != ps.Parent || ps.Parent == grid1))
//近くにある場合はスナップ移動してグループ化をする
bSnap = Grouping(ps, pd, pd.px - piecesize * cx - ps.px, pd.py - piecesize * cy - ps.py);
// クリアしたか
if (grid1.Children.Count == 1)
{
clearpanel.Visibility = Windows.UI.Xaml.Visibility.Visible;
timeClear.Text = "Time: " + Sec;
timer.Stop();
time.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
}
return bSnap;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ④b ピースの接続処理(グループ化)
////////////////////////////////////////////////////////////////////////////////////////////////////////////
private Boolean Grouping(piece ps, piece pd, double dx, double dy)
{
if (!pd.bGroup)
{
//1つのピースどうしが並んで接続する場合
///////////////////////////////////////////////////////////
ps.px += dx;
ps.py += dy;
//ps.noline();
//pd.noline();
Grid g = new Grid();
grid1.Children.Remove(ps);
grid1.Children.Remove(pd);
g.Children.Add(ps);
g.Children.Add(pd);
grid1.Children.Add(g);
pd.bGroup = true;
}
else
{
if (!ps.bGroup)
{
//1つのピースをグループ化されたピースに接続する場合
///////////////////////////////////////////////////////////
ps.px += dx;
ps.py += dy;
//ps.noline();
grid1.Children.Remove(ps);
((Grid)pd.Parent).Children.Add(ps);
}
else
{
//グループ化されたピース群を他のグループ化されたピースに接続する場合
////////////////////////////////////////////////////////////////////////
Grid g = (Grid)(ps.Parent); // 移動元のグループ
while (g.Children.Count > 0)
{
// 移動元のピースをグループから外して移動先のピースに追加する
//////////////////////////////////////////////////////////////////
piece item = (piece)(g.Children.First());
item.px += dx;
item.py += dy;
g.Children.Remove(item);
((Grid)(pd.Parent)).Children.Add(item);
}
// 移動移動が完了したらグループは削除
grid1.Children.Remove(g);
}
}
ps.bGroup = true;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ⑤ ピースの再配置
////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void redraw()
{
foreach(object item in grid1.Children)
{
if( item.GetType() == typeof(piece))
{
grid1.Children.Remove((piece)item);
((piece)item).Margin = getrandamposition();
grid1.Children.Add((piece)item);
}
else
{
((Grid)item).Margin = getrandamposition();
}
}
grid1.InvalidateArrange();
}
private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.ActualHeight > 0) redraw();
}
private void grid1_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
redraw();
}
}
}
このアレンジに対し、高橋さんは次のようにコメントを寄せてくれた。
高橋:タイマーとクリア処理入れてきましたね。クリア処理の判定は完璧です。きちんとプログラムが読めていて、UIオブジェクトの構造がちゃんと理解できてないとできない処理なので、本当に素晴らしい。
人が作った処理を読むのは、自分でコードを描くよりも何倍も難しいですが、完璧に読めていますね。さて、処理は100点、そしてゲームの機能としては……残念ながら60点。
なぜでしょう。ジグソーパズルの楽しみは2つあります。1つはパズルを解いていくこと。そして、もう一つは完成したパズルを眺めること。
パズルを飾っておく人もたくさんいるので、パズルを眺める方が楽しみとしては大きいかもしれません。しかし、今の処理ではようやく完成し、ゆっくり眺めようとした瞬間にクリア画面が出てしまいます。これでは楽しむこともできませんし、もしかしたらきゃんちが好きな人は怒って2度と遊んでくれないかもしれません。
ではどうすれば良いのか。例えばクリアしたら画面の真ん中に『クリア!』の文字が出て、しばらくすると消える、という処理にした方がいいかもしれません。クリア画面の背景を半透明の水色にして、少し見えるようにして、さらにクリア画面をタップするとクリア画面が消えるとか。
ぜひ、今度機能を追加するようなことがあるときは、「遊ぶ人にとって、この方法が一番いいのかな?」というのを考えてみてください。ちょっと、レベルが高いかもしれませんが、シュン君のプログラミングレベルは十分高いので、次はこういったところにも注意するとよりよいプログラマになれると思います。好きな画像にして、ピースを小さくしてレベルを上げるとか、興味があればぜひまた楽しんでくださいね。
「落ちモノゲーム」は爆弾に当たるとゲームオーバーになるようアレンジ
続いて、物江さんが出題した雪だるまが空から降る雪を集めるという「落ちものゲーム」は、シュン君いわく「背景色を変えて、爆弾に当たったらゲームオーバーになるようにしました」とのこと。
(function () {
/* ゲーム内で共通で使用する変数 */
//Sprite を格納する配列
var snow_sprites = [];
//矢印キーのコード
var LEFT_KEY_CODE = 37;
var RIGHT_KEY_CODE = 39;
var key_value = 0;
//雪の画像サイズ
var SNOW_PIC_SIZE = 32;
//雪の降るスピード
var SNOW_DOWS_SPEED = 3;
//表示する雪の数
var DRAW_SNOW_COUNT = 12;
var DRAW_SNOW_GAP = 55;
// 表示する爆弾の数
var DRAW_BOM_COUNT = 3;
//雪ダルマの画像サイズ
var SNOW_MAN_PIC_SIZE = 80;
//html ドキュメント上の canvas のインスタンスが格納される
var canvas;
// 2d コンテキストのインスタンスが格納される
var ctx;
//雪ダルマの Sprite オブジェクトが格納される
var img_snow_man;
//雪ダルマの移動量
var PLAYER_MOVE_SPEED_R = 5;
var PLAYER_MOVE_SPEED_L = -5;
//画面の書き換え数をカウントする
var loopCounter = 0;
var requestId;
//前回動いた時間を格納
var bofore_animation_time = 0;
// ゲームオーバーかどうか
var isGameover = false;
//ゲーム内で動作する Sprite クラスの定義
var Sprite = function (imgSrc, width, height) {
var that = this;
that.imageLoaded = false;
that.imageSource = imgSrc;
that.x = 0;
that.y = 0;
that.dx = 0;
that.dy = 0;
that.width = width;
that.height = height;
var _offset_x_pos = 0;
//使用するインデックスを設定するための Setter/Getter
var imageIndex = 0;
Object.defineProperty(this, "index", {
get: function () {
return imageIndex;
},
set: function (val) {
imageIndex = val;
_offset_x_pos = width * imageIndex;
}
});
//新しい image オブジェクトのインスタンスを生成
var img = new Image();
//image オブジェクトに画像をロード
img.src = imgSrc;
//画像がロードされたら
img.onload = function () {
that.imageLoaded = true;
that.image = img;
};
//Sprite を描画するメソッド
that.draw = function () {
ctx.drawImage(img, _offset_x_pos, 0, width, height, that.x, that.y, width, height);
};
};
//Document の準備ができたら
document.addEventListener("DOMContentLoaded", function () {
loadAssets();
setHandlers();
});
function setHandlers() {
//キーイベントの取得 (キーダウン)
document.addEventListener("keydown", function (evnt) {
if (evnt.which == LEFT_KEY_CODE) {
key_value = PLAYER_MOVE_SPEED_L;
} else if (evnt.which == RIGHT_KEY_CODE) {
key_value = PLAYER_MOVE_SPEED_R;
}
});
//キーイベントの取得 (キーアップ)
document.addEventListener("keyup", function () {
key_value = 0;
});
//タッチした際の右クリックメニューの抑制
document.oncontextmenu = function () { return false; }
//Canvas へのタッチイベント設定
canvas.addEventListener("touchstart", function (evnt) {
if ((screen.width / 2) > evnt.touches[0].clientX) {
key_value = PLAYER_MOVE_SPEED_L;
} else {
key_value = PLAYER_MOVE_SPEED_R;
}
});
canvas.addEventListener("touchend", function (evnt) {
key_value = 0;
});
}
//canvas 内に使用する画像をロード
function loadAssets() {
//HTML エレメント上の canvas のインスタンスを取得
canvas = document.getElementById('myCanvas');
//2d コンテキストを取得
ctx = canvas.getContext('2d');
console.log("hoge2");
for (var i = 0; i < DRAW_SNOW_COUNT - DRAW_BOM_COUNT; i++) {
//雪のインスタンスを生成
var sprite_snow = new Sprite('/img/snowSP.png', SNOW_PIC_SIZE, SNOW_PIC_SIZE);
sprite_snow.dy = SNOW_DOWS_SPEED;
sprite_snow.dx = DRAW_SNOW_GAP;
snow_sprites.push(sprite_snow);
sprite_snow = null;
}
for (i = 0; i < DRAW_BOM_COUNT; i++) {
//爆弾のインスタンスを生成
var sprite_snow = new Sprite('/img/bom0.png', 16, 16);
sprite_snow.dy = SNOW_DOWS_SPEED * 2;
sprite_snow.dx = DRAW_SNOW_GAP;
snow_sprites.push(sprite_snow);
sprite_snow = null;
}
//雪だるまのインスタンスを生成
img_snow_man = new Sprite('/img/snow_man.png', SNOW_MAN_PIC_SIZE, SNOW_MAN_PIC_SIZE);
img_snow_man.limit_rightPosition = getRightLimitPosition(canvas.clientWidth, img_snow_man.width);
//画像のロードが完了したかどうかをチェックする関数
loadCheck();
};
//Splite に画像がロードされたかどうかを判断
function loadCheck() {
if (!img_snow_man.imageLoaded) {
//雪だるまの画像のロードが完了していなければループして待つ
requestId = window.requestAnimationFrame(loadCheck);
};
var length = snow_sprites.length;
for (var i = 0; i < length; i++) {
var snow_sprite = snow_sprites[i];
if (!snow_sprite.imageLoaded) {
requestId = window.requestAnimationFrame(loadCheck);
return;
} else {
snow_sprite.y = getRandomPosition(DRAW_SNOW_COUNT, -50);
snow_sprite.x = i * snow_sprite.dx;
}
}
var center_x = getCenterPostion(canvas.clientWidth, img_snow_man.width);
img_snow_man.x = center_x;
img_snow_man.y = 0;
img_snow_man.y = canvas.clientHeight - img_snow_man.width;
startScece();
}
//fps のコントロールコード
function control_fps(fps) {
var now_the_time = (new Date()).getTime() || window.performance.now();
var renderFlag = !(((now_the_time - bofore_animation_time) < (600 / fps)) && bofore_animation_time);
if (renderFlag) bofore_animation_time = now_the_time;
return renderFlag;
}
function startScece() {
if (control_fps(48) && !isGameover) {
//canvas をクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#5df";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if ((img_snow_man.x < img_snow_man.limit_rightPosition && key_value > 0)
|| (img_snow_man.x >= SNOW_DOWS_SPEED && key_value < 0)) {
//img_snow_man の x 値を増分
img_snow_man.x += key_value;
}
var length = snow_sprites.length;
for (var i = 0; i < length; i++) {
var snow_sprite = snow_sprites[i];
if (snow_sprite.imageSource != "/img/bom0.png") {
//img_snow の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (snow_sprite.y > canvas.clientHeight) {
snow_sprite.y = getRandomPosition(DRAW_SNOW_COUNT, -50);
snow_sprite.index = 0;
} else {
if (loopCounter == 30 && snow_sprite.index != 2) {
snow_sprite.index = (snow_sprite.index == 0) ? 1 : 0;
}
}
} else {
//img_snow の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (snow_sprite.y > canvas.clientHeight) {
snow_sprite.y = getRandomPosition(DRAW_SNOW_COUNT, -50);
snow_sprite.x = Math.floor(Math.random() * canvas.width);
snow_sprite.index = 0;
} else {
if (loopCounter == 30 && snow_sprite.index != 2) {
snow_sprite.index = (snow_sprite.index == 0) ? 1 : 0;
}
}
}
//img_snow の y 値を増分
snow_sprite.y += snow_sprite.dy;
//画像を描画
snow_sprite.draw();
//当たり判定
if (isHit(snow_sprite, img_snow_man)) {
hitJob(snow_sprite);
};
snow_sprite = null;
}
//画像を描画
img_snow_man.draw();
if (loopCounter == 30) { loopCounter = 0; }
loopCounter++;
//ループを開始
requestId = window.requestAnimationFrame(startScece);
} else {
requestId = window.requestAnimationFrame(startScece);
}
}
//中央の Left 位置を求める関数
function getCenterPostion(containerWidth, itemWidth) {
return (containerWidth / 2) - (itemWidth / 2);
};
//Player (雪だるまを動かせる右の限界位置)
function getRightLimitPosition(containerWidth, itemWidth) {
return containerWidth - itemWidth;
}
function getRandomPosition(colCount, delayPos) {
return Math.floor(Math.random() * colCount) * delayPos;
};
//雪と雪だるまがヒットした際の処理
function hitJob(snow_sprite) {
if (snow_sprite.imageSource != "/img/bom0.png") {
ctx.font = "bold 50px";
ctx.fillStyle = "red";
ctx.fillText("ヒットしました", 100, 160);
snow_sprite.index = 2;
// change: 音を鳴らす
document.getElementById("audio").play();
// /音を鳴らす
} else {
ctx.font = "bold 50px";
ctx.fillStyle = "red";
ctx.fillText("ゲームオーバー", 100, 160);
isGameover = true;
}
}
//当たり判定
function isHit(targetA, targetB) {
if ((targetA.x <= targetB.x && targetA.width + targetA.x >= targetB.x)
|| (targetA.x >= targetB.x && targetB.x + targetB.width >= targetA.x)) {
if ((targetA.y <= targetB.y && targetA.height + targetA.y >= targetB.y)
|| (targetA.y >= targetB.y && targetB.y + targetB.height >= targetA.y)) {
return true;
}
}
}
})();
このアレンジに対し、物江氏は次のようにコメント。
物江:音が出るようになっていることはもちろんなのですが、障害物である複数の爆弾が降るようになっています。
この『複数の障害物を出す』『降らす』というところは、クラス/インスタンスの概念を理解していないと実装できないのですが、みごとに実装してあります。しかも、雪用に定義してあるSpriteクラス (JavaScriptなので厳密には関数)をうまく再利用してあります。素晴らしいと思います。
ここまでできるのであれば、雪だるまの壊れた画像もこちらで用意しておけばよかったかなと。きっと爆弾とぶつかったときに雪だるまが壊れる、といったエフェクトも追加してくれたことでしょう。
ここまでで本当に十分なのですが、あえて、アドバイスするならば次の2点です。今は音を鳴らすためにhtmlファイルにaudioタグを追加していますが、以下のようにすればJavaScriptのみで完結できます。
また、オブジェクトのプロパティに値として持たせれば、インスタンスごとに違った音を指定することができるようになります。
イメージとしては以下のような感じです。 (ちなみにこれは、今回のクイズの回答でもあります)
var Sprite = function(audio_src){
//説明のためにコードは簡略化してあります。
this.sound = new Audio(audio_src);
this.sound.onload = function () {
this.soundLoaded = true;
}
};
鳴らすときには以下のようにします。
var bom = new Sprite('/sound/kiiiin1.mp3');
if(bom.soundLoaded){
bom.sound.play();
}
もう一点は、複数の爆弾のインスタンスを生成する際の変数として、雪のインスタンスを生成するのに使用している変数と同名のsprite_snowをfor文の中で使用していますが、これは別の名前に変えることをお勧めします。
for (var i = 0; i < DRAW_SNOW_COUNT - DRAW_BOM_COUNT; i++) {
//雪のインスタンスを生成
var sprite_snow = new Sprite('/img/snowSP.png', SNOW_PIC_SIZE, SNOW_PIC_SIZE);
/* 省略 */
}
for (i = 0; i < DRAW_BOM_COUNT; i++) {
//爆弾のインスタンスを生成
var sprite_snow = new Sprite('/img/bom0.png', 16, 16);
/* 省略 */
}
なぜかというと、JavaScriptの変数のスコープは関数単位なので、記述の仕方によっては意図しない値が設定される可能性があるからです(今回のコードは毎回nullクリアしているので実質問題はない)。
今回の場合、もともとの私の書いたコードの変数の定義位置が誤解を与えているような場所にあるのがよくなかったのですが…。このことについては、私のブログの以下の記事の「変数を宣言する場所」の項を読んでいただくと理解が深まるでしょう。
- 『モテる JavaScript』フォローアップ?<その2>
JavaScriptの慣例的な記述方法とマナー
HTML5+JavaScriptのゲームをゼロから開発するためのハンズオン資料も、ブログで書いているので、こちらも参考にしてください。
現在は、友達と一緒にいろいろ開発をしているそうですが、そこがまたいいですね。いくら優秀な技術者でも1人でできることは限られていますので。いつかシュン君が世界中の技術者とつながり、彼らと一緒に面白いものを作ってくれることを切に願っております。
今回のマイクロソフト訪問で、ますますプログラミング、ITの面白さにはまった様子のシュン君。
シュン:Azureもすごくはまっています。Windows Serverの仮想マシンを作って、それをファイルサーバーにしようとしたり、単純に仮想マシンで遊んでみたりしています。
ファイルサーバーを作ってみたいのですが、ネットワークが別で、仮想ネットワークでつないで、ファイルを共有するとか、今、いろいろと勉強中です。Windowsのクライアントはしょっちゅう使っているので、慣れているのですが、サーバー版だと、サーバーマネージャーとか、Active Directoryとか、知らないことが多くて…。
でもその知らないことにチャレンジすることも本当に楽しいんです。
このようにITの面白さをキラキラとした目で語るシュン君。ぜひ、みなさんも今回の問題でオリジナルアプリ作りを楽しんだり、改めてITの面白さを実感してみてくださいね。挑戦お待ちしています!
ちなみに、今回のキャンペーン問題参加者には、クラウド型Windowsアプリ開発環境が、先着100名様限定で体験できるという特典がある。
今回の問題は、クラウド型 Windows アプリ開発環境(お名前.com デスクトップクラウド)を使って解くことができるので、ぜひ各問題ページでチェックを!
今回のキャンペーン問題に応募すると、抽選で豪華プレゼントも!
今回のCodeIQ問題「きゃんちのジグソーパズルアプリを完成させよう」、もしくは「HTML5+JSで落ちものゲームを作ってみよう」の正解者の中から、抽選で100名様に超豪華商品をプレゼント!
【A賞】8インチWindowsタブレット「Dell Venue 8 Pro」10名様
【B賞】Blutooth対応 キーボード「Microsoft Universal Mobile Keyboard」20名様
【C賞】C丼 オリジナルストラップ:70名様
▲ジグソーパズル問題に挑戦する⇒ ▲雪だるま問題に挑戦する⇒
締切は、5月8日(金)AM12:00まで。皆さんの挑戦、お待ちしております!