2-3 ゲームを完成させる

第一章で学んだHTML5&JavaScriptの技術と、二章で学んだゲームを制作するために必要な知識を使って、ゲームを完成させましょう。

完成させるゲームは 2-1 (2) で考えた 「タップ入力した位置に猫を誘導しネズミを捕らせるゲーム」 にします。
index画面内容
0タイトル
画面をタップしたらゲーム開始
1ゲーム
タップした位置に猫を移動させる
ネズミに触れたら点数を増やし、一定数捕まえたらステージクリア
持ち時間を減らしていき、時間0でゲームオーバー
犬に接触したら持ち時間を多く減らす
2ステージクリア
一定時間文字を表示する
ステージ数が10の場合はエンディングへ
そうでなければステージ数を+1し再びゲーム開始
3エンディング
「おめでとう!」の文字を表示し、タイトルに戻る
4ゲームオーバー
一定時間文字を表示したらタイトルに戻る


(1)図形を描く命令でキャラクターを表示

実際のゲームやソースコードを見る前に、ゲームで使う画像について少し考えてみます。 個人レベルのゲーム制作では「グラフィックをどう用意するか?」で悩むことがあります。 デザイナー志望の方や、プログラマー志望でかつデザインもできる方は、ご自身で画像を用意できますが、皆さん全員がそうできるとは限りません。 デザインは苦手という方は著作権フリーの素材を利用したり、絵が描けるお知り合いがいればデザインをお願いしてみましょう。
すぐには画像を用意できない方もいると思いますので、今回はそういった方への参考に、図形を描画する命令でキャラクターを表示しました。 円、矩形、三角の組み合わせですので、どうしてもシンプルなものになりますが、 デザインを簡易的に用意する(プログラムする)こういった方法もあります。

HTML5のCanvasには楕円を描く命令がありませんが、scale命令とarc命令で描くことができます。 楕円とキャラクターを描くソースコードを以下に抜粋します。

・楕円を描く

function fOval( x, y, r, sc, col ) {
 cnt.fillStyle = col;
 cnt.save();
 cnt.translate( x, y );
 cnt.scale( sc, 1 );
 cnt.beginPath();
 cnt.arc( 0, 0, r, 0, Math.PI*2, false );
 cnt.fill();
 cnt.restore();
}

中心座標、半径、縦横比、色を指定し、楕円を描く関数です。 context.save() は canvas の context に設定した状態(色やスケールなど)を保存する命令です。 translateで(x,y)に描画の原点を移動、scaleで縦横の伸縮を設定し、arcで円を描いています。 最後にcontext.restore()で保存しておいたcontextの状態を復元します。 saveとrestoreを行わないと、以後の描画がここでtranslateやscaleした値で行われてしまいます。

次が3種類の動物を表示する関数です。 fRectは矩形を描く関数、fTriは三角を描く関数で、fOvalと同じように定義しています(ソースコードは(2)をご覧下さい)。 効率の良いソースコードを書くには、このように何度も使う処理を関数として定義します。

・ネズミを描く 引数は座標と顔の色

function drawRat( x, y, col ) {
 fOval( x-24, y-30, 20, 1.0, col );//左耳
 fOval( x-22, y-28, 12, 1.0, "#fac" );
 fOval( x+24, y-30, 20, 1.0, col );//右耳
 fOval( x+22, y-28, 12, 1.0, "#fac" );
 fOval( x, y+ 6, 36, 1.0, col );//顔
 fOval( x-18, y- 6, 6, 1.0, "#000" );//左目
 fOval( x+18, y- 6, 6, 1.0, "#000" );//右目
 fOval( x, y+12, 10, 1.0, "#444" );//鼻
}


・猫を描く 引数は座標、顔の色、目の色

function drawCat( x, y, colFace, colEye ) {
 fOval( x, y, 40, 1.5, colFace );//顔
 fTri( x, y, x-30, y-60, x-60, y, colFace );//左耳
 fTri( x, y, x+30, y-60, x+60, y, colFace );//右耳
 fOval( x-24, y, 8, 2.0, colEye ); fOval( x-24, y, 8, 0.5, "#000" );//左目
 fOval( x+24, y, 8, 2.0, colEye ); fOval( x+24, y, 8, 0.5, "#000" );//右目
}


・犬を描く 引数は座標と顔の色

function drawDog( x, y, col ) {
 var i, tx, ty;
 var chin = 20*(tmr%2);
 fRect( x-60, y-30, 120, 50, col );//頬
 fRect( x-40, y+40+chin, 80, 30, col );//下あご
 fTri( x-60, y-30, x-30, y-80, x, y-30, col );//左耳
 fTri( x, y-30, x+30, y-80, x+60, y-30, col );//右耳
 for( i = 0; i <= 3; i ++ ) {//上の歯
  tx = x-60 + 30*i;
  ty = y+20;
  fTri( tx, ty, tx+15, ty+20, tx+30, ty, "#fff" );
 }
 for( i = 0; i <= 2; i ++ ) {//下の歯
  tx = x-45 + 30*i;
  ty = y+40+chin;
  fTri( tx, ty, tx+15, ty-20, tx+30, ty, "#fff" );
 }
 fTri( x-50, y-20, x-40, y+5, x-25, y-5, "#f00" );//左目
 fTri( x+50, y-20, x+40, y+5, x+25, y-5, "#f00" );//右目
 fOval( x, y+8, 16, 1.5, "#422" );//鼻
}

chin = 20*(tmr%2);はあごの動き用の変数です。


(2)「ねこアクション」の完成

それではゲームを見てみましょう。
example231.html ← 動作の確認
ソースコードは次のようになります。
01<!DOCTYPE html>
02<html lang="ja">
03<head>
04<meta charset="utf-8">
05<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0 user-scalable=no">
06<title>JavaScriptのテストプログラム</title>
07</head>
08<body style="background-color:#000;" id="mybody">
09<canvas style="position:absolute; top:0; bottom:0; left:0; right:0; margin:auto;" id="bg"></canvas>
10<script>
11var winW = window.innerWidth;
12var winH = window.innerHeight;
13var SCALE;
14var canvas = document.getElementById("bg");
15if( winW < winH ) {
16 canvas.width = winW;
17 canvas.height = winW;
18 SCALE = winW / 800;
19}
20else {
21 canvas.width = winH;
22 canvas.height = winH;
23 SCALE = winH / 800;
24}
25var cnt = canvas.getContext("2d");
26cnt.textAlign = "center";
27cnt.textBaseline = "middle";
28cnt.scale( SCALE, SCALE );
29
30//マウスとタップの判定
31var tapX = 0, tapY = 0, tapC = 0;
32
33canvas.addEventListener( "touchstart", touchStart );
34canvas.addEventListener( "touchend", touchEnd );
35function touchStart(event) {
36 event.preventDefault();
37 var rect = event.target.getBoundingClientRect();
38 tapX = event.touches[0].clientX-rect.left;
39 tapY = event.touches[0].clientY-rect.top;
40 transformXY();
41 tapC = 1;
42}
43function touchEnd(event) {
44 event.preventDefault();
45 tapC = 0;
46}
47
48canvas.addEventListener( "mousedown", mouseDown );
49canvas.addEventListener( "mouseup", mouseUp );
50function mouseDown(event) {
51 var rect = event.target.getBoundingClientRect();
52 tapX = event.clientX-rect.left;
53 tapY = event.clientY-rect.top;
54 transformXY();
55 tapC = 1;
56}
57function mouseUp(event) {
58 tapC = 0;
59}
60
61function transformXY() {//実座標→仮想座標への変換
62 tapX = toInt(tapX/SCALE);
63 tapY = toInt(tapY/SCALE);
64}
65
66function toInt( val ) {//整数を返す関数
67 return parseInt(val);
68}
69
70function rnd( max ) {//乱数を返す関数
71 return toInt( Math.random()*max );
72}
73
74//描画用の関数
75function fontSize( siz ) {//フォントの大きさをセット
76 cnt.font = siz + "px monospace";
77}
78
79function fText( str, x, y, col ) {//文字表示
80 cnt.fillStyle = "#000"; cnt.fillText( str, x, y+2 );//文字に影を付ける
81 cnt.fillStyle = col;    cnt.fillText( str, x, y );
82}
83
84function fRect( x, y, w, h, col ) {//矩形
85 cnt.fillStyle = col;
86 cnt.fillRect( x, y, w, h );
87}
88
89function fTri( x1, y1, x2, y2, x3, y3, col ) {//三角
90 cnt.fillStyle = col;
91 cnt.beginPath();
92 cnt.moveTo(x1,y1);
93 cnt.lineTo(x2,y2);
94 cnt.lineTo(x3,y3);
95 cnt.closePath();
96 cnt.fill();
97}
98
99function fOval( x, y, r, sc, col ) {//楕円
100 cnt.fillStyle = col;
101 cnt.save();
102 cnt.translate( x, y );
103 cnt.scale( sc, 1 );
104 cnt.beginPath();
105 cnt.arc( 0, 0, r, 0, Math.PI*2, false );
106 cnt.fill();
107 cnt.restore();
108}
109
110function drawRat( x, y, col ) {//ネズミ
111 fOval( x-24, y-30, 20, 1.0, col );//左耳
112 fOval( x-22, y-28, 12, 1.0, "#fac" );
113 fOval( x+24, y-30, 20, 1.0, col );//右耳
114 fOval( x+22, y-28, 12, 1.0, "#fac" );
115 fOval( x,    y+ 6, 36, 1.0, col );//顔
116 fOval( x-18, y- 6,  6, 1.0, "#000" );//左目
117 fOval( x+18, y- 6,  6, 1.0, "#000" );//右目
118 fOval( x,    y+12, 10, 1.0, "#444" );//鼻
119}
120
121function drawTap( x, y, col ) {//肉球(タップした位置)
122 fOval( x, y, 16, 1.0, col );
123 fOval( x-20, y-12, 6, 1.0, col );
124 fOval( x- 8, y-22, 6, 1.0, col );
125 fOval( x+ 8, y-22, 6, 1.0, col );
126 fOval( x+20, y-12, 6, 1.0, col );
127}
128
129function drawCat( x, y, colFace, colEye ) {//猫
130 fOval( x, y, 40, 1.5, colFace );//顔
131 fTri( x, y, x-30, y-60, x-60, y, colFace );//左耳
132 fTri( x, y, x+30, y-60, x+60, y, colFace );//右耳
133 fOval( x-24, y, 8, 2.0, colEye ); fOval( x-24, y, 8, 0.5, "#000" );//左目
134 fOval( x+24, y, 8, 2.0, colEye ); fOval( x+24, y, 8, 0.5, "#000" );//右目
135}
136
137function drawDog( x, y, col ) {//犬
138 var i, tx, ty;
139 var chin = 20*(tmr%2);
140 fRect( x-60, y-30, 120, 50, col );//頬
141 fRect( x-40, y+40+chin, 80, 30, col );//下あご
142 fTri( x-60, y-30, x-30, y-80, x,    y-30, col );//左耳
143 fTri( x,    y-30, x+30, y-80, x+60, y-30, col );//右耳
144 for( i = 0; i <= 3; i ++ ) {//上の歯
145  tx = x-60 + 30*i;
146  ty = y+20;
147  fTri( tx, ty, tx+15, ty+20, tx+30, ty, "#fff" );
148 }
149 for( i = 0; i <= 2; i ++ ) {//下の歯
150  tx = x-45 + 30*i;
151  ty = y+40+chin;
152  fTri( tx, ty, tx+15, ty-20, tx+30, ty, "#fff" );
153 }
154 fTri( x-50, y-20, x-40, y+5, x-25, y-5, "#f00" );//左目
155 fTri( x+50, y-20, x+40, y+5, x+25, y-5, "#f00" );//右目
156 fOval( x, y+8, 16, 1.5, "#422" );//鼻
157}
158
159//ヒットチェックを行う関数
160function hitCheck( x1, y1, x2, y2, r ) {
161 var dis = Math.sqrt( (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) );
162 if( dis < r ) return true;
163 return false;
164}
165
166//変数の宣言
167var idx = 0;
168var tmr = 0;
169var score = 0;
170var stage = 0;
171var gtime = 0;
172var norma = 0;
173var catCol = "#000";//猫の色
174var catX = 400, catY = 400;
175var ratX = 200, ratY = 400, ratXP = 0;
176var dogX = 600, dogY = 400, dogXP = 0, dogYP = 0;
177
178//各ステージの背景の色
179var BG_COLOR = [ "#000", "#C00", "#B40", "#A60", "#880", "#4A0", "#0A8", "#08C",  "#06F", "#62F", "#A0A", ];
180
181function gameStart() {//ゲーム開始時に初期化する変数
182 gtime = 600;//持ち時間
183 norma = stage*2;//何匹捕まえればクリアか
184 catCol = "#000";
185 catX = 400; catY = 400;//猫の座標
186 ratX = 200; ratY = 40+rnd(720); ratXP = -8-stage;//ネズミ
187 dogX = 600; dogY = 40+rnd(720); dogXP = 12+stage; dogYP = 8+stage;//犬
188 tapX = 0; tapY = 0; tapC = 0;//タップ値をクリア
189 document.getElementById("mybody").style.backgroundColor = BG_COLOR[stage];
190 idx = 1;//ゲームの処理へ移行
191 tmr = 0;
192}
193
194window.onload = indexProc();
195function indexProc() {
196 var i, x, y;
197 tmr ++;
198
199 //バックを描く
200 fRect( 0, 0, 800, 800, BG_COLOR[stage] );
201 cnt.globalAlpha = 0.2;
202 for( i = 0; i < 10; i ++ ) {//チェック柄
203  x = 80*i+20; fRect( x, 0, 40, 800, "#fff" );//縦のライン
204  y = 80*i+20; fRect( 0, y, 800, 40, "#ccc" );//横のライン
205 }
206 cnt.globalAlpha = 1.0;
207
208 drawRat( ratX, ratY, "#888" );
209 drawCat( catX, catY, catCol, "#cf0" );
210 drawDog( dogX, dogY, "#840" );
211
212 fontSize(40);
213 fText( "STAGE:"+stage, 150, 40, "#4ef" );
214 fText( "SCORE:"+score, 400, 40, "#0f8" );
215 fText( "TIME:"+gtime, 650, 40, "#ff4" );
216
217 switch( idx ) {
218  case 0:
219  fontSize(80); fText( "ねこアクション(仮)", 400, 200, "#fff" );
220  fontSize(40); fText( "画面をタップしてスタート", 400, 600, "#dbf" );
221  if( tapC == 1 ) {
222   score = 0;
223   stage = 1;
224   gameStart();
225  }
226  break;
227
228  case 1:
229  fText( "あと " + norma + "匹捕まえればクリア", 400, 720, "#fff" );
230  drawTap( tapX, tapY, "#fff" );
231  //猫の動き
232  catCol = "#000";
233  if( tapX != 0 && tapY != 0 ) {
234   if( tapX > catX ) catX += 20;
235   if( tapX < catX ) catX -= 20;
236   if( tapY > catY ) catY += 20;
237   if( tapY < catY ) catY -= 20;
238  }
239  //ネズミの動き
240  if( ratX+ratXP < 40 || ratX+ratXP > 760 ) ratXP = -ratXP;
241  ratX += ratXP;
242  if( hitCheck( catX, catY, ratX, ratY, 90 ) == true ) {//捕まえたか?
243   catCol = "#fff";
244   score += toInt(gtime/10);
245   norma --;
246   if( norma == 0 ) { idx = 2; tmr = 0; break; }
247   ratX = 50+rnd(700);
248   ratY = 50+rnd(700);
249  }
250  //犬の動き
251  if( dogX+dogXP < 40 || dogX+dogXP > 760 ) dogXP = -dogXP;
252  if( dogY+dogYP < 40 || dogY+dogYP > 760 ) dogYP = -dogYP;
253  dogX += dogXP;
254  dogY += dogYP;
255  if( hitCheck( catX, catY, dogX, dogY, 120 ) == true ) {//犬と接触したか?
256   if( tmr%2 == 0 ) catCol = "#f00";
257   gtime -= 10;
258  }
259  gtime --;
260  if( gtime < 0 ) { gtime = 0; idx = 4; tmr = 0; }
261  break;
262
263  case 2:
264  y = 400; if( tmr < 6 ) y = 10*tmr*tmr;
265  fontSize(60);
266  fText( "ステージクリア!", 400, y, "#48f" );
267  if( tmr == 30 ) {
268   if( stage == 10 ) {
269     idx = 3;
270     tmr = 0;
271   }
272   else {
273    stage ++;
274    gameStart();
275   }
276  }
277  break;
278
279  case 3:
280  y = 400; if( tmr < 20 ) y = tmr*tmr;
281  fontSize(60);
282  fText( "おめでとう!", 400, y-100, "#fc0" );
283  fText( "全ステージクリアです!", 400, y, "#0f0" );
284  if( tmr == 100 ) idx = 0;
285  break;
286
287  case 4:
288  y = 400; if( tmr < 6 ) y = 10*tmr*tmr;
289  fontSize(60);
290  fText( "ゲームオーバー", 400, y, "#f00" );
291  if( tmr == 50 ) idx = 0;
292  break;
293 }
294
295 setTimeout( indexProc, 100 );
296}
297</script>
298</body>
299</html>

2-1 で学んだインデックスとタイマーで処理の流れを管理しています。
猫がネズミを捕まえた判定、犬と接触した判定は 2-2 で学んだヒットチェックです。

ソースコードを読み解く際、どの変数で何を行っているか判ると内容を理解しやすいですので、今回使っている変数を列挙します。
変数名用途何を行っているか
idx,tmrインデックスとタイマープログラム全体の流れを管理
score点数ネズミを捕まえた時に増やす
増やす値は (ゲームの残り時間÷10) としている
stageステージ数1から10ステージまで
gtimeゲーム時間0になったらゲームオーバー
normaクリアに必要なネズミの捕獲数ゲーム開始時に値をセット
ネズミを捕まえたら1減らし、0になればゲームクリア
catCol猫の色ネズミを捕まえた時に白、犬と接触した時に赤にしている
catX,catY猫の座標タップした位置(tapX,tapY)に猫を移動させる
ratX,ratYネズミの座標
ratXPネズミの移動の座標の増減値X方向に移動する値
ステージが進むほど移動量を大きくしている
画面の左右端に来たら値を反転し向きを変える
dogX,dogY犬の座標
dogXP,dogYP犬の移動の座標の増減値X方向、Y方向に移動する値
ステージが進むほど移動量を大きくしている
画面上下左右に来たら向きを変える

また配列変数で各ステージのバックの色を定義しています。
var BG_COLOR = [ "#000", "#C00", "#B40", "#A60", "#880", "#4A0", "#0A8", "#08C", "#06F", "#62F", "#A0A", ];

以下が今回のソースコードに実装した新しいポイントです。

【ポイント1】 ブラウザのなるべく広い領域を使う 15~24行
今回はゲーム画面を正方形としました。 ブラウザの縦と横のドット数を調べ、縦長の画面であればキャンバスをブラウザの横幅いっぱい、横長の画面であれば縦幅いっぱいの大きさに広げています。 こうすることでスマートフォンやタブレットで、縦持ち、横持ちを問わず、できるだけ広い画面でプレイできるようにしています。

【ポイント2】 マウスとタップイベントを同時に実装 33~59行
タップ入力できるWindows-PCには標準ブラウザとしてEdgeが入っています。 このPCにChromeをインストールして使う場合、 if( 'ontouchend' in document ) の判定でタップイベントだけを有効にすると、マウス判定が行われません。 タップ、マウスともに入力を行うには 'ontouchend' in document の判定をせずに、タップイベント、マウスイベントのソースコードを記述します。

いつでもタップとマウスイベントの両方を実装しておけばよいのでは?
タップ入力できるWindows-PCが安いものでは3万円程度で購入できるようになりました。 タップ入力のWindows-PCは今後更に普及するでしょうから、マルチプラットフォーム対応のゲームを作るなら、タップとマウス両方の操作ができると良いでしょう。 その際、注意すべき点があります。 今回のソースコードはタップとマウスの入力値を取得するだけの処理ですから両方実装して問題ありません。 では例えばタップかマウス入力があった時に一度だけ重要な処理を行うプログラムではどうでしょうか? ユーザーがマウス操作しながら液晶画面に触れた時などに重要な処理が繰り返されないソースコードを書く必要があります。 またスマートフォンとタブレットはタップイベントが前提(第一優先)です。 制作するプログラムの内容によって必要な実装を行いましょう。


【ポイント3】 乱数
ゲームのプログラムではよく乱数を用います。今回のソースコードでも乱数を返す関数を用意しています。 Math.random()が乱数を発生させる命令で、乱数の値は0以上1未満の少数となります。 今回用意した関数は、発生させたい乱数の最大値を引数で与え、0から最大値未満のいずれかの整数が返るようになっています。

function rnd( max ) {//乱数を返す関数
 return toInt( Math.random()*max );
}


その他、背景のチェック柄を描く処理の context.globalAlpha は描画の透明度を設定する命令です。 値は1以下の少数で指定し、0を指定すると完全な透明(描画されません)、1を指定すると完全な不透明(通常の描画)となります。 設定した値は図形、文字、画像ファイルの表示という全ての描画に適用されます。
またキャンバスを塗り潰す色( BG_COLOR[stage] )を、HTMLのbody要素の色に設定し、キャンバス周りの余白の色としています。

document.getElementById("mybody").style.backgroundColor = BG_COLOR[stage];


さあ、ここまでくれば、ご自身でゲームを開発できます。 まずは簡単な内容でよいのです。ご自身の手でゲームを制作してみましょう。



前のページへ / 次のページへ

お気軽にお問い合わせ下さい →