#はじめに
本当は別のテーマで書く予定でしたが、コーディングが詰んでしまい、急遽内容を変更しました...。
今回は、今年の夏前にプチコン4で作ったスネークゲームについて書かせていただきました。
console.log()でスネークゲームを出力する
https://qiita.com/prononami/items/7571ce9bb382981a56bb
実はスネークゲームに関してはOrganizations内に先駆者がいますが、気にせず続けます。
#プチコン4
プチコン4公式サイト
https://www.petc4.smilebasic.com/
プチコン4は、Nintendo Switch上でプログラミングを楽しめるソフトウェアです。
というものです。価格は税込3,000円です。安い!
使用できる言語は「SmileBASIC」です。
#SmileBASIC
SmileBASICのリファレンスはこちら。
##特徴
- 型定義が不要、行末にセミコロン(;)が不要など、文法がシンプル。
- グラフィックや音声などの出力が楽
- オブジェクト指向ができない(それっぽいことはできるが制限あり)
正直言えば、Javaなどに慣れている自分からすると、少々書きにくかった印象があります。確かに初学者には易しいと思いますが。
#スネークゲーム実装
概要はこれくらいにして、実際にゲームを作っていきます。
まずは簡単に設計(のようなもの)から見ていきます。
また、今回はコンソール出力でゲームを表示します。本当にだだ被り...
##実装
###進む方向を決める
進む方向を決めるBECTOR関数です。(例によって妙な命名)
// 方向と要素番号を対応
CONST #RIGHT = 0
CONST #UP = 1
CONST #LEFT = 2
CONST #DOWN = 3
// フレーム数
CONST #SPEED = 20
CONST #DASH = 4
// 蛇が向いている方向
DIM bector[2]
// 方向ごとに加算する値
DIM bectors[4, 2]
bectors[#RIGHT, 0] = 1 : bectors[#RIGHT, 1] = 0
bectors[#UP , 0] = 0 : bectors[#UP , 1] = -1
bectors[#LEFT , 0] = -1 : bectors[#LEFT , 1] = 0
bectors[#DOWN , 0] = 0 : bectors[#DOWN , 1] = 1
DEF BECTOR
// 20フレーム経過したらループ終了
FOR i = 0 TO #SPEED - 1
IF BUTTON(CONTROLLER(1), #B_LRIGHT) == 1 THEN
bector[0] = bectors[#RIGHT, 0]
bector[1] = bectors[#RIGHT, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_LUP) == 1 THEN
bector[0] = bectors[#UP, 0]
bector[1] = bectors[#UP, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_LLEFT) == 1 THEN
bector[0] = bectors[#LEFT, 0]
bector[1] = bectors[#LEFT, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_LDOWN) == 1 THEN
bector[0] = bectors[#DOWN, 0]
bector[1] = bectors[#DOWN, 1]
ENDIF
// Aボタンでダッシュ
IF BUTTON(CONTROLLER(1), #B_RRIGHT) == 1 THEN
// 4フレーム待つ
VSYNC #DASH
// ループ脱出
BREAK
ENDIF
// 1フレーム待つ
VSYNC 1
NEXT
END
IFの中の判定ですが、
BUTTON(CONTROLLER(1), #B_LRIGHT) == 1
これは「Switch1Pの左コントローラーの右ボタン(いわゆる十字キー右)が押された状態か」を判定しています。
#B_LRIGHT
は、そのボタンを表す定数で、プチコン4ではデフォルトで使用できます。また、SmileBASICでの真偽値は、TRUEとFALSEではなく0と1を使っています。(1が真)
BECTOR関数では、ループ1回で全方向のボタンの状態を確認し、1フレーム待ち、それを20フレーム分繰り返しています。
###進む先に何があるか判定
// ゲーム領域の限界値
CONST #BOX_LEFT = 1
CONST #BOX_RIGHT = 48
CONST #BOX_TOP = 1
CONST #BOX_BOTTOM = 28
// 蛇の体
CONST #SNAKE$ = "#"
// アイテム
CONST #NEW_HEAD$ = "ロ"
// 現在の頭の座標
DIM head[2]
// 次の頭の座標
DIM next_head[2]
// next_headにある文字のコード
DIM next_head_code
DEF CHECK_NEXT
// 次の頭の位置を計算
next_head[0] = head[0] + bector[0]
next_head[1] = head[1] + bector[1]
// 壁(ゲーム領域の限界)より外ならゲームオーバー
IF next_head[0] < #BOX_LEFT OR next_head[0] > #BOX_RIGHT OR next_head[1] < #BOX_TOP OR next_head[1] > #BOX_BOTTOM THEN
RETURN
ENDIF
next_head_code = CHKCHR(next_head[0], next_head[1])
// アイテム(新しい体)の場合
IF next_head_code == ASC(#NEW_HEAD$) THEN
NEW_HEAD
// 自分の体の場合
ELSEIF next_head_code == ASC(#SNAKE$) THEN
RETURN
// 何もない場合
ELSE WALK
ENDIF
END
NEW_HEADやWALKは、後述する関数の名前です。
CHKCHR(x, y)
CHKCHRは、コンソール上の指定位置に表示されている文字のコードを返却する関数です。
ASC(char)
ASCは、引数に渡した文字のコードを返却する関数です。
next_head_code == ASC(#NEW_HEAD$)
次の頭の位置にある文字が何かを、コードで判定しています。
該当した文字によって、次の処理を分岐させています。
###歩く
// 最後尾の座標
DIM tail[2]
// 頭の座標
DIM head[2]
DEF WALK
// 最後尾の位置の体(#)を消去
LOCATE tail[0], tail[1]
? " "
SET_BODY next_head
次の頭の位置に体(#)を表示
LOCATE head[0], head[1]
? #SNAKE$
// BECTOR関数へ移行
BECTOR
END
// 蛇の体全体を表す二次元配列
// 体の位置の座標を格納
DIM body[1500,2]
DIM head[2]
DIM tail[2]
// 蛇の体の長さ
DIM snake_cnt
// 引数:next_head[2](次の頭の位置)
DEF SET_BODY next_haed
// 体の位置を1つずつズラす
FOR i = 0 TO snake_cnt - 2
body[i, 0] = body[i + 1, 0]
body[i, 1] = body[i + 1, 1]
NEXT
// 頭の位置に引数の値を格納
body[snake_cnt - 1, 0] = next_head[0]
body[snake_cnt - 1, 1] = next_head[1]
SET_HEAD
SET_TAIL
END
// 頭の位置を保持
DEF SET_HEAD
head[0] = body[snake_cnt - 1, 0]
head[1] = body[snake_cnt - 1, 1]
END
// 最後尾の位置を保持
DEF SET_TAIL
tail[0] = body[0, 0]
tail[1] = body[0, 1]
END
「体のためにわざわざ配列使わなくてもいいかな」とも思いましたが、消去する尻尾の判定ができなくなる(できても計算量が多くなる)ので、このようにしました。
###新しい体を追加する
// 点数
DIM score
DEF NEW_HEAD
// scoreを1増やす
// 「score++」と同じ
INC score
LOCATE 7, 0
? score
// 体を増やす処理へ移行
ADD_BODY next_head
LOCATE next_head[0], next_head[1]
? #SNAKE$
// 次のアイテムの出現位置を決める
LOOP
new_head[0] = RND(48) + 1
new_head[1] = RND(48) + 1
chr = CHKCHR(new_head[0], new_head[1])
// 蛇の体がある位置には出現させない
IF chr != ASC(#SNAKE$) THEN BREAK
ENDLOOP
LOCATE new_head[0], new_head[1]
? #NEW_HEAD$
// BECTOR関数へ移行
BECTOR
END
// 新しい体を追加する
// 引数:新しい体の位置
DEF ADD_BODY new_head
body[snake_cnt, 0] = new_head[0]
body[snake_cnt, 1] = new_head[1]
INC snake_cnt
SET_HEAD
END
頭の位置は変わりますが、尻尾の位置は変わらないので、SET_HEADのみを実行します。
体の配列で、0番目を頭ではなく尻尾にした理由は、体を追加する処理をラクにするためです。(結果論)
##コード全体
以上の処理を整理し、多少の演出を加え、完成したコードがこちらです。(長いので折りたたんでます)
ソースコード全文
// 蛇の初期位置
CONST #CENTER_X = 25
CONST #CENTER_Y = 15
// ゲーム領域の限界値
CONST #BOX_LEFT = 1
CONST #BOX_RIGHT = 48
CONST #BOX_TOP = 1
CONST #BOX_BOTTOM = 28
// オブジェクトで使う文字
CONST #SNAKE$ = "#"
CONST #NEW_HEAD$ = "ロ"
CONST #MISS$ = "※"
// 進む方向を表す定数
CONST #RIGHT = 0
CONST #UP = 1
CONST #LEFT = 2
CONST #DOWN = 3
// フレーム数
CONST #SPEED = 20
CONST #DASH = 4
DIM body[1500,2]
DIM head[2]
DIM tail[2]
DIM snake_cnt
DIM new_head[2]
DIM bector[2]
DIM bectors[4, 2]
bectors[#RIGHT, 0] = 1 : bectors[#RIGHT, 1] = 0
bectors[#UP , 0] = 0 : bectors[#UP , 1] = -1
bectors[#LEFT , 0] = -1 : bectors[#LEFT , 1] = 0
bectors[#DOWN , 0] = 0 : bectors[#DOWN , 1] = 1
DIM next_head[2]
DIM next_head_code
DIM score
DEF SET_HEAD
head[0] = body[snake_cnt - 1, 0]
head[1] = body[snake_cnt - 1, 1]
END
DEF SET_TAIL
tail[0] = body[0, 0]
tail[1] = body[0, 1]
END
DEF SET_BODY next_haed
FOR i = 0 TO snake_cnt - 2
body[i, 0] = body[i + 1, 0]
body[i, 1] = body[i + 1, 1]
NEXT
body[snake_cnt - 1, 0] = next_head[0]
body[snake_cnt - 1, 1] = next_head[1]
SET_HEAD
SET_TAIL
END
DEF ADD_BODY new_head
body[snake_cnt, 0] = new_head[0]
body[snake_cnt, 1] = new_head[1]
INC snake_cnt
SET_HEAD
END
// 蛇の初期化
DEF RESET_BODY
body[0, 0] = #CENTER_X
body[0, 1] = #CENTER_Y
snake_cnt = 1
score = 0
SET_HEAD
SET_TAIL
END
// ゲーム画面作成
DEF STAGE_CREATE
// Clean Screen
CLS
// 長方形を表示する
GBOX 7, 7, 392, 232, #C_WHITE
COLOR #C_WHITE
LOCATE 1, 0
? "SCORE:0"
LOOP
new_head[0] = RND(48) + 1
new_head[1] = RND(28) + 1
IF new_head[0] != #CENTER_X OR new_head[1] != #CENTER_Y THEN BREAK
END LOOP
LOCATE #CENTER_X, #CENTER_Y
? #SNAKE$
END
// ゲーム開始
DEF GAME_START
// BGMを流す
BGMPLAY 27
// 最初はボタンが押されるまで動かない
bector[0] = 0
bector[1] = 0
LOOP
IF BUTTON(CONTROLLER(1), #B_LRIGHT) == 1 THEN
bector[0] = bectors[#RIGHT, 0]
bector[1] = bectors[#RIGHT, 1]
BREAK
ENDIF
IF BUTTON(CONTROLLER(1), #B_LUP) == 1 THEN
bector[0] = bectors[#UP, 0]
bector[1] = bectors[#UP, 1]
BREAK
ENDIF
IF BUTTON(CONTROLLER(1), #B_LLEFT) == 1 THEN
bector[0] = bectors[#LEFT, 0]
bector[1] = bectors[#LEFT, 1]
BREAK
ENDIF
IF BUTTON(CONTROLLER(1), #B_LDOWN) == 1 THEN
bector[0] = bectors[#DOWN, 0]
bector[1] = bectors[#DOWN, 1]
BREAK
ENDIF
ENDLOOP
// 次の頭の位置のチェックへ
CHECK_NEXT
END
DEF CHECK_NEXT
next_head[0] = head[0] + bector[0]
next_head[1] = head[1] + bector[1]
IF next_head[0] < #BOX_LEFT OR next_head[0] > #BOX_RIGHT OR next_head[1] < #BOX_TOP OR next_head[1] > #BOX_BOTTOM THEN
RETURN
ENDIF
next_head_code = CHKCHR(next_head[0], next_head[1])
IF next_head_code == ASC(#NEW_HEAD$) THEN
NEW_HEAD
ELSEIF next_head_code == ASC(#SNAKE$) THEN
RETURN
ELSE WALK
ENDIF
END
DEF BECTOR
FOR i = 0 TO #SPEED - 1
IF BUTTON(CONTROLLER(1), #B_LRIGHT) == 1 THEN
bector[0] = bectors[#RIGHT, 0]
bector[1] = bectors[#RIGHT, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_LUP) == 1 THEN
bector[0] = bectors[#UP, 0]
bector[1] = bectors[#UP, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_LLEFT) == 1 THEN
bector[0] = bectors[#LEFT, 0]
bector[1] = bectors[#LEFT, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_LDOWN) == 1 THEN
bector[0] = bectors[#DOWN, 0]
bector[1] = bectors[#DOWN, 1]
ENDIF
IF BUTTON(CONTROLLER(1), #B_RRIGHT) == 1 THEN
VSYNC #DASH
BREAK
ENDIF
VSYNC 1
NEXT
CHECK_NEXT
END
DEF WALK
LOCATE tail[0], tail[1]
? " "
SET_BODY next_head
LOCATE head[0], head[1]
? #SNAKE$
BEEP 59, 0, 16
BECTOR
END
DEF NEW_HEAD
INC score
LOCATE 7, 0
? score
ADD_BODY next_head
LOCATE next_head[0], next_head[1]
? #SNAKE$
BEEP 12
LOOP
new_head[0] = RND(48) + 1
new_head[1] = RND(48) + 1
cnt = CHKCHR(new_head[0], new_head[1])
IF cnt != ASC(#SNAKE$) THEN BREAK
ENDLOOP
LOCATE new_head[0], new_head[1]
? #NEW_HEAD$
BECTOR
END
DEF MISS
LOCATE head[0], head[1]
? #MISS$
BGMSTOP
BEEP 13
VSYNC 150
END
// メイン処理
STAGE_CREATE
GAME_START
MISS
実際に処理が始まるのは、最後の方にある「STAGE_CREATE」からです。あまりに簡略化されて分かりにくいですが、イメージはこんな感じです。
実際のゲーム画面です
↓アイテムを取ると、蛇の体とスコアが増え、新たなアイテムが出現
#総括
- 過去に作ったコードを見直すと、結構粗が見えた。自分の実力が上がったということにしたい
- 方向を決める部分で、実は若干操作性が悪い。フレーム関連と仲良くしないといけないかもしれない。
- 記事用にコードを手打ちで書き写すのが一番大変でした。
今回書く予定だったけど断念したものに関しては....なんとかします....。
#オマケ
このスネークゲーム、アプデにアプデを重ね、今はこんなことになっています。
実装したもの
- 障害物
- 障害物を壊せるアイテム
- 残機
- 強制方向転換
- ワープ
- 首尾逆転
- 強制ダッシュ
何のゲームだっけな...。