Help us understand the problem. What is going on with this article?

SmileBASIC(プチコン4)で作る、たぶん一番簡単なスネークゲーム

はじめに

本当は別のテーマで書く予定でしたが、コーディングが詰んでしまい、急遽内容を変更しました...。

今回は、今年の夏前にプチコン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などに慣れている自分からすると、少々書きにくかった印象があります。確かに初学者には易しいと思いますが。

スネークゲーム実装

概要はこれくらいにして、実際にゲームを作っていきます。
まずは簡単に設計(のようなもの)から見ていきます。

設計

以下、フローチャートです。
SNAKE (1).jpg

また、今回はコンソール出力でゲームを表示します。本当にだだ被り...

実装

進む方向を決める

進む方向を決めるBECTOR関数です。(例によって妙な命名)

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フレーム分繰り返しています。

進む先に何があるか判定

CHECK_NEXT
// ゲーム領域の限界値
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$)

次の頭の位置にある文字が何かを、コードで判定しています。
該当した文字によって、次の処理を分岐させています。

歩く

WALK
// 最後尾の座標
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
SET_BODY
// 蛇の体全体を表す二次元配列
// 体の位置の座標を格納
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
SET_HEAD
// 頭の位置を保持
DEF SET_HEAD
  head[0] = body[snake_cnt - 1, 0]
  head[1] = body[snake_cnt - 1, 1]
END
SET_TAIL
// 最後尾の位置を保持
DEF SET_TAIL
  tail[0] = body[0, 0]
  tail[1] = body[0, 1]
END

図でいうとこんな感じです。
SNAKE_WALK.jpg

「体のためにわざわざ配列使わなくてもいいかな」とも思いましたが、消去する尻尾の判定ができなくなる(できても計算量が多くなる)ので、このようにしました。

新しい体を追加する

NEW_HEAD
// 点数
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
ADD_BODY
// 新しい体を追加する
// 引数:新しい体の位置
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番目を頭ではなく尻尾にした理由は、体を追加する処理をラクにするためです。(結果論)

コード全体

以上の処理を整理し、多少の演出を加え、完成したコードがこちらです。(長いので折りたたんでます)

ソースコード全文
MAIN.PRG
// 蛇の初期位置
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」からです。あまりに簡略化されて分かりにくいですが、イメージはこんな感じです。
SNAKE_FINALLY.jpg

実際のゲーム画面です

↓アイテムに向かう
SNAKE_Qiita_1.jpg

↓アイテムを取ると、蛇の体とスコアが増え、新たなアイテムが出現
SNAKE_Qiita_2.jpg

↓無駄に200くらいまで頑張ってみた
SNAKE_Qiita_3.jpg

総括

  • 過去に作ったコードを見直すと、結構粗が見えた。自分の実力が上がったということにしたい
  • 方向を決める部分で、実は若干操作性が悪い。フレーム関連と仲良くしないといけないかもしれない。
  • 記事用にコードを手打ちで書き写すのが一番大変でした。

今回書く予定だったけど断念したものに関しては....なんとかします....。

オマケ

このスネークゲーム、アプデにアプデを重ね、今はこんなことになっています。
SNAKE_Qiita_4.jpg

実装したもの

  • 障害物
  • 障害物を壊せるアイテム
  • 残機
  • 強制方向転換
  • ワープ
  • 首尾逆転
  • 強制ダッシュ

何のゲームだっけな...。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした