0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Dodge Game」(避けゲー)の設計と実装

Last updated at Posted at 2025-11-08

AI時代のプログラミング

こちらより引用します。

これまでの学習は、「文法→アルゴリズム→設計」というピラミッドでした。

しかし、AI時代は、このピラミッドが逆になります。

まず、

「何を作りたいか?」という情熱(Vision)と、
「どう構成すべきか?」という設計(Architecture)から始める。

そして、

その実現に必要な文法や手続き(Implementation)は、
その都度、AIに聞けばいい。

というわけで、何を作りたいかを考えます。

リスクとリターン

こちらのゲームでは、イナヅマを避けて、落雷場所のアイテムを取ります。
アイテムは、すぐに消えてしまうので、すぐに取る必要があります。
しかし、すぐ取るにはイナヅマに近づく必要があり、危険です。
そういう意味で、このゲームは、
「ハイリスク・ハイリターン」「ローリスク・ローリターン」
の設計がよくできています。

さて、これをこのまま作るのは大変そうなので、このエッセンスを抜き出して、シンプルなゲームを設計します。

タイトルは「神回避」
でも、このタイトルはすでに使われているので「Dodge Game」(避けゲー)とします。

コンセプトは、
「ギリギリで避けるほど、ハイスコア」
「当たったら、アウト」

01 キャラクタ操作のみ

chatGPTとのやり取りは上記のとおりです。

まず、適当にゲームの概要を伝えて、最初の一歩のプログラムを書いてもらいます。
適当な絵でも、だいたい理解してくれます。

image.png

弾を避けるゲーム「Dodge Game」をprocessingで作ります。 まずは1000,1000のウィンドウを開いて、カーソルキーで円を動かしたいです。 circleクラスを用意して、位置と半径、速度を変数に持ち、移動のmoveメソッドを持たせて、動くようにしたいです。キーボードの押下状態を保持するboolean変数を使って、キーを押している間のみ動くようにしたい。弾はまだ作らなくていいです。自分が動くのみの実装。

Circle player;

boolean upPressed = false;
boolean downPressed = false;
boolean leftPressed = false;
boolean rightPressed = false;

void setup() {
  size(1000, 1000);
  player = new Circle(width/2, height/2, 20);
}

void draw() {
  background(255);

  // 操作キーに応じて速度を更新
  if (upPressed) player.vy = -5;
  else if (downPressed) player.vy = 5;
  else player.vy = 0;

  if (leftPressed) player.vx = -5;
  else if (rightPressed) player.vx = 5;
  else player.vx = 0;

  player.move();
  player.display();
}

// -------------------------------------------
// キー押下・離したときの処理
// -------------------------------------------
void keyPressed() {
  if (keyCode == UP) upPressed = true;
  if (keyCode == DOWN) downPressed = true;
  if (keyCode == LEFT) leftPressed = true;
  if (keyCode == RIGHT) rightPressed = true;
}

void keyReleased() {
  if (keyCode == UP) upPressed = false;
  if (keyCode == DOWN) downPressed = false;
  if (keyCode == LEFT) leftPressed = false;
  if (keyCode == RIGHT) rightPressed = false;
}

// -------------------------------------------
// Circleクラス
// -------------------------------------------
class Circle {
  float x, y;
  float r;
  float vx, vy;

  Circle(float x, float y, float r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = 0;
    this.vy = 0;
  }

  void move() {
    x += vx;
    y += vy;

    // 画面外に出ないよう制限
    x = constrain(x, r, width - r);
    y = constrain(y, r, height - r);
  }

  void display() {
    fill(255, 100, 100);
    noStroke();
    circle(x, y, r*2);
  }
}

ここで、例えば、カーソルキーの↑と↓を同時に押すと、上に進んでしまうので、下記のように修正しました。
修正コードは自分で考えました。

void draw() {
  background(255);

  // 操作キーに応じて速度を更新
  if (upPressed) player.vy -= 5;
  if (downPressed) player.vy += 5;
  if (leftPressed) player.vx -= 5;
  if (rightPressed) player.vx += 5;

  player.move();
  player.display();
  
  player.vy = 0;
  player.vx = 0;
}

キーの同時押下時に相殺して動かなくなるように、しました。
以後は、これを前提に続けます。

02 周囲から飛んでくる弾の生成

chatGPTへの指示は以下のとおりです。

次は、弾を生成します。
衝突判定は、まだ考えません。
弾の発生場所は、ウィンドウを中心とした半径500の円周上のランダムな位置です。circleクラスを使いまわしてください。プレーヤーと色は分けてください。色はメンバ変数にしたほうがいいかもしれません。飛ぶ方向はその時のプレーヤーの場所に向かってください。移動速度は一定です。いずれかのタイミングで正規化処理が必要だと思います。画面外にいったら、消しましょう。arraylistがいいかな。

Circle player;
ArrayList<Circle> bullets;

boolean upPressed = false;
boolean downPressed = false;
boolean leftPressed = false;
boolean rightPressed = false;

void setup() {
  size(1000, 1000);
  player = new Circle(width/2, height/2, 20, true, color(255, 100, 100)); // 赤いプレーヤー
  bullets = new ArrayList<Circle>();
}

void draw() {
  background(255);

  // 操作キーに応じて速度を更新(同時押し相殺方式)
  if (upPressed)    player.vy -= 5;
  if (downPressed)  player.vy += 5;
  if (leftPressed)  player.vx -= 5;
  if (rightPressed) player.vx += 5;

  // 一定間隔で弾を生成(ここでは6フレームごと)
  if (frameCount % 6 == 0) {
    spawnBullet();
  }

  // プレーヤー更新
  player.move();
  player.display();
  // 速度リセット(次フレーム用)
  player.vx = 0;
  player.vy = 0;

  // 弾の更新
  for (int i = bullets.size()-1; i >= 0; i--) {
    Circle b = bullets.get(i);
    b.move();
    b.display();
    if (b.isOutOfScreen()) {
      bullets.remove(i);
    }
  }
}

// -------------------------------------------------
// 弾生成:円周上ランダム+プレーヤー方向
// -------------------------------------------------
void spawnBullet() {
  float cx = width/2;
  float cy = height/2;
  float radius = 500;

  float ang = random(TWO_PI);
  float bx = cx + radius * cos(ang);
  float by = cy + radius * sin(ang);

  Circle bullet = new Circle(bx, by, 8, false, color(0)); // 黒い弾

  // プレーヤーへの方向ベクトルを正規化して速度に
  float dx = player.x - bullet.x;
  float dy = player.y - bullet.y;
  float len = sqrt(dx*dx + dy*dy);
  float speed = 5;

  if (len != 0) {
    bullet.vx = dx / len * speed;
    bullet.vy = dy / len * speed;
  }

  bullets.add(bullet);
}

// -------------------------------------------------
// キー入力(押している間だけ true )
// -------------------------------------------------
void keyPressed() {
  if (keyCode == UP)    upPressed = true;
  if (keyCode == DOWN)  downPressed = true;
  if (keyCode == LEFT)  leftPressed = true;
  if (keyCode == RIGHT) rightPressed = true;
}

void keyReleased() {
  if (keyCode == UP)    upPressed = false;
  if (keyCode == DOWN)  downPressed = false;
  if (keyCode == LEFT)  leftPressed = false;
  if (keyCode == RIGHT) rightPressed = false;
}

// -------------------------------------------------
// Circle クラス(プレーヤー・弾兼用)
// -------------------------------------------------
class Circle {
  float x, y;
  float r;
  float vx, vy;
  boolean isPlayer;
  color col;

  Circle(float x, float y, float r, boolean isPlayer, color col) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = 0;
    this.vy = 0;
    this.isPlayer = isPlayer;
    this.col = col;
  }

  void move() {
    x += vx;
    y += vy;

    // プレーヤーだけ画面内に制限
    if (isPlayer) {
      x = constrain(x, r, width - r);
      y = constrain(y, r, height - r);
    }
  }

  void display() {
    noStroke();
    fill(col);
    circle(x, y, r*2);
  }

  // 画面外に出たかどうか(弾用)
  boolean isOutOfScreen() {
    return (x < -r || x > width + r || y < -r || y > height + r);
  }
}

イメージどおりです。

image.png

03 衝突判定とスコア計算

chatGPTへの指示は以下のとおりです。

if (frameCount % 6 == 0) {

if (frameCount % 60 == 0) {
に変えて、難易度を下げます。

点数は、弾との距離を計算して、プレーヤーの半径と球の半径を加算した数に10を加算してXとします。プレーヤーと弾がその距離Xよりも近づいていたら、スコアを(10-X)加算します。

プレーヤーと弾がぶつかったら、スコアを0点に戻します。
その弾は、削除してください。
そのとき、ハイスコアの保持と表示をお願いします。

Circle player;
ArrayList<Circle> bullets;

boolean upPressed = false;
boolean downPressed = false;
boolean leftPressed = false;
boolean rightPressed = false;

int score = 0;
int highScore = 0;

void setup() {
  size(1000, 1000);
  player = new Circle(width/2, height/2, 20, true, color(255, 100, 100)); // 赤いプレーヤー
  bullets = new ArrayList<Circle>();

  textAlign(LEFT, TOP);
  textSize(24);
}

void draw() {
  background(255);

  // 操作キーに応じて速度を更新(同時押し相殺方式)
  if (upPressed)    player.vy -= 5;
  if (downPressed)  player.vy += 5;
  if (leftPressed)  player.vx -= 5;
  if (rightPressed) player.vx += 5;

  // 一定間隔で弾を生成(難易度低め:60フレームごと)
  if (frameCount % 60 == 0) {
    spawnBullet();
  }

  // プレーヤー更新
  player.move();
  player.display();
  player.vx = 0;
  player.vy = 0;

  // 弾の更新(当たり判定+スコア処理付き)
  for (int i = bullets.size()-1; i >= 0; i--) {
    Circle b = bullets.get(i);

    // ==== スコア・当たり判定 ====
    float d = dist(player.x, player.y, b.x, b.y);
    float sumR = player.r + b.r;
    float nearThreshold = sumR + 10; // プレーヤー半径+弾半径+10

    // ぶつかった(接触以下)
    if (d <= sumR) {
      // ハイスコア更新
      if (score > highScore) {
        highScore = score;
      }
      score = 0;         // スコアを0に戻す
      bullets.remove(i); // ぶつかった弾は削除
      continue;          // 次の弾へ
    }

    // 「ギリギリまで近づいた」分だけスコア加算
    // d が nearThreshold より小さいほど、近づき度合いが大きい
    if (d < nearThreshold) {
      float add = nearThreshold - d;  // 最大10点(接触ギリギリのとき)
      score += int(add);
    }

    // ==== 移動・表示 ====
    b.move();
    b.display();

    // 画面外に出た弾は削除
    if (b.isOutOfScreen()) {
      bullets.remove(i);
    }
  }

  // ハイスコアは常に最新にしておく(念のため)
  if (score > highScore) {
    highScore = score;
  }

  // スコア表示
  fill(0);
  text("Score: " + score, 20, 20);
  text("High Score: " + highScore, 20, 50);
}

// -------------------------------------------------
// 弾生成:円周上ランダム+プレーヤー方向
// -------------------------------------------------
void spawnBullet() {
  float cx = width/2;
  float cy = height/2;
  float radius = 500;

  float ang = random(TWO_PI);
  float bx = cx + radius * cos(ang);
  float by = cy + radius * sin(ang);

  Circle bullet = new Circle(bx, by, 8, false, color(0)); // 黒い弾

  // プレーヤーへの方向ベクトルを正規化して速度に
  float dx = player.x - bullet.x;
  float dy = player.y - bullet.y;
  float len = sqrt(dx*dx + dy*dy);
  float speed = 5;

  if (len != 0) {
    bullet.vx = dx / len * speed;
    bullet.vy = dy / len * speed;
  }

  bullets.add(bullet);
}

// -------------------------------------------------
// キー入力
// -------------------------------------------------
void keyPressed() {
  if (keyCode == UP)    upPressed = true;
  if (keyCode == DOWN)  downPressed = true;
  if (keyCode == LEFT)  leftPressed = true;
  if (keyCode == RIGHT) rightPressed = true;
}

void keyReleased() {
  if (keyCode == UP)    upPressed = false;
  if (keyCode == DOWN)  downPressed = false;
  if (keyCode == LEFT)  leftPressed = false;
  if (keyCode == RIGHT) rightPressed = false;
}

// -------------------------------------------------
// Circle クラス(プレーヤー・弾兼用)
// -------------------------------------------------
class Circle {
  float x, y;
  float r;
  float vx, vy;
  boolean isPlayer;
  color col;

  Circle(float x, float y, float r, boolean isPlayer, color col) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = 0;
    this.vy = 0;
    this.isPlayer = isPlayer;
    this.col = col;
  }

  void move() {
    x += vx;
    y += vy;

    // プレーヤーだけ画面内に制限
    if (isPlayer) {
      x = constrain(x, r, width - r);
      y = constrain(y, r, height - r);
    }
  }

  void display() {
    noStroke();
    fill(col);
    circle(x, y, r*2);
  }

  // 画面外に出たかどうか(弾用)
  boolean isOutOfScreen() {
    return (x < -r || x > width + r || y < -r || y > height + r);
  }
}

04 スレスレの可視化

点滅します。
目を近づけすぎないこと
点滅に弱い人は、試さないこと。

スレスレであるかが、わかりにくいので、スレスレ度合いに応じて、背景色が変化するようにしました。ちょっと目が痛いです。

chatGPTへの指示は以下のとおりです。

if (frameCount % 20 == 0) {
こうして、60から20に変更

float nearThreshold = sumR + 20; // プレーヤー半径+弾半径+20 調整
こうする。10から20に変更

点数が加算されているときは、backgroundを赤くしたい。
少しの加算なら、少し赤く。
多い加算なら、かなり赤く。

スコアの表示位置の調整をした、最終プログラムが以下です。

Circle player;
ArrayList<Circle> bullets;

boolean upPressed = false;
boolean downPressed = false;
boolean leftPressed = false;
boolean rightPressed = false;

int score = 0;
int highScore = 0;
float flashStrength = 0;  // 背景赤の強さ(0〜255)

void setup() {
  size(1000, 1000);
  player = new Circle(width/2, height/2, 20, true, color(255, 100, 100));
  bullets = new ArrayList<Circle>();

  textAlign(LEFT, TOP);
  textSize(24);
}

void draw() {
  // 背景(スコア加算があるほど赤く)
  background(255, 255 - flashStrength, 255 - flashStrength * 0.7);
  flashStrength *= 0.9;  // 徐々にフェードアウト

  // 操作キー
  if (upPressed)    player.vy -= 5;
  if (downPressed)  player.vy += 5;
  if (leftPressed)  player.vx -= 5;
  if (rightPressed) player.vx += 5;

  // 弾生成間隔(20フレーム)
  if (frameCount % 20 == 0) {
    spawnBullet();
  }

  player.move();
  player.display();
  player.vx = 0;
  player.vy = 0;

  // スコア加算の合計をこのフレームで計算
  float totalAdd = 0;

  for (int i = bullets.size()-1; i >= 0; i--) {
    Circle b = bullets.get(i);

    float d = dist(player.x, player.y, b.x, b.y);
    float sumR = player.r + b.r;
    float nearThreshold = sumR + 20; // 距離20以内を判定範囲

    if (d <= sumR) {
      // 接触:スコアリセット・弾削除
      if (score > highScore) highScore = score;
      score = 0;
      bullets.remove(i);
      continue;
    }

    // 距離が近いほどスコア加算
    if (d < nearThreshold) {
      float add = nearThreshold - d;
      score += int(add);
      totalAdd += add; // 背景色用に蓄積
    }

    b.move();
    b.display();

    if (b.isOutOfScreen()) bullets.remove(i);
  }

  if (score > highScore) highScore = score;

  // 背景赤さ更新(加算量に応じて強く)
  flashStrength = constrain(totalAdd * 10, 0, 255);

  // スコア表示
  fill(0);
  text(score, player.x+20, player.y+20);
  text("High Score: " + highScore, 20, 50);
}

// -------------------------------------------------
void spawnBullet() {
  float cx = width/2;
  float cy = height/2;
  float radius = 500;

  float ang = random(TWO_PI);
  float bx = cx + radius * cos(ang);
  float by = cy + radius * sin(ang);

  Circle bullet = new Circle(bx, by, 8, false, color(0));

  float dx = player.x - bullet.x;
  float dy = player.y - bullet.y;
  float len = sqrt(dx*dx + dy*dy);
  float speed = 5;

  if (len != 0) {
    bullet.vx = dx / len * speed;
    bullet.vy = dy / len * speed;
  }

  bullets.add(bullet);
}

// -------------------------------------------------
void keyPressed() {
  if (keyCode == UP)    upPressed = true;
  if (keyCode == DOWN)  downPressed = true;
  if (keyCode == LEFT)  leftPressed = true;
  if (keyCode == RIGHT) rightPressed = true;
}

void keyReleased() {
  if (keyCode == UP)    upPressed = false;
  if (keyCode == DOWN)  downPressed = false;
  if (keyCode == LEFT)  leftPressed = false;
  if (keyCode == RIGHT) rightPressed = false;
}

// -------------------------------------------------
class Circle {
  float x, y;
  float r;
  float vx, vy;
  boolean isPlayer;
  color col;

  Circle(float x, float y, float r, boolean isPlayer, color col) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = 0;
    this.vy = 0;
    this.isPlayer = isPlayer;
    this.col = col;
  }

  void move() {
    x += vx;
    y += vy;
    if (isPlayer) {
      x = constrain(x, r, width - r);
      y = constrain(y, r, height - r);
    }
  }

  void display() {
    noStroke();
    fill(col);
    circle(x, y, r*2);
  }

  boolean isOutOfScreen() {
    return (x < -r || x > width + r || y < -r || y > height + r);
  }
}

image.png

まとめ

リスクとリターンの設計思想で、AIを使ったゲーム「Dodge Game」を作りました。

情熱と設計を人間が行い、
文法はAIにまかせました。

本稿は、これで終わりです。

グラフィックを差し替えていけば、それなりなゲームの体裁は整うでしょう。

簡単なアレンジも、AI任せで、行けるはずです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?