Arduboyでゲームを作ろう


はじめに

(この記事はゲーム制作 Advent Calendarの7日目の記事です.)
この記事ではArduboyでのゲームプログラミングを解説します.読者としてはゲームプログラミングに詳しくてArduboyを始めたい人と、Arduboyから初めてゲームプログラミングをやりたい人の両方を想定しています.

そこで,Arduboyを持っていなくてもイメージが出来るように動画も使って,この記事を読めばArduboyでのゲームプログラミングに必要な基本的なことが全て分かることを目標に書きました.

Arduinoでのプログラムの書き方

ArduinoはC/C++を用いてプログラミングを行います.C言語は全てサポートされているようですが.C++の全てがサポートされているわけではないそうです.(あまり気にする場面はないですが.)

Arduinoで使える関数は公式サイトを確認してください.

Arduinoのプログラムは必ずsetup関数とloop関数を含んでいる必要があります.setup関数は起動時に一度だけ実行されます.setup関数が実行された後はloop関数が繰り返し実行されます.逆にmain関数は必要ありません.コンパイル時にmain関数を適切に補った後でハードにプログラムを書き込むようです.

最低限のことを説明したので,いくつかのプログラムを見ていきましょう.

Hello World

ArduboyのHello Worldを通して基本的なことを説明します.

helloWorld.ino
#include <Arduboy2.h>

Arduboy2 arduboy;

void setup() {
  arduboy.begin();
  arduboy.clear();

}

void loop() {
  arduboy.clear();

  arduboy.setCursor(0,0);
  arduboy.print("Hello World");

  arduboy.display();
}

このプログラムを実行すると以下のようになります.


一つずつ説明していきます.

ArduboyではArduboyクラスを用いることで入出力を制御します.今回はArduboyクラスを少し改良したArduboy2を使っていますが,違いはほとんどないので単にArduboyと呼ぶことにします.Arduboy2のみにある機能についてはそのつど適宜説明します.

arduboyクラスのbegin()関数は起動時に"ARDUBOY"という文字列を上から中央まで移動させ表示させる関数です.表示中に右ボタンを押せば表示をスキップすることができます.

arduboyクラスでは表示内容を保存するバッファがあります.それを書き換えることで表示内容を変更していきます.バッファを初期化するarduboy.clear()とバッファに従って画面に表示するarduboy.print()をloop()の最初と最後でそれぞれ実行するのはほとんどのプログラムで同じだと思います.

Arduboyの画面は左端を原点として,左向きにx軸,下向きにy軸が伸びています.画面の解像度は128×64ドットです.

画面に文字を表示するには,まずarduboy.setCursor(x,y)でカーソルを(x,y)座標に設定します.次にarduboy.print()によりバッファの(x,y)からテキストを書き込みます.

arduboy.print()はバッファの内容を書き換えているだけで,これで画面に表示されるわけではないことに注意してください.画面に表示させるのはarduboy.display()関数です.

ゲームモードの遷移

ボタンを押すたびにゲームモードを変更するプログラムは以下のようになります.(ただし不具合あり.)

gameMode.ino
#include<Arduboy2.h>

Arduboy2 arduboy;

int gameMode;

void setup() {
  arduboy.begin();
  arduboy.clear();

  gameMode = 1;
}

void loop() {
  arduboy.clear();

  switch(gameMode){
          //スタート画面
          case 1:
                  arduboy.setCursor(30,30);
                  arduboy.print("Start mode");

                  if(arduboy.pressed(A_BUTTON)){
                          gameMode = 2;
                  }
                  break;
          //プレイ画面
          case 2:
                  arduboy.setCursor(30,30);
                  arduboy.print("Play mode");

                  if(arduboy.pressed(A_BUTTON)){
                          gameMode = 3;
                  }
                  break;
          //ゲームオーバー
          case 3:
                  arduboy.setCursor(30,30);
                  arduboy.print("Game over");

                  if(arduboy.pressed(A_BUTTON)){
                          gameMode = 1;
                  }
                  break;
  }
  arduboy.display();
}

これを実行すると,以下のようになります.

あまりうまくいっていないことが分かるでしょうか?少し押しただけで画面が何度も変わってしまいます.

とりあえず解説をします.

ゲームのモードはswitchで制御しています.今回はモード毎に処理を直接書いていますが,一つの関数にまとめた方が可読性が高まるでしょう.例えば,以下のようにすると良いでしょう.

 switch(gameMode){
          case 1:
                  startMode();
                  break;
          case 2:
                  ...

また,簡単のため,ゲームのモードをint型の変数で保存していますが,列挙型(Enum)を使った方がいいでしょう.今回のプログラムではsetCursorを何度も書いています.カーソルはswitchの前に書けば一回で済むのではないかと思われるかもしれません.実際にそうですが,長いプログラムを書くときにはprintの前に必ずsetCursorで設定する方が処理が分かりやすくなるのでそうしています.

ボタンの入力はarduboy.pressed()で検出することができます.例えば,Arduboy.pressed(LEFT_BUTTON)やArduboy.pressed(B_BUTTON)は左ボタンやBボタンが押しているときにtrueになります.ここで,Arduboyでは左側の丸ボタンがAボタンで右側がBボタンであることに注意してください.ファミコンなど任天堂のハードとは逆になっています.

さて,現段階ではボタンが押されたこと,つまり,押されていない状態から押された状態に変更されたというのが検出できません.そこで,バッファ変数を使うことで,ボタンを一度だけ押すということを実現しましょう.

gemeMode2.ino
#include<Arduboy2.h>

Arduboy2 arduboy;

int gameMode;

bool aBuffer;

void setup() {
  arduboy.begin();
  arduboy.clear();

  gameMode = 1;
  aBuffer = false;
}

void loop() {
  arduboy.clear();

  switch(gameMode){
      //スタート画面
          case 1:
                  arduboy.setCursor(30,30);
                  arduboy.print("Start mode");

                  if(arduboy.pressed(A_BUTTON) && aBuffer == false){
                          aBuffer = true;
                          gameMode = 2;
                  }
                  break;
          //プレイ画面
          case 2:
                  arduboy.setCursor(30,30);
                  arduboy.print("Play mode");

                  if(arduboy.pressed(A_BUTTON) && aBuffer == false){
                          aBuffer = true;
                          gameMode = 3;
                  }
                  break;
          //ゲームオーバー
          case 3:
                  arduboy.setCursor(30,30);
                  arduboy.print("Game over");
                  if(arduboy.pressed(A_BUTTON) && aBuffer == false){
                          aBuffer = true;
                          gameMode = 1;
                  }
                  break;
  }

 //ボタンが押されていないことをバッファに記録する
  if(arduboy.notPressed(A_BUTTON)){
          aBuffer = false;
  }

  arduboy.display();
}

前のループでAボタンが押されているときにaBufferがtrueになるようにしておきます.ボタンが押されていないことはarduboy.notPressed()でチェックできます.ボタンが押された後の処理をする際に必ずaBufferをtrueにしてください.

このバッファを用いた方法でもボタンを扱うことができます.しかし,長いゲームを作るとバッファを適切に変更するのは結構大変です.

解決策として,arduboy2からボタンが押されたばかりであることをチェックするjustPressed()やボタンが放された瞬間をチェックするjustReleased()が使えるようになりました.ただし,これらを使うためにはloopの最初でpollButtons()を実行しておく必要があります.

ともかく.このjustPressedを用いれば,以下のように書くこともできます.

gameMode3.ino
#include<Arduboy2.h>

Arduboy2 arduboy;

int gameMode;

bool aBuffer;

void setup() {
  arduboy.begin();
  arduboy.clear();

  gameMode = 1;
}

void loop() {
  arduboy.clear();

  arduboy.pollButtons();

  switch(gameMode){
          case 1:
                  arduboy.setCursor(30,30);
                  arduboy.print("Start mode");

                  if(arduboy.justPressed(A_BUTTON)){
                          gameMode = 2;
                  }
                  break;
          case 2:
                  arduboy.setCursor(30,30);
                  arduboy.print("Play mode");

                  if(arduboy.justPressed(A_BUTTON)){
                          gameMode = 3;
                  }
                  break;
          case 3:
                  arduboy.setCursor(30,30);
                  arduboy.print("Game over");
                  if(arduboy.justPressed(A_BUTTON)){
                          gameMode = 1;
                  }
                  break;
  }

  arduboy.display();
}

実行結果は変わらないので省略します.

プレイヤーを動かす

プレイヤーを動かすこと自体に難しいことはありません.とりあえず,少し不具合がある以下のプログラムを見てください.

movePlayer.ino
#include<Arduboy2.h>

Arduboy2 arduboy;

int xPlayer;
int yPlayer;

void setup() {
  arduboy.begin();
  arduboy.clear();

  xPlayer = 0;
  yPlayer = 0;
}

void loop() {
  arduboy.clear();

  if(arduboy.pressed(LEFT_BUTTON))  xPlayer--;
  if(arduboy.pressed(RIGHT_BUTTON)) xPlayer++;
  if(arduboy.pressed(UP_BUTTON))    yPlayer--;
  if(arduboy.pressed(DOWN_BUTTON))  yPlayer++;

  arduboy.fillRect(xPlayer,yPlayer,8,8,WHITE);

  arduboy.display();
}

arduboy.fillRectで長方形を表示することができます.それ以外は説明不要でしょう.実行すると以下のようになります.

動きが早すぎますね.これはloopの処理が終わればすぐに次のループに進んでしまうからです.

そこで,フレームレートを固定し.loopが終わっても次のフレームまでは処理を行わないというのが定石です.setFrameRate(n)で1秒にnフレームにレートを固定します.次にloopの最初で

  if (!arduboy.nextFrame()) return;

とすることで次のフレームまでスキップできます.これらを用いると以下のプログラムになります.

movePlayer.ino
#include<Arduboy2.h>

Arduboy2 arduboy;

int xPlayer;
int yPlayer;

void setup() {
  arduboy.begin();
  arduboy.clear();

  //フレームレートを固定
  arduboy.setFrameRate(60);

  xPlayer = 0;
  yPlayer = 0;
}

void loop() {
  //次のフレームまでスキップ
  if (!arduboy.nextFrame()) return;

  arduboy.clear();

  if(arduboy.pressed(LEFT_BUTTON))  xPlayer--;
  if(arduboy.pressed(RIGHT_BUTTON)) xPlayer++;
  if(arduboy.pressed(UP_BUTTON))    yPlayer--;
  if(arduboy.pressed(DOWN_BUTTON))  yPlayer++;

  arduboy.fillRect(xPlayer,yPlayer,8,8,WHITE);

  arduboy.display();
}

こうすればいい感じに動きます.

ドット絵を貼る

質の良いゲームを作るにはキャラクターや背景をドットで表現し描写することが不可欠です.ドット絵をArduboyの画面に貼る方法の一つを説明します.

まず,どのような方法でもいいのでドット絵を用意します.私はclip studioでちまちま作っています.

次にToCharsというサイトで,ドット絵をchar型の文字列に変換します.上の画像なら以下の文字列になります.

0x00, 0x00, 0x8, 0xd8, 0xf8, 0xf0, 0xe0, 0xc0, 0x40, 0x98, 0xfc, 0xb4, 0x18, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7, 0x3, 0x3, 0x1, 0x1, 0x1, 0xe0, 0xbf, 0xe, 0x1d, 0xf3, 0x81, 0x00, 0x00, 

この文字列を配列に格納します.

const unsigned char Player[] PROGMEM = {
0x00, 0x00, 0x8, 0xd8, 0xf8, 0xf0, 0xe0, 0xc0, 0x40, 0x98, 0xfc, 0xb4, 0x18, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7, 0x3, 0x3, 0x1, 0x1, 0x1, 0xe0, 0xbf, 0xe, 0x1d, 0xf3, 0x81, 0x00, 0x00, 
};

最後にdrawBitmap関数を使うことで画像をarduboyのバッファに格納します.

arduboy.drawBitmap(xPlayer,yPlayer,Player1,16,16,WHITE);

画像の扱いは非常に難しいので詳しいことはarduboy.hや以下のサイトを参考にしてみてください.
Arduboyでスプライトを表示する
Make Your Own Sideways Scroller: Part 2 - Prerequisite Concepts

最後に

以上で色々ゲームが作れるようになってると思います。簡単にゲームが作れるのが良いところなので、興味を持った方は是非ともArduboyを手に入れて実験してみてください。