スカッシュゲームを作ってみる

2024年8月28日

私はほとんどゲームをやりません。でも、高度なプログラミングを要求されるゲームプログラミングには、憧れがありました。結局、学生時代や社会人でゲームを作ることはなかったのですが、老後を迎えて少しずつ作ってみようかと思っています。

しかしながら、一気に高度なものは作れません。そこで、まずはスカッシュゲームを作ってみて、基本的な考え方や骨組みを考えてみようと思います。

1.使用するハードウェア

使用するマイコンは、Raspberry Pi Picoを使います。また、表示器はILI9341ドライバを使う2.8インチTFT液晶を使います。これらの使い方は既に投稿していますので、ご覧になってください。

左右に移動するボタンを、それぞれGP4、GP5に割り当てています。

プログラムには、Arduino IDEを使います。

2.ゲームクラスの作成

さて、ゲームプログラミングをしたことがない私は、どこから始めて良いか見当が付きません。かと言って、いき当たりばったりで進めると、後で訳のわからないものになってしまいます。やはり、オブジェクト指向で考える必要があるでしょう。(今の時代はどうなのか良く分かりません)

しかしながら、もう仕事でのプログラミングから遠ざかっている私では、バリバリのオブジェクト指向プログラミングはできません。そこで、クラスとその継承を用いたプログラムを作成しようと思います。

いくら私でも、何度も同じような定義をしたり、権限設定がないためにどこでもアクセスできてしまうようなプログラムは作りたくありません。そこで、一番基本となるベースクラスを作り、そのベースクラスから派生させたクラスを作ります。

今回はスカッシュゲームを作るので、ボールとラケットが必要になります。ボールとラケットには、サイズがあり、位置があります。こういった共通の定義をベースクラスに用意して、ボールとラケット特有の動作については、派生クラスで処理します。

具体的に定義したクラスが以下のものです。

class Kunimiyasoft_Game {

protected :
  int m_ScreenWidth = 240 ;   // LCD width 
  int m_ScreenHeight = 320 ;  // LCD height

  int m_x ; // 現在位置
  int m_y ;
  int m_width ;   // 幅
  int m_height ;  // 高さ
  int m_prevX ;   // 前の位置
  int m_prevY ;

public:
  Kunimiyasoft_Game() {
      // nop  
	}	

  int GetPositionX() {
		return m_x ;
  }
  int GetPositionY() {
		return m_y ;
  }

  void SetPositionX(int x){
    m_x = x ;
  }
  void SetPositionY(int y){
    m_y = y ;
  }

  int GetWidth() {
    return m_width ;
  }

  int GetHeight () {
    return m_height ;
  }

  int GetPrevPositionX() {
		return m_prevX ;
  }
  int GetPrevPositionY() {
		return m_prevY ;
  }

};

class MyRacket : public Kunimiyasoft_Game {
private:

public:
    MyRacket() {
      m_width = 30 ;
      m_height = 10 ;
      InitPosition();
    }	

    // 初期位置
    void InitPosition() {
      m_x = 110 ;
      m_y = 300 ;
      m_prevX = m_x ;
      m_prevY = m_y ;
    }

  // ラケットの位置を移動する
  void SetRacketPosition(int sx) {

    m_x += sx * 5 ;

    if (m_x < 0) {
      m_x = 0 ;
    } else if (m_x > (m_ScreenWidth - m_width)) {
      m_x = m_ScreenWidth - m_width ;
    }

    m_prevX = m_x ;
  }

  // 復帰値
  // -1 : gameover
  // 0 : HITなし
  // 1 : 上下でヒット
  // 2 : 左右でヒット
  // 3 : 左右でヒット
  int isHitBall(int bx, int by, int bsize) {
    int returncode = 0 ;

    if (bx <= 0) {
      // 左の壁に当たった
      returncode = 1 ;

    } else if (bx >= (m_ScreenWidth - bsize)) {
      // 右の壁に当たった
      returncode = 1 ;
    }
    
    if (by <= 0) {
      // 上の壁に当たった
      if (returncode == 1) {
        returncode = 3 ;
      } else {
        returncode = 2 ;
      }

    } else if (by >= (m_y - bsize)) {
      if ((bx + bsize) > m_x && bx < (m_x + m_width)) {
        // ラケットに当たった
        if (returncode == 1) {
          returncode = 3 ;
        } else {
          returncode = 2 ;
        }
      } else {
        returncode = -1 ;
      } 

    }

    return returncode ;
  }  

};

class MyBall : public Kunimiyasoft_Game {
private:

    int m_sx = -5 ; // X軸初期移動量
    int m_sy = -5 ; // Y軸初期移動量  

public:
    MyBall() {

      m_x = 50 ;  // ボール初期位置
      m_y = 80 ;
      m_width = 10 ; 
      m_height = 10 ;
      m_prevX = m_x ;
      m_prevY = m_y ;

	}	

  // ボール位置をセット
  // rx : ラケットX座標
  void SetBallPosition() {

    m_x += m_sx ;
    m_y += m_sy ;

    m_prevX = m_x ;
    m_prevY = m_y ;

  }

  // 移動量を再設定
  void SetSXSY(int hitvalue) {
    if (hitvalue == 1) {
      // 左右の壁に当たった
      m_sx = -1 * m_sx ;
    }  else if (hitvalue == 2) {
      m_sy = -1 * m_sy ;
    }  else if (hitvalue == 3) {
      m_sx = -1 * m_sx ;
      m_sy = -1 * m_sy ;
    } 
    
  }

  // 再スタート
  void InitPosition() {
    int xpos = random(3, 21) ;
    int direction = random(0, 2) ;

    m_x = xpos * 10 ;
    m_y = 80 ;
    m_prevX = m_x ;
    m_prevY = m_y ;
    
    if (direction) {
      m_sx = -5 ; 
    } else {
      m_sx = 5 ; 
    } 
    
    m_sy = -5 ;   
  }

};

クラスの構成は、今後、修正していくつもりです。できれば、WindowsやRaspberry Pi、Androidでも共通で使えるような構成にしたいです。また、関数は将来的に仮想関数を使おうと考えています。

(壁に当たった処理をラケットクラスで行っていますが、ソースを冗長させたくなかったのでご了承ください)

3.メインプログラム

メインプログラムでは、ゲームクラスのインスタンスを作成してゲーム処理をします。また、グラフィックライブラリは、LovyanGFXを使います。

#include "jglib.hpp"  // ゲーム用クラスが定義されたファイル
#define LGFX_USE_V1
#include <LovyanGFX.hpp>

#define TFT_MISO 16
#define TFT_MOSI 19
#define TFT_SCLK 18
#define TFT_CS   17
#define TFT_DC   26
#define TFT_RST  27

// Pin definitions for the buttons
#define BUTTON_L 4
#define BUTTON_R 5

// 「あろしーど」さん定義例使用
class LGFX : public lgfx::LGFX_Device
{
  lgfx::Panel_ILI9341 _panel_instance;
  lgfx::Bus_SPI _bus_instance;
public:
  LGFX(void)
  {
    {                                    // バス制御の設定を行います。
      auto cfg = _bus_instance.config(); // バス設定用の構造体を取得します。

      cfg.spi_host = 0;          // 使用するSPIを選択
      cfg.spi_mode = 0;          // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 40000000; // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      cfg.freq_read = 20000000;  // 受信時のSPIクロック

      cfg.pin_sclk = TFT_SCLK; // SPIのSCLKピン番号を設定
      cfg.pin_mosi = TFT_MOSI; // SPIのMOSIピン番号を設定
      cfg.pin_miso = TFT_MISO; // SPIのMISOピン番号を設定 (-1 = disable)
      cfg.pin_dc = TFT_DC;     // SPIのD/Cピン番号を設定  (-1 = disable)

      _bus_instance.config(cfg);              // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance); // バスをパネルにセットします。
    }
    {                                      // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config(); // 表示パネル設定用の構造体を取得します。
      cfg.pin_cs = TFT_CS;                 // CSが接続されているピン番号   (-1 = disable)
      cfg.pin_rst = TFT_RST;               // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;                   // BUSYが接続されているピン番号 (-1 = disable)

      // cfg.panel_width = 240;  // 実際に表示可能な幅
      // cfg.panel_height = 320; // 実際に表示可能な高さ
      // cfg.offset_x = 0;       // パネルのX方向オフセット量
      cfg.offset_y = 0;       // パネルのY方向オフセット量

      _panel_instance.config(cfg);
    }
    setPanel(&_panel_instance); // 使用するパネルをセットします。
  }
};

// LovyanGFX インスタンス作成
static LGFX lcd ;
static LGFX_Sprite sprite(&lcd);
static LGFX_Sprite sprite2(&lcd);
static LGFX_Sprite sprite3(&lcd);
static LGFX_Sprite sprite4(&lcd);

// Kunimiyasoft Game Class
static MyRacket myracket ;
static MyBall myball ;

void setup() {
  // put your setup code here, to run once:
  lcd.init() ;

  // 左右ボタン定義
  pinMode(BUTTON_L, INPUT_PULLUP);
  pinMode(BUTTON_R, INPUT_PULLUP);

  // ラケット
  int rw = myracket.GetWidth() ;
  int rh = myracket.GetHeight() ;
  sprite.createSprite(rw, rh);
  sprite3.createSprite(rw, rh);

  // ボール
  int bw = myball.GetWidth() ;
  int bh = myball.GetHeight() ;
  sprite2.createSprite(bw, bh);
  sprite4.createSprite(bw, bh);

  sprite.fillScreen(TFT_WHITE);
  sprite2.fillScreen(TFT_YELLOW);
  sprite3.fillScreen(TFT_BLACK);
  sprite4.fillScreen(TFT_BLACK);

  // 初期表示
  int rx = myracket.GetPositionX() ;
  int ry = myracket.GetPositionY() ;
  sprite.pushSprite(rx, ry);  

  lcd.startWrite();
 
}

void loop() {
  // put your main code here, to run repeatedly:

  int rprevX = myracket.GetPrevPositionX() ;
  int rprevY = myracket.GetPrevPositionY() ;

  // ボタン判定
  if (digitalRead(BUTTON_L) == LOW) {
    myracket.SetRacketPosition(-1) ;
  }
  if (digitalRead(BUTTON_R) == LOW) {
    myracket.SetRacketPosition(1) ;
  }

  int rx = myracket.GetPositionX() ;
  int ry = myracket.GetPositionY() ;
  int rw = myracket.GetWidth() ;

  int bprevX = myball.GetPrevPositionX() ;
  int bprevY = myball.GetPrevPositionY() ;
  int bsize = myball.GetWidth() ; // 幅を使う

  myball.SetBallPosition();
  int bx = myball.GetPositionX() ;
  int by = myball.GetPositionY() ; 

  int hitvalue = myracket.isHitBall(bx, by, bsize) ;

  bool isGameover = false ;
  switch (hitvalue) {
    case 1 :
    case 2 :
    case 3 :
      myball.SetSXSY(hitvalue) ;
  
      break ;
  
    case -1 :
      isGameover = true ;
      break ;

    default :
      break ;  
  }

  if (isGameover) {
    lcd.clear() ;
    myball.InitPosition() ;
    // myracket.InitPosition() ;
    sprite.pushSprite(rx, ry);  

  } else {

    if (rprevX != rx) {
      sprite3.pushSprite(rprevX, rprevY); 
      sprite.pushSprite(rx, ry);  
    }   
    sprite4.pushSprite(bprevX, bprevY);
    sprite2.pushSprite(bx, by);
  }

  delay(25) ;

}

LovyanGFXのスプライトを使用しています。ここで、ラケットとボールそれぞれ2つずつスプライトを使いました。例えばラケットであれば、ラケットの描画で1つスプライトを使い、移動前のラケットを消す描画に1つスプライトを使っています。

実は、ゲームとして成り立たせるために、いろいろ試行錯誤しました。例えば画面サイズでクリアすると、処理速度が遅くてゲームになりません。また、スプライトの挙動も想定したとおりにはなかなか動かず、今回のような処理になりました。たぶん、もっと良い方法があるのでしょう。

また、ラケットやボールのプロパティを何度も取得していますが、Arduino IDEの構造上、外部変数で取得してしまい、どこでも使えるようにした方が良いのかもしれません。無理にスコープを意識すると、かえって無駄な処理になってしまいます。

created by Rinker
Raspberry Pi
¥1,780 (2024/09/16 22:31:34時点 Amazon調べ-詳細)

4.動作させてみて

※ここに動画を挿入予定

一応、スカッシュゲームができました。まだまだ完成にはほど遠く、ボールを円形で描画したり、スコアをいれたり、打ち方によってボールの角度が変わるなど処理を入れなければなりません。

ボールの打ち返しができないとゲームオーバーですが、すぐに再開します。ボールの位置は、random関数で可変になるようにしました。

ゲームをしてみて、ラケットやボールの移動速度も調整が必要に感じました。ボールの方はdelay関数で簡単に調整できます。ラケットが微妙ですね。

今回の考え方を拡張して、ブロック崩しを作ってみたいです。まだまだやらなければならないことが山積みですが、のんびりと進めて行こうと思います。