【プチコン4講座】1~4回目のコードまとめ&関数を覚えよう
こんにちは。継続の錬金術士なおキーヌです。
ブログ毎日更新は212日目になります。
第4回「背景を表示してみよう」にて背景を描写することに成功しました。
ここまではシューティングゲーム作りに必要なことを別々にコードを書いていきましたが、そろそろ本格的にSTG作りを始めましょう。
今までやってきたコードをまとめるのと、今後プログラミングがとても便利になる関数という仕組みを学んでいきます。
それではプチコン4でSTG作りその第5回目始めましょう。
学んだことを使ってゲームを考えてみよう
ココまで覚えたことだけでも1つのゲームに仕上げることが出来るんだけど、気付いてた?
え?もうゲームになってたのか?
ぶつかる判定についてはまだ教えてないから凄く単純なゲームしか作れないけど考え方によっては色々作れるね。今まで覚えたことをリスト化してみるといいよ。
わかったわ。ちょっとまとめてみるわね。
- キャラクターを出す
- キャラクターを動かす
- コントローラーを認識させる
- 条件式を使って上下左右に動かす
- 配列を使ってマップデータを作る
- 背景を表示する
こんなところかしら?
これだけの仕組みを作って1つゲームを考えてみて。面白くなくてもいいよ。ゲームとして成立するならなんでも大丈夫。
うーん……
マップで迷路みたいに作って、キャラクターを動かしてゴールを目指すゲームとか?小さいころノートに迷路を書いて友達とやってたような感じなんだけど……
うん、ゲーム的には現状壁をすり抜けちゃえるけどちゃんとルールを守ってやれば迷路ゲームとしては成立するね。合格だよ。
やった♪
……!思いついたぜ!こんなのはどうだ?
ひらめいたみたいだね。どんなゲーム?
キャラクター動かして画面をぐるぐると回ってどんどん中央にいかせるんだ。そして完全に中央に行ったら失敗。プレイヤーは出来る限り中央のところでボタンを押してキャラクターを止めて中央に近いほうのやつの勝ち!ってゲームはどうだ?
チキンレース風のゲームでいいね。白熱しそうなゲームだよ。
あ、なんかリキの方が面白そうだわ……ちょっと悔しいなー
伊達にゲームしてねぇぜ!
こうやって自分のできる範囲で考えてゲームを作ると案外面白いゲームができたりするんだ。昔のファミコンゲームが良い例だね。制限の多い中いかに考えている内容を再現するかがゲームプログラマーの腕の見せ所だったんだ。
今までやったコードをまとめてみよう
それじゃあ今回は今までやったコードをきれいにまとめてみようか。以下の内容をもとに組み立ててみてね。
- スプライトの準備と表示(SPSET, SPOFS)
- 背景の準備と表示(TPUT)
- コントローラーの制御(Button(0))
ここは俺に任せろ!
ACLS
' ボタンの状態を格納する変数
var B
' 移動用変数
var X = 0, Y = 0
' ループ用変数
var I = 0, J = 0
' マップ配列
DIM MAPDATA[] = [\
0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,\
0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\
]
' マップの描写
for J=0 to 14
for I=0 to 24
if MAPDATA[I * (J+1)] == 0 then
TPUT 0, I, J, CHR$(&HE8D8)
elseif MAPDATA[I * (J+1)] == 1 then
TPUT 0, I, J, CHR$(&HE8D9)
endif
next
next
' スプライトを定義
SPSET 0, 352
' ループ開始
loop
' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
B = Button(0)
' 押されているボタンの情報を表示
if (B AND 1 << #B_LUP) != 0 then
' 上を押されたらY座標を減算
DEC Y
elseif (B AND 1 << #B_LDOWN) != 0 then
' 下を押されたらY座標を加算
INC Y
endif
if (B AND 1 << #B_LLEFT) != 0 then
' 左を押されたらX座標を減算
DEC X
elseif (B AND 1 << #B_LRIGHT) != 0 then
' 右を押されたらX座標を加算
INC X
endif
' プレイヤーの移動
SPOPS 0, X, Y
' 垂直同期
VSYNC
endloop
うん、完璧だね。
だろ!
でも前回の最後に言ったように全面砂漠にして障害物はスプライトにするんじゃなかったっけ?
ハッ!そうだった!ってことはマップ配列は要らないってことか。
そうだね。全面砂漠にするにはそのままTPUTでループをしてもいいんだけどもっといい方法があるよ。
トモ君トモ君!私にやらせて!
マユミちゃん、もしかして予習してきたの?
ふふん!まぁ見ててよ!
ACLS
' ボタンの状態を格納する変数
var B
' 移動用変数
var X = 0, Y = 0
' マップの塗りつぶし描写
TFILL 0,0,0,24,14,CHR$(&HE8D8)
' スプライトを定義
SPSET 0, 352
' ループ開始
loop
' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
B = Button(0)
' 押されているボタンの情報を表示
if (B AND 1 << #B_LUP) != 0 then
' 上を押されたらY座標を減算
DEC Y
elseif (B AND 1 << #B_LDOWN) != 0 then
' 下を押されたらY座標を加算
INC Y
endif
if (B AND 1 << #B_LLEFT) != 0 then
' 左を押されたらX座標を減算
DEC X
elseif (B AND 1 << #B_LRIGHT) != 0 then
' 右を押されたらX座標を加算
INC X
endif
' プレイヤーの移動
SPOPS 0, X, Y
' 垂直同期
VSYNC
endloop
おぉ、素晴らしいね。TFILLはこの後教えようと思ってたんだけど手間が省けたね。
テキストスクリーンについて調べてたら良さそうなのがあったから試してみたらやりたいことにピッタリだったのよ!
TFILLってなんだ??
簡単に説明すると指定のマップチップで開始地点から終了地点まで塗りつぶす命令なんだ。今回は砂漠を敷き詰めるだけに切り替えたからループを使ってやるより
こっちの方がシンプルに書けるんだよ。細かいところは違うかもしれないけど入れ子ループを使った時とほぼ同じ処理をしてるよ。
公式で用意されててなおかつ1行で書けるなら確かにこっちの方がいいな!
先にループで描写するやり方は別のゲームにも使える手法だから覚えておいて損はないよ。
でもなーんかしっくりこないのよね……ごちゃごちゃしてるっていうか……
安心して、次教える事でだいぶ見通しがよくなるようになるよ。
関数(命令)の仕組みを学ぼう
今から関数について勉強していくよ。
げ、俺アレ学生時代ずっと苦手だったんだよな……
数学の時の関数とは別物だから安心して。かなり簡単だから。
とりあえず今のコードを関数を使って書いてみたからよく見てね。
' ゲーム初期化
ACLS
' ボタンの状態を格納する変数
var B
' 移動用変数
var X = 0, Y = 0
D_Initialize
' ループ開始
loop
D_Controller
D_PlayerMove
' 垂直同期
VSYNC
endloop
' ゲーム初期化
'───────────────────────────────
def D_Initialize
' マップ描写
D_MapDraw
' スプライト初期化
D_SpriteInitialize
end
' コントローラー
'───────────────────────────────
def D_Controller
' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
B = BUTTON(0)
' 押されているボタンの情報を表示
if (B AND 1 << #B_LUP) != 0 then
' 上を押されたらY座標を減算
DEC Y
elseif (B AND 1 << #B_LDOWN) != 0 then
' 下を押されたらY座標を加算
INC Y
endif
if (B AND 1 << #B_LLEFT) != 0 then
' 左を押されたらX座標を減算
DEC X
elseif (B AND 1 << #B_LRIGHT) != 0 then
' 右を押されたらX座標を加算
INC X
endif
end
' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
' スプライトを定義
SPSET 0, 352
end
' スプライト(プレイヤー)の移動
'───────────────────────────────
def D_PlayerMove
' プレイヤーの移動
SPOPS 0, X, Y
end
' マップの描写
'───────────────────────────────
' マップの塗りつぶし描写
def D_MapDraw
TFILL 0,0,0,24,14,CHR$(&HE8D8)
end
な、なんだ!?もともとあったコードが下にまとまっているし何だこのDEFってのは!?
落ち着いて、上から説明していくよ。まず「D_Initialize」ってのが関数なんだ。関数には大きく2種類あってプチコン4に組み込まれている組み込み命令と自分で作ることができるユーザー定義命令があるんだ。今回やったのはユーザー定義命令の方だね。ちなみに組み込み命令は「PRINT」とか「TPUT」が該当するよ。
このコードをみてなんか喉のつかえがとれた感じだわ♪でもプログラムって確か上から順に動くんじゃなかったっけ?
うん、それは変わってないよ。ユーザー命令が呼ばれたらそれを定義している場所に処理がジャンプして終わったら呼び出されたところに戻るイメージをもってくれたらいいかな。
ちなみにプチコン4ではこのように呼び出すだけの場合は命令といって「Button(n)」のようにカッコを付けて呼び出すのを関数というんだ。
更に言うと関数のカッコの中に入れる数字や文字列は引数っていうんだよ。
命令化しているとなんかメリットはあるのかしら?
一番のメリットは同じコードを2度書かなくてもよくなるってことかな。命令じゃなくて関数にすれば引数を与えれば結果を変更することもできるんだ。
結果を変えるか……例えばじゃんけんゲームとかに使えそうな感じがするな。
そうだね。基本的なことを関数にまとめてプレイヤーの選んだ手を引数として与えれば「グーチョキパー」のどれか同じコードで呼び出せることができるわけさ。
それに一度作っておけば別のゲームを作るときに同じような処理の場合コピーしてきたら時間の節約にもなるからできる限り処理は関数化しておくことをオススメするよ。
なるほどな……この「D_Intialize」に飛ぶと……ACLSから始まって……あれ?命令の中でまた命令を読んでるぞ。こんなこともできるのか。
関数化(命令化)は1つの部品にして何度もどこでも使いまわせるって考えるとより分かりやすくなるかな。
こりゃ便利だな!同じコードを書かなきゃいけないのって面倒だもんなぁ!
なんでもかんでも関数化すりゃいいってわけでもないけどね。とりあえず書いてみてまとめられそうだなって思ったら関数化してみるといいよ。関数化したことによって扱いにくくなることもでてくるから臨機応変って感じだね。
私はなるべく関数化しておきたいわ。だってエラーが起きたときメンテナンスしやすいじゃない。
そうだね。ずっと続けて書いているとエラーの場所を探すのがしんどい時もあるし、実際エラーが出た箇所より前のところでエラーがでてるってこともあるからメンテナンスにおいて関数化はとても有用だよ。
ところでこの「D_」って頭についてる名前は軟化意味があるのか?
プログラム的な決まりはないんだけど頭に「D_」がついていたらユーザー定義命令or関数なのかが1発で判るようにボクが勝手につけているだけだよ。
ちなみにプログラムのどこでも使える変数をグローバル変数っていうんだけど……今の時点でいえばプレイヤーのXとY座標とかの名前を「G_X」とか「G_Y」にしてあげればグローバル変数って1発で判るってわけさ。
どこでも使えない変数っていうのがあるの?
うん、それをローカル変数っていうんだけど例えば関数の中でしか使わない変数は、関数の中で定義してあげればそこ以外からは使えないんだ。そして関数が終わったらその変数は消滅するよ。(厳密には特定のタイミングで削除される)
そっか、なんでもかんでもグローバル変数にするべきではないのね。
どこからでもアクセスできるとバグの原因になることもあるから、できる限りグローバル変数は使わないほうがいいんだよ。使うのは明らかにどこからでもアクセスすべき変数だけにしておこうね。
BASIC時代は関数なんて便利なものがなかったので、GOTOを使って指定の場所に処理を飛ばすという方法を取っていたのですが
便利なあまり乱用するとどこに処理の順番がすごく追いづらいというデメリットが生じます。
複雑に絡み合っているのでこれをスパゲッティコード・スパゲッティプログラムとか言われたりします。
一方関数であれば呼び出し元に処理が戻るのでわかりやすくていいですね。
余談ですが、「GOTO」と指定のラベルにジャンプできる「GOSUB」の方が処理速度が速いそうです。
GOSUBもreturn命令を使えば関数と同じような処理ができるようなのでお好みですね。
正直処理落ちしない範囲であるならDEF ~ ENDで統一しておいた方がいいでしょう。
他のプログラミング言語ではGOTOやGOSUBはあまり実装されていませんので
プログラマーを目指すのであれば「DEF」による関数プログラミングに慣れておくと後々幸せになれます。
しかしこれはあくまで私の主観なので自分のやりやすいと思ったプログラムを書いて構いません。
自由に書けるのがプログラミングの一番楽しいところなのですから。
シューティングゲーム作り講座第5回まとめ
- TFILL スクリーンID, 開始X座標, 開始Y座標, 終了X座標, 終了Y座標, 文字コード
-
TPUTと同じで1で16×16のサイズを使用します。なので画面最大横幅は25チップ分、縦幅は15チップ分となります。
-
左上から右下まで敷き詰めたいときは「0,0,24,14」を設定すればOKです。
- DEF ~ END
-
ユーザー定義命令です。引数は持たないので決まった処理をまとめたい時に使います。
- DEF() ~ END
-
ユーザー定義関数です。引数を渡すことができます。
-
処理の終わりには「return」命令で必ず値を返さなければエラーがでます。詳しくは公式リファレンスを見てください。
今回は1~4回目のまとめと命令&関数の作り方を覚えました。
ここから本格的にシューティングゲームを作っていきます。
次回はシューティングゲームらしくするためにボタンを押したら弾を発射できるようにしてみましょう。
それでは。