Web上には自作のゲームがあふれている。それらは主に
FlashやJavaScriptあるいはPerlやPHPなどで作られてい
る。中には、SVGやPDFなどでゲームを作る猛者も見かけ
るがそれらやFlashやJavaScriptはクライアントサイド
のゲームでありPerlやPHPはサーバーサイドのゲームだ。
もちろん、これらを組み合わせたゲームも最近はよく見
かけるようになってきている。
サーバーサイドの自作ゲームではサーバーに蓄積した
データを共有しつつゲームを進行させるネットワーク型
のゲームが多く、クライアントサイドのゲームではビジ
ュアルやインタラクティブな動作に凝った作品が多い。
今回作るWebゲームはJavaScriptがベースのものであり
基本的には制限時間内にどれだけアイテムをゲットでき
るかというクライアントサイドの得意分野ともいえるも
のに仕上げてみた。
ただ、タイムアタック的ゲームなので他人と成績を競
わなくては楽しくない、ということで、NewGameWeb
(http://game.gr.jp/)のスコアランキングシステムを
利用している。これは、JavaScriptコードを1行書き加
えるだけで、自作ゲーム用のスコアランキングを利用で
きるようにする無料のシステムだ。
あと、注意したいのはPC環境やクロスブラウザについ
ての考慮だ。Webの閲覧と違ってゲームで競うとなると
そのプレイ環境の優劣は大きなハンデとなってくる。
家庭用ゲーム機ならみな同じ環境でプレイできるわけ
だがWeb上では、千差万別の環境でプレイすることにな
る。したがって、優劣をあまり気にしなければ、少しで
も多くの人に遊んでもらえるクロスブラウザなタイプに
し、逆にある程度成績にシビアになるならPCやブラウザ
を限定することも必要になるだろう。
今回のテーマのポイント
1. 自作のWebゲームについて
2. ゲームの構成をどう考えればよいか
3. 自分のゲーム専用関数をまとめながら作ってみよう
ゲームの基本構成を考える
作りたいと思うことが何よりも大切だ!
■基本構成を考える
作りたいゲームの種類によってゲームの構
成は異なってくる。したがって、まず、どん
なゲームを作るのかを考えてみよう。
今回は、キー操作でキャラを動かしお宝を
探すというものにチャレンジしたい。いや、
ネタはシューティングでもRPGでもなんでも
よいのだが、とりあえず4ページで済んで(笑)
拡張可能なものにした。
では、まず最初にどんなことを考えればよ
いのだろうか?
何よりも大切なのは「作りたい!」という
気持ちだ。学校や塾の授業と違って、手取り
足取り教えてくれる先生はいない。どんな
困難も自分の意思で乗り越えていかなければ
ならないのだ。
したがって、最初の「どんなゲームにする
か考える」という作業はかなり大切だ。自分
が「作りたい!」と思えないゲーム作成に挑
戦することは致命傷といえるからだ。
さてまず、ゲームの絵づらはどうだろう?
お宝を隠し、キャラが動き回る背景が必要だ。
これは、自分の好きな絵柄にすれば良いわけ
だが、具体的には今回はHTMLで<img>タグを
使って書くことにしよう。
この背景上に、キャラの画像をかぶせて、
JavaScriptで動かし、お宝隠しやゲット時
の処理を書いていくという流れで今回はやっ
てみようと思う。
整理しよう。
1.どんなゲームにするか考える
2.「作りたい!」と強く念じる
3.ゲームの絵づらを考える
4.スクリプト(シナリオ)を考える
5.画像とHTMLとスクリプトを書く
6.動かしてみて、駄目なら →2へ
7.6を完成するまで繰り返す!
■どんなゲームにするか
「キー操作でキャラを動かしお宝を探す」
ゲームといってもバリエーションは無限にあ
る。まず、どんな宝探しにするかを考えてみ
よう。
筆者が子供のころ町内会でやった宝探しは
林の中のあちこちに隠したお宝の紙を「よー
いどん」で駆け込んでゲットするというもの
だった。
安いお宝は小石の下などの簡単なところに
隠してあるので、まず、早い者勝ちですばや
く駆け回ってたくさん取り、その合間に高額
なお宝に目配りするのが筆者のノウハウだっ
た。まあ、複雑な仕掛けを考えるときりがな
いので今回は矢印キーで動かし改行キーでお
宝ゲットするタイムアタックにしてみる。
スクリプトの構成は次のようなものだ。
1.汎用関数群読み込み
キーボード処理用
レイヤー移動用関数群読み込み
2.データのセットと処理開始
ゲームに必要なデータのセット
初期値のセットと処理開始
3.このゲーム用の関数群
画面の描画用関数
キャラクタ書替用関数
お宝生成関数
カウントダウンタイマー関数
キー制御と判定関数
当たり&スカ時処理関数
まず、汎用関数群読み込みとゲームに必要
なデータのセットからみていこう。
キーボードのキーを読み取って動作させる
処理を1から作るのは手間なので、以前紹介し
たクロスブラウザ関数群の中からgetKEYCODE()
などの関数を選びonkeypressとonkeydownで
key_Pressとkey_Downという名前の関数を起動
する形にしてkeyworks.jsという名前で.js外部
ファイル化しておく。
また、レイヤー移動用の関数群も同様に
以前紹介したクロスブラウザ関数群の中から
選んでmoveworks.jsにまとめてある。
これによって、それらの処理に付いては
もう細かい実装などを考えなくても良くなる
わけだ。
では、最初の部分を見てみよう。上記外部
ファイルの読み込みと、グローバル変数設定
部分だ。「変数」とはいっても、主にあとで
あまり変更する予定のないものをここで最初
に設定している。ちなみに、あとで変更され
るものは次ページのmain()関数の中で初期化
されてるという仕組みになっている。
↓今回のスクリプトサンプル ./sample/otakara.htm
<!-- キーボード処理用関数群 -->
<script language="JavaScript" src="keyworks.js"></script>
<!-- レイヤー移動用関数群 -->
<script language="JavaScript" src="moveworks.js"></script>
<script language="JavaScript">
<!--
/***
* グローバル変数
*
*/
// 植え込み
var BOXLENGTH = 120 // 「植え込み」のMax数
var ROW_X = 20 // x方向の列数
var COL_Y = 10 // y方向の列数
var OFFSET_X = 20 // 「植え込み」書き始め位置x
var OFFSET_Y = 120 // 「植え込み」書き始め位置y
var xy = "" // 「植え込み」位置記録用変数
var img_k1 = new Image() // 「植え込み」画像
img_k1.src = "kusa1.gif"
var img_k2 = new Image() // 「とる?」画像
img_k2.src = "kusa2.gif"
// 「ペンギン」
var xpos = OFFSET_X // 「ペンギン」の親DIV 初期位置x
var ypos = OFFSET_Y // 「ペンギン」の親DIV 初期位置y
var img_r = new Image() // 「ペンギン」右向き画像
img_r.src = "pen_r.gif"
var img_l = new Image() // 「ペンギン」左向き画像
img_l.src = "pen_l.gif"
var img_u = new Image() // 「ペンギン」上向き画像
img_u.src = "pen_u.gif"
var img_d = new Image() // 「ペンギン」下向き画像
img_d.src = "pen_d.gif"
// お宝、スコア、タイマー
var OTAKARALEN = 5 // お宝やスカの種類数
var timernum = 0 // タイマー残
var img_o1 = new Image() // 「とる?」画像
img_o1.src = "king.gif" // 王冠
var img_r1 = new Image() // 「とる?」画像
img_r1.src = "ring.gif" // 王冠
var a = new Array() // スコア、タイマーボードを生成
a[0] = new followingLAYER('msgbord',10,20,1,'')
//つづく
初期値のセットと処理開始、ゲーム用の関数群を用意する
ゲーム用の関数群を用意するとゲームが作りやすくなる
つづいて、初期値のセットと処理開始、
そして、いくつかのこのゲーム用の関数群に
ついて説明しよう。
まず、 main()という名前の関数があるが
これは「Game start !」ボタンを押すと起動
され、ゲームに必要な初期値をセットし、各
関数を呼び出すことでゲームを進行する。
再ゲームしたいときにはもう一度「Game
start !」ボタンを押せば、必要な変数値が
すべてリセットされてゲームが再開される
という仕組みになっている。
作業を大きくわけると「植え込み」を書き
「お宝」を生成し、「ペンギン」や「植え込
み」画像の書き換え用関数を用意して「カウ
ントダウンタイマー」をスタートさせている。
各関数の名前と作業内容は次のようになっ
ている。
main() 処理開始 初期値セット
mkbox() 「植え込み」の描画用関数
getPenImageOj() 「ペンギン」画像取得
chgPenImg() 「ペンギン」書き換え
chgKusaImg() 「植え込み」書き換え
chgheight() 「ペンギン」の呼吸
mkotakara() 「お宝」生成
startclock() カウントダウン開始
tick() 「カウントダウンタイマー」
main()から起動されたmkbox()関数のなか
をのぞくとわかるが、「植え込み」はBOXLENGTH
でセットされた数だけimgタグを含んだdivタグ
をfor文で書き出すことで作っている。このとき
xとyの位置をランダムで生成しているが
xy += mkotakara()+"_" + x + "_" + y + "_;"
と「お宝」データとともに変数xyへ記録して
いる部分がポイントだ。mkotakara()は次ペ
ージに出てくる関数だがお宝の値を1から5の
間の値で返している。
したがって、たとえばleft:32px;top32px;
へお宝コード2の「植え込み」を書き出すと、
xyへは "2_32_32_;" という文字列が追加され
る。
そして、このゲームの「ペンギン」の動作
やあたり判定は、このレコードを読み出すこ
とで行っているのだ。
ここでは省略しているが、main()でセット
されるメッセージボードに関するレイヤーは
moveworks.jsの中で生成されたものを利用し
ているのでそこを見たい場合は、WebDesining
のサイトでダウンロードしてみて欲しい。
/***
* 処理開始 初期値セット
*
*/
function main()
{
// 初期値セット
//
// 植え込み
xy = "" // 「植え込み」位置記録用変数
torux = 0 // 「とる?」画像の親DIV x位置
toruy = 0 // 「とる?」画像の親DIV y位置
kusalayer = getLayOj('kusa') // 「植え込み」の親DIV
outputLAYER("kusa",mkbox()) // 「植え込み」の描画を行う
zindexLAYER('kusa',0) // 「植え込み」親ojのz-index設定
// 「ペンギン」
xpos = OFFSET_X // 「ペンギン」の親DIV 初期位置x
ypos = OFFSET_Y // 「ペンギン」の親DIV 初期位置y
penlayerStyle = getLayStyleOj('penLay') // 「ペンギン」の親DIV
penimg = getPenImageOj('pen') // 「ペンギン」画像
moveLAYERoj(penlayerStyle,xpos,ypos) // 初期位置へ移動
// スコア、メッセージボード
pscore = 0 // スコア
getslength = 0 // お宝ゲット数
msg = "" // スコアボード用メッセージ
zindexLAYER('msgbord',20) // 重なり順を20に設定
setOpacity('msgbord',0.8) // 不透明度を70%に設定
showHELP('',0) // ボードを書き出す
dofollow() // 「ペンギン」を追尾開始
bord1 = document.getElementById('msg1') // メッセージ
bord2 = document.getElementById('msg2') // カウントダウンタイマー
bord3 = document.getElementById('msg3') // ゲットしたアイテム
bord4 = document.getElementById('msg4') // スコア
moveLAYERoj(getLayStyleOj('me'),-900,-900)//スタートボタンを隠す
// タイマー
timernum = 2000 // タイマー残
clockId = 0 // カウントダウンタイマー
penkokyu = 0 // 「ペンギン」の呼吸動作用タイマー
clearInterval(penkokyu) // 「ペンギン」呼吸タイマークリア
penkokyu = setInterval("chgheight()",100) // 「ペンギン」呼吸開始
clearInterval(clockId) // カウントダウンタイマークリア
startclock() // カウントダウンタイマー開始
}
/***
* 「植え込み」描画用HTML生成
*
*/
function mkbox()
{
div = "" //クリア
for( i = 0 ; i < BOXLENGTH ; i++ )
{
//「植え込み」位置xyをランダムに生成
x = Math.ceil( Math.random() * ROW_X ) * 32 + OFFSET_X
y = Math.ceil( Math.random() * COL_Y ) * 32 + OFFSET_Y
if( xy.indexOf('_'+x+'_'+y+'_') == - 1 ) //重複回避
{
//位置とお宝を記録する
xy += mkotakara()+"_" + x + "_" + y + "_;"
div += "<div "
div += " id='d"
div += "_" + x + "_" + y + "_"
div += "'"
div += " style='position:absolute;z-index:0;left:"
div += x
div += "px;"
div += "top:"
div += y
div += "px;"
div += "'>"
div += "<img "
div += " name='i"
div += "_" + x + "_" + y + "_"
div += "'"
div += " src="+img_k1.src+">"
div += "</div>\n"
}
}
return div
}
/***
* 「ペンギン」と「植え込み」の画像書き換え
*
*/
// 「ペンギン」画像取得
function getPenImageOj(imageName)
{
return document.images[imageName].style
}
// 画像書き換え(「ペンギン」の向き)
function chgPenImg(img)
{
document.images['pen'].src = img // 書き換え
}
// 画像書き換え(植え込み)
function chgKusaImg(imageName,img)
{
document.images['i'+imageName].src = img // 書き換え
}
// 「ペンギン」の呼吸
flg = false
function chgheight()
{
if(flg) //1pxだけサイズ書き換えを繰り返す
{
penimg.height = '32px'
} else {
penimg.height = '33px'
}
flg = !flg
}
/***
* お宝生成
*
*/
function mkotakara()
{
return Math.ceil( Math.random() * OTAKARALEN )
}
/***
* カウントダウンタイマー
*
*/
//--カウントダウン動作開始
function startclock(){
clockId = setInterval('tick()',1)
}
//--カウントダウン文字列出力
function tick()
{
if( timernum > 0 )
{
//タイマーが終了前なら
timernum--
bord2.innerHTML="<b>あと"+timernum/100+"秒</b>"
window.status ="あと"+timernum/100+"秒"
bord4.innerHTML="お宝数 : "+getslength+" //" //お宝数表示
bord4.innerHTML+="<b>スコア : "+pscore+"</b>"//スコア表示
} else {
//タイマーが終了後なら
clearInterval(clockId)
bord1.style.backgroundColor='orange'
bord1.innerHTML="再ゲームするなら"
bord1.innerHTML+="<input type=button value='再ゲーム' onclick='main()'>"
bord1.innerHTML+="<br>スコアを送信するなら"
bord1.innerHTML+="<input type=button value='スコア登録' onclick='syoubu()'>"
bord2.innerHTML = "<center>★終了★</center>"
}
}
//つづく
キー制御と判定とあたり判定処理
キー制御本体は汎用関数を利用して楽をして、キー仕分けとあたり判定処理を書き加えていく。
次は、キー制御と判定とあたりすか処理、
そしてスコア送信だ。用意した関数はつぎの
とおり。
memov() キー制御と判定
atari() あたり時の共通処理
suka() すか時の共通処理
syoubu() NGWへスコア送信処理
memov()はキーボード処理用関数群moveworks.js
から起動される関数で、第一引数にkeypress
かkeydownのイベントタイプを表す文字列、
第一引数にkeypressなら押したキーの文字列
が渡され、keydownなら押したキーのコード
が渡される。
関数内部では、押されたキーにしたがって
「ペンギン」の向きや移動、「植え込み」の
画像やあたり判定関数の呼び出しなどを行っ
ている。atari()とsuka()関数は文字通りその
判定後の処理をまとめたものだ。
そして、ゲームが終了したところ、つまり
カウントダウンが終わったところで、前ペー
ジのstartclock()により、スコアを送信用
ボタンと再ゲームボタンが表示される。
/***
* キー制御と判定
*
*/
function memov(type,key){
if( timernum <= 0 ) return
var pressedkey = 0 // プレスキークリア
var downedkey = 0 // ダウンキークリア
//キータイプによって値をセット
if(type == 'keypress') pressedkey = key
else downedkey = key
xpos_old = xpos // 現在位置xを記録
ypos_old = ypos // 現在位置yを記録
//5 キー押したら...
if(pressedkey=='5'){
xpos = 0 // 位置xをホームポジションへ
ypos = 0 // 位置yをホームポジションへ
xpos_old = xpos
ypos_old = ypos
}
//s or S キー押したら...
if(pressedkey=='s' || pressedkey=='S'){
main()
}
//8 or ↑ キー押したら...
if(pressedkey=='8' || downedkey =='104' || downedkey =='38'){
chgPenImg(img_u.src) // 上向き「ペンギン」に書き換える
bord1.innerHTML = "" // ボード文字クリア
ypos -= 32 // 位置yを1コマ分上に書き換える(-32px)
}
//6 or → キー押したら...
if(pressedkey=='6' || downedkey =='102' || downedkey =='39'){
chgPenImg(img_r.src) // 右向き「ペンギン」に書き換える
bord1.innerHTML = "" // ボード文字クリア
xpos += 32 // 位置xを1コマ分右に書き換える(+32px)
}
//4 or ← キー押したら...
if(pressedkey=='4' || downedkey =='100' || downedkey =='37'){
chgPenImg(img_l.src) // 左向き「ペンギン」に書き換える
bord1.innerHTML = "" // ボード文字クリア
xpos -= 32 // 位置xを1コマ分左に書き換える(-32px)
}
//2 or ↓ キー押したら...
if(pressedkey=='2' || downedkey =='98' || downedkey =='40'){
chgPenImg(img_d.src) // 下向き「ペンギン」に書き換える
bord1.innerHTML = "" // ボード文字クリア
ypos += 32 // 位置yを1コマ分下に書き換える(+32px)
}
//「とる?」が出てれば
if(torux!=0)
{
//13 キー押したら...
if(downedkey=='13')
{
//判定へ
hantei('_'+torux+'_'+toruy+'_')
}
chgKusaImg( '_'+torux+'_'+toruy+'_' , img_k1.src )
msg = ""
}
// 移動先位置に「植え込み」があったときの処理
if( xy.indexOf('_'+xpos+'_'+ypos+'_') !=- 1 )
{
chgKusaImg('_'+xpos+'_'+ypos+'_',img_k2.src) //「とる?」画像
msg = '改行キーでお宝GET!!' // メッセージ
torux = xpos //「とる?」が出てるフラグ
toruy = ypos //「とる?」が出てるフラグ
xpos = xpos_old // 「ペンギン」を元の位置xへ戻す
ypos = ypos_old // 「ペンギン」を元の位置yへ戻す
}
//「ペンギン」を移動
moveLAYERoj(penlayerStyle,xpos,ypos)
dofollow()
}
/***
* 判定とあたりすか処理
*
*/
//あたり時の共通処理
function atari(syurui,ichi,getItem)
{
getslength++ //お宝数カウントアップ
bord1.innerHTML = msg //メッセージ
bord1.style.backgroundColor = "orange" //メッセージ背景色を変更
bord3.innerHTML += "<img src=" + getItem +">"//ゲットしたお宝画像表示
if(getslength%8==0) bord3.innerHTML += "<br>"//ゲットお宝8個で一旦改行
document.images['i'+ichi].height = 0 //「植え込み」を消す
xy=xy.split(syurui+ichi+";").join("") //「植え込み」データも消す
document.bgColor = "orange" //背景色フラッシュ
document.bgColor = "#ffffff" //背景色フラッシュ戻す
xpos = parseInt(ichi.split('_')[1]) //「ペンギン」位置xをGET先へ
ypos = parseInt(ichi.split('_')[2]) //「ペンギン」位置yをGET先へ
moveLAYERoj(penlayerStyle,xpos,ypos) //「ペンギン」を移動する
}
//すか時の共通処理
function suka(syurui,ichi)
{
bord1.innerHTML = msg //すかメッセージ
bord1.style.backgroundColor = "red" //メッセージ背景色を変更
document.images['i'+ichi].height = 10 //「植え込み」を小さくする
}
// 判定
function hantei(ichi)
{
//記録からお宝データを抽出する
var syurui = xy.charAt(xy.indexOf(ichi)-1)
switch(syurui)
{
case "1" : msg = "植え込み 1pt ゲット!" ;
pscore += 1 ;
atari(syurui,ichi,img_k1.src) ;
break ;
case "2" :
msg = "指輪 5pt ゲット!!" ;
pscore += 5 ;
atari(syurui,ichi,img_r1.src) ;
break ;
case "3" :
msg = "王冠 10pt ゲット!!!" ;
pscore += 10 ;
atari(syurui,ichi,img_o1.src) ;
break ;
case "4" :
msg = "すか" ;
pscore -= 1 ;
suka(syurui,ichi) ;
break ;
case "5" :
msg = "すか" ;
pscore -= 1 ;
suka(syurui,ichi) ;
break ;
defaurt :
msg = "すか" ;
pscore -= 1 ;
suka(syurui,ichi) ;
}
}
/***
* スコア送信
*
*/
//NGWへスコア送信処理
function syoubu(){
GRS_score = pscore //スコアをセットする
GRS_sendScore() //送信する
}
//-->
</script>
<!-- 「植え込み」用DIV -->
<div id='kusa' style='position:absolute;left:0px;top:0px'>
</div>
<!-- タイトル用DIV -->
<div id="title" style="position : absolute ;
left : 0px ;
top : 0px ;
font-size: 12px ;
">
<img name="otakara" src="./otakara.gif" borders=0 align = "left"
><span style="width:250px;padding : 12px ">
制限時間内に「ペンギン」を動かして
植え込みに隠されたお宝を改行キーでGET!。
GETしたら植え込みが消え、スカだと植え込み
は小さくなりますが通れません。</span>
<br clear="all"></div>
<!-- 「ペンギン」用DIV -->
<div id="penLay" style="position : absolute ;
left : 50px ;
top : 80px ;">
<img name="pen" src="./pen.gif" width=28 height=30 borders=0>
</div>
<!-- スタートボタン用DIV -->
<div id="me" style="position : absolute ;
left : 80px ;
top : 80px ;"><form>
<input type="button" value="Game start !"
onfocus="blur()"
onclick="main()"></form>
</div>
<!--スコア送信モジュール -->
<script
language="JavaScript"
src="http://game.gr.jp/GameWeb/GameCenter/***.js">
</script>
【動作ブラウザ】
win mac linux
n4 n6 n7 m1 e5 e6 o7 n4 n6 n7 m1 e5 s1 n4 n6 n7 m1 k3
× ○ ○ ○ ○ ○ ○ × ○ ○ ○ ○ ○ × ○ ○ ○ ×
////////////////////////////////////////////////////////////////////
*上記サンプルの動作環境は各サンプルをご確認ください
====================================================================
凡例
Win
n3 -- NetscapeNavogator 3.x
n4 -- NetscapeNavogator 4.x
n6 -- NetscapeNavogator 6.x
n7 -- NetscapeNavogator 7.x
m1 -- Mozilla1.x
e4 -- Internet Explorer 4.x
e5 -- Internet Explorer 5.x
e6 -- Internet Explorer 6.x
o6 -- Opera 6.0
o7 -- Opera 7.0
Mac
n3 -- NetscapeNavogator 3.x
n4 -- NetscapeNavogator 4.x
n6 -- NetscapeNavogator 6.x
n7 -- NetscapeNavogator 7.x
m1 -- Mozilla1.x
e4.5 -- Internet Explorer 4.5
e5 -- Internet Explorer 5.0 または 5.1
s1 -- Safari
Linux
n3 -- NetscapeNavogator 3.x
n4 -- NetscapeNavogator 4.x
n6 -- NetscapeNavogator 6.x
n7 -- NetscapeNavogator 7.x
m1 -- Mozilla1.x
k3 -- Konqueror 3.x
--------------------------------------------------------------------
|