九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、このブログでは過去に反射神経ゲームやタイピングゲームなど、Vueを使ったちょっとしたゲームの作り方を公開してきました。
そして、最近Google Analyticsを見てみると、これらゲーム記事の滞在時間は他と比べると長めになっているのでより皆さんに喜んでいただけているようです。^^
ただ、そうはいってもゲーム開発となると見た目はシンプルでもプログラム的にはコードが複雑だったりするので、主にVueビギナーの方々へ記事をお届けしたい私からするとどのような記事がいいか少し悩ましかったりもします。
そこで!
今回は、シンプルだけど比較的簡単にプログラムをつくれるであろう「○×ゲーム」をVueでつくる記事をお届けすることにしました。
ぜひ学習に役立ててくださいね。
(最後に教材ソースコードをダウンロードできます)
※ 開発環境: Vue 2.5
目次
やりたいこと
子供の頃よく友達と遊んだ次のような○×ゲーム(3目並べ)をVueでつくります。
ルール: プレイヤーが交互に○×をマスに置いていき、どちらかのマークが3つ並べば勝ちです。
また、実装内容は次のとおりです。
- ○は赤文字、×は青文字にする。
- どちらの番かが分かるように「○(×)プレイヤーさん、マスを選んでください」と表示する(つまり2Pプレイ)
- ゲームが終了したら「○(×)さんの勝ちです。おめでとうございます!」と表示する
- 引き分けの場合は「引き分けです!」と表示する
では実際にコードを書いていきましょう。
Vueの基本形をつくる
まずはVueが使えるように基本形をつくります。
<html> <head> <style></style> </head> <body> <div id="app"> <!-- ここがVueエリア --> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script> <script> new Vue({ el: '#app' }) </script> </body> </html>
やっていることは、cdn
からVue本体を読み込み、app
というid
をVue
に設定しているだけです。
レイアウトをつくる
続いて○
、×
を書いていくゲームのマス目を作ります。
<html> <head> <style> #table { margin:0 auto; border-collapse: collapse; border: 3px solid #ccc; } #table td { border:1px solid #ccc; height: 80px; width: 80px; text-align: center; vertical-align: middle; font-size: 75px; cursor: pointer; } </style> </head> <body> <div id="app"> <table id="table"> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> </table> </div> <!-- 省略 --> </body> </html>
中身は3×3のマスがある<table>〜</table>
タグを作っているだけです。
これを実行すると次のようになります。
では、このマス目を使ってVue(JavaScript)部分を作っていきましょう。
JavaScript部分をつくる
Vueに変数をつくる
まず、○×ゲームで必要な変数をつくっていきます。
必要なものは次の2点です。
- マス目の状態(3×3の配列)
- 選択できるプレイヤーのID
3×3の配列の中身は次のとおりです。
- -1 ・・・ まだ選択されてない
- 1 ・・・ ○が選択されている
- 2 ・・・ ×が選択されている
また、プレイヤーIDは
- 1 ・・・ ○のプレイヤー
- 2 ・・・ ×のプレイヤー
そして、これらをVueの変数にするとこうなります。
new Vue({ el: '#app', data: { states: [ [-1, -1, -1], [-1, -1, -1], [-1, -1, -1] ], playerId: 1 // ○のプレイヤーから開始 } })
変数をバインディングする
では、設定した変数をHTML内にバインディングしていきます。
まずはマス目部分です。
<table id="table"> <tr v-for="row in states"> <td v-for="state in row"> <div style="color:#f00;" v-if="state==1">○</div> <div style="color:#00f;" v-if="state==2">×</div> </td> </tr> </table>
ここでやっていることは、先ほどつくったdata
変数のstates
をv-for
を使ってループさせ、3×3のマスを作ると同時に、マス目の状態によって○
、×
、空白を切り替えています。
では、初期値を次のようにして実際に表示してみましょう。
states: [ [2, 2, 2], [-1, -1, -1], [1, 1, 1] ]
うまくいきました。
では続いてplayerId
ですが、これは現在マスを選ぶことができるプレイヤーのIDなので、このIDを使って次の2つの表示を切り替えるようにしてみます。
- ○ プレイヤーさん、マスを選んでください
- × プレイヤーさん、マスを選んでください
<table id="table"> <!-- 省略 --> </table> <div style="text-align:center;"> <div style="color:#f00;" v-if="playerId==1">「○ プレイヤーさん、マスを選んでください」</div> <div style="color:#00f;" v-if="playerId==2">「× プレイヤーさん、マスを選んでください」</div> </div>
実行するとこうなります。
マス目が選択されたときのコードをつくる
では、実際にマス目が選択されたときのコードをつくっていきます。
まずはマス目になる<td>
タグにクリックイベントでonSelect()
が実行されるようにします。
<table id="table"> <tr v-for="(row,rowsIndex) in states"> <td v-for="(state,colsIndex) in row" @click="onSelect(rowsIndex, colsIndex)"> <!-- 省略 --> </td> </tr> </table>
なお、選択されたマスがどこなのかが分かるように、v-for
にはそれぞれrowsIndex
とcolsIndex
を追加してonSelect()
へ送っています。
ではメソッド本体です。
new Vue({ // 省略 methods: { onSelect: function(rowsIndex, colsIndex) { if(this.states[rowsIndex][colsIndex] != -1) { alert('そのマスは、すでに選択されています!'); } else { var states = JSON.parse(JSON.stringify(this.states)) states[rowsIndex][colsIndex] = this.playerId; this.states = states; this.playerId = (this.playerId == 1) ? 2 : 1; } } } })
この中で、まず選択されたマスの状態を確認し、もし-1
でなければ(つまり、すでに選択済みなら)エラーを表示するようにしています。
そして、ここが重要なのですが、Vueの配列を更新するためには変数自体を書き換える必要があります。つまり、この場合だと
this.states[rowsIndex][colsIndex] = this.playerId;
としても、バインディングされた部分は新しく書き換えられることはありません。
そのため、少しまわりくどいですが、まず元々のデータを一旦JSONにして再度元に戻すことで配列をコピー。そして、選択されたマス目のデータを更新し、最後にthis.states
全体を書き換えるという方法をとっています。(お好みでVue.set()
を使ってもいいでしょう)
また、選択が済んだあとは、プレイヤーを交代させるためにplayerId
を変更するようにしています。
ゲームが完了したかどうかをチェックする
これでマス目の選択部分は完了しましたが、肝心のゲームに勝ったかどうかをチェックする機能がついていませんので、この部分を作っていきます。
onSelect: function(rowsIndex, colsIndex) { if(this.states[rowsIndex][colsIndex] != -1) { // 省略 } else { // 省略 var winnerId = this.getWinnerId(); if(winnerId != -1) { this.states = [ [-1, -1, -1], [-1, -1, -1], [-1, -1, -1] ]; playerIds = { 1: '○', 2: '×' }; alert(playerIds[winnerId] +' さんの勝ちです。おめでとうございます!'); } } }, getWinnerId: function() { for(var i = 0; i < 3 ; i++){ // 横の列 var row = this.states[i]; if(this.isStatesFilled(row)) { return row[0];} // 縦の列 var col = [ this.states[0][i], this.states[1][i], this.states[2][i] ]; if(this.isStatesFilled(col)) { return this.states[0][i]; } } // ななめ var skew1 = [ this.states[0][0], this.states[1][1], this.states[2][2] ]; if(this.isStatesFilled(skew1)) { return this.states[0][0]; } var skew2 = [ this.states[0][2], this.states[1][1], this.states[2][0] ]; if(this.isStatesFilled(skew2)) { return this.states[0][2]; } return -1; }, isStatesFilled: function(states) { return( states[0] != -1 && states[0] == states[1] && states[1] == states[2] ); }
少しコードが長いのでひとつずつ紹介していきます。
まずgetWinnerId()
の中では、states
の中身をisStatesFilled()
を使って次の勝利条件をチェックし、さらに勝ったプレイヤーのIDを取得しています。
- -1を含まず(まだ選択されていないため)
- 全ての数字が同じ値である
そして、もしこの勝利条件を満たす場合は1
もしくは2
の勝ったプレイヤーIDを返し、アラートを表示。さらにstates
を初期値へ戻します。
引き分けの場合をチェックする
○×ゲームは、引き分けの場合もありますので、勝利条件を満たしていない場合はisDraw()
で引き分けをチェックするようにします。
onSelect: function(rowsIndex, colsIndex) { if(this.states[rowsIndex][colsIndex] != -1) { // 省略 } else { // 省略 if(winnerId != -1) { // 省略 } else if(this.isDraw()) { this.states = [ [-1, -1, -1], [-1, -1, -1], [-1, -1, -1] ]; alert('引き分けです!'); } } }, // 省略 isDraw: function() { for(var i in this.states) { var row = this.states[i]; for(var j in row) { var state = row[j]; if(state == -1) { return false; } } } return true; }
isDraw()
の中ではstates
の中身をすべてチェックして-1
が存在していたら(つまりまだゲームは終わっていない)false
を返し、そうでなければゲームは終了しているのに勝敗が決まっていない(=引き分け)ということでtrue
を返します。
そして、もし引き分けの場合はアラートを起動して完了です。
お疲れ様でした!
おまけ – 1(リファクタリング)
今回紹介したコードの中には複数回同じコードが出てくる場面があります。
そうです。states
を初期化する部分ですね。
this.states = [ [-1, -1, -1], [-1, -1, -1], [-1, -1, -1] ];
同じコードはできるだけ一回書くだけにしておくほうがいいのでinit()
というメソッドを作ってこの中でstates
を初期化するようにリファクタリングしてみましょう。
methods: { init: function() { this.states = [ [-1, -1, -1], [-1, -1, -1], [-1, -1, -1] ]; }, // 省略
そして、init()
を呼び出す場所は先ほどのonSelect()
の中ですね。
onSelect: function(rowsIndex, colsIndex) { if(this.states[rowsIndex][colsIndex] != -1) { // 省略 } else { // 省略 if(winnerId != -1) { this.init(); playerIds = { 1: '○', 2: '×' }; alert(playerIds[winnerId] +' さんの勝ちです。おめでとうございます!'); } else if(this.isDraw()) { this.init(); alert('引き分けです!'); } } },
そして、もう一つあります。
データの初期値です。
今まで直接値を設定していましたが、空白の配列へ変更します。
data: { states: [], // 初期値はmountedの中で設定 // 省略 },
そして、ページが表示されたらすぐに実行されるmounted
の中でinit()
を実行すると可読性もあがりますし、コードの共通化ができて保守もしやすいでしょう。
mounted: function() { this.init(); }
おまけ – 2(勝った時に画像を表示する)
先ほどのコードでは、○×どちらかのプレイヤーが勝ったときにアラートを表示するだけでしたが、ちょっとこれだけでは味気ないので勝ったら画像を表示するようにしてみます。(ただし、対戦中は表示しません)
まずは変数部分です。
winnerText
という変数を追加し、どちらかが勝ったときのメッセージをここへ格納します。
data: { // 省略 winnerText: '' },
そして、レイアウト部分です。
<div v-if="winnerText" style="text-align:center;"> <h2 v-text="winnerText"></h2> <img src="/images/congrats.png"> </div>
もしwinnerText
の中にメッセージが入っていれば、そのテキストと画像を表示するようにします。
そして、winnerText
はマスが選択される度に初期化しますが、もしどちらかが勝った場合だけテキストを格納します。(こうすることで対戦中にテキストと画像が表示されることはなくなります)
onSelect: function(rowsIndex, colsIndex) { this.winnerText = ''; // 省略 if(winnerId != -1) { // 省略 this.winnerText = playerIds[winnerId] +' さんの勝ちです。おめでとうございます!'; } // 省略 }
デモを用意しました
今回のコードを実際に体験していただくためにデモページを用意しました。
ぜひ楽しんで体験してみてくださいね。
教材ソースコードをダウンロードする
今回実際に開発したコードを以下からダウンロードすることができます。cdn
を使っているので、展開してすぐ体験できますよ!
※ ただし画像をご自身で用意してください。
Vueで丸バツゲームをつくるおわりに
ということで今回は、Vueを使って子供の頃よくやったゲーム「○×ゲーム」を作ってみました。
できるだけ分かりやすくすることを心がけてコードを書きましたが、やはり冒頭でもいったとおり、ゲームのコードは色々と数学的な発想も必要になってくるので見た目よりは少し複雑なコードになってしまったかもしれません。
ただ、以前アプリで個人的にゲームを作ったことがあるのですが、やっぱりゲームを作るのは楽しいですね。
そういう意味では、子供の頃テレビゲームを夢中になってやった経験も生きてきているのかもしれません(・・・と信じたいです ^^;)
ではでは〜!