100行で作るアクションゲーム講習会

@kegra

講師自己紹介


@kegra

19B/24M 工学院電気電子系

  • 好きなゲーム: Celeste、洞窟物語など
  • 色々雑多なものを作っています

本日の内容

マリオみたいな
ジャンプアクションを
100行程度で作ろう

この講習会のテーマ

  • Unity、UEを始めとするゲームエンジンは大道具すぎる!!
    → まずは小さいツールで小さいゲームを作れるようになろう

  • 特定のエンジンの使い方はゲームプログラミングの本質ではない
    → 言語やツールに関係ない基礎能力を付けよう

  • Unityしか使えない人材になって欲しくない

サポートする言語/ライブラリ

本講習会ではこれらをサポートしますが、これらで無くても良いです

  • C++ & Siv3D
  • Python & PyGame
  • JavaScript & Canvas
  • Processing

「座標指定で何かを描画できる」「何らかのボタン入力を受け取れる」
あたりを満たしていればなんでもあり!

(DXライブラリ、プチコン、HSP、etc...)

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

ここまで100行!

今日の予定(後半)

  • 発展
    • カメラの実装
    • 画面切り替えの実装
    • 多数オブジェクトの実装
  • 座学
    • 描画フレームと処理フレーム
    • 大量のオブジェクトを扱う設計
    • ゲームエンジンは結局何をやっているのか?

実況チャンネルなど

traQの #event/workshop/100line-action を使ってください

分かんないところがあったら書き込んでくれれば反応します

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

メインループ

まずは一番最初の雛型を作ろう!

配布のサンプルコードを確認してください

メインループ(Siv3D)

#include <Siv3D.hpp>

void Main() {

    while(System::Update()) {
        // メイン処理
    }
}

メインループ(PyGame)

※コード一部省略

pygame.init()
screen = pygame.display.set_mode((640, 480))
while (1):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    # メイン処理
    pygame.display.update()

メインループ(JavaScript)

const canvas = document.getElementById("main");
const ctx = canvas.getContext("2d");

function update() {
    // メイン処理
    window.requestAnimationFrame(update);
}

window.requestAnimationFrame(update);

メインループ(Processing)

void setup() {
  size(640, 480);
}
 
void draw() {
    // メイン処理
}

メインループ

1回計算するだけのプログラム → 1回実行して終わり

ゲームなどはユーザーが終了ボタンなどを押すまでずっと動作し続ける
 → 無限ループが基本 (メインループ)

メインループ

無限ループにおける1回1回の実行を「フレーム」と呼ぶ

ゲームの世界の時間変化はフレームが最小単位!


1秒間のフレーム数はFPS(Frames per Second)と呼ばれる

現代は60fpsが一般的(勿論そうでないものもある)

メインループ

無限ループの基本はwhile文  ※Rust言語などはloop文のような専用文もある

void Main() {
    // 初期化処理...

    while(System::Update()) {
        // 各フレームの処理
    }
}

Processing、JavaScript、その他Unityなどではwhile文の存在が見えない
が、必ず裏に隠されていると思って良い

(補足)Processingの中身のイメージ

int main() {
    setup();

    while(true) {
        draw();
    }
}

(補足)JavaScriptの中身のイメージ

events = [];
while(true) {
    const next = events.shift();
    next.callback();
}

// 別スレッド、JavaScriptの外側
while(true) {
    waitVsync();
    events.push({ callback: requestAnimationFrameCallback });
}

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

描画

正方形を描いてみよう!

毎フレーム描画するので、ループの中にプログラムを書く

描画(Siv3D)

void Main() {
    while(System::Update()) {
        Rect{ 0, 0, 100, 100 }.draw(Palette::Red);
    }
}

描画(PyGame)

while (1):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    pygame.draw.rect(screen, pygame.Color('red'),
                     pygame.Rect(0, 0, 100, 100))

    pygame.display.update()

描画(JavaScript)

function update() {
    ctx.fillStyle = "red";
    ctx.fillRect(0, 0, 100, 100);

    window.requestAnimationFrame(update);
}

描画(Processing)

void draw() {
    fill(255, 0, 0);
    rect(0, 0, 100, 100);
}

描画

赤い正方形が表示されたかな?

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

アニメーション

正方形を動かしてみよう!

アニメーションの原理: パラパラ漫画

フレーム毎に少しずつ違うものを表示すると動いて見える

アニメーション(Siv3D)

int x = 0, y = 0;

while(System::Update()) {
    x++; y++;
    Rect{ x, y, 100, 100 }.draw(Palette::Red);
}

アニメーション(PyGame)

x = 0
y = 0
while True:
    # (一部省略)

    x += 1
    y += 1
    pygame.draw.rect(screen, pygame.Color('red'),
                     pygame.Rect(x, y, 100, 100))

    pygame.display.update()

アニメーション(JavaScript)

let x = 0; y = 0;

function update() {
    x++; y++;

    ctx.fillStyle = "red";
    ctx.fillRect(x, y, 100, 100);

    window.requestAnimationFrame(update);
}

アニメーション(Processing)

int x = 0, y = 0;

void draw() {
    x++;
    y++;

    fill(255, 0, 0);
    rect(x, y, 100, 100);
}

アニメーション

Siv3D以外の人は
こんなんなってしまったのではないか

画面クリア

描画のたびに画面をクリア(消去)しなければ前の画面が残ってしまう!

毎フレーム描画前に画面全体をクリアする必要がある

※Siv3D等はお節介なので画面クリアなどは勝手にやってくれている

描画とアニメーション(JavaScript)

ctx.clearRect(0, 0, 640, 480);  // 画面クリア

ctx.fillStyle = "red";
ctx.fillRect(x, y, 100, 100);

画面クリア(PyGame)

screen.fill(pygame.Color('black'))  # 画面クリア

pygame.draw.rect(screen, pygame.Color('red'),
                    pygame.Rect(x, y, 100, 100))

画面クリア(Processing)

background(0);  // 画面クリア

fill(255, 0, 0);
rect(x, y, 100, 100);

FPS調整

PyGameなど一部の環境での開発では
正方形がものすごい速度で飛び去ってしまったはず

これはFPSが制限されていないことによる

適宜待ち処理を入れることでFPSを60fps一定に調整しよう

2種類のFPS調整

  • スリープによる調整
  • 垂直同期による調整

順に見ていこう

FPS調整(スリープによる調整)

最も単純な方法: 1フレームの処理時間を計測し、
1フレームの時間からそれを差し引いて待つ

例(60fps): 1フレームは1000ms/60 = 16.666...ms

→大体(16-t)ミリ秒だけ待つ 以下は疑似コード

beginTime = time();

// 処理...

endTime = time();
Sleep(16 - (endTime - beginTime));

FPS調整(スリープによる調整)

工夫すればもっと正確な調整もできる

firstTime = time(); frameCount = 0;

while(true) {
    // 処理...

    endTime = time(); frameCount++;
    Sleep(firstTime + frameCount * 1000 / 60 - lastTime);
}

↑これは処理時間が1フレーム以上かかった場合を考慮してないけど...

FPS調整(垂直同期信号による調整)

根本的に違うのが垂直同期信号を使う方法


~垂直同期信号とは?~

一般的にディスプレイは60Hzとか120Hzとかで画面を更新している。

その際に「今更新のタイミングだよ」という信号を流している。

↑これが垂直同期信号(VSync, Vertical Syncronization Signal)


※「垂直」の名前は、ディスプレイが上から下へ順に画面を更新していることによる

FPS調整(垂直同期信号による調整)

この信号が来るまで待つことで、ディスプレイの更新とゲームの更新を同期させることができる。

これが上手くいってないとチラつき(テアリング)が発生してしまう。

FPS調整(垂直同期信号による調整)

注意点!

垂直同期信号はプログラムの都合ではなく
ディスプレイやGPUの都合が左右するので、プログラムで制御できない


例えば60fps前提で作ったゲームを120Hzのモニターに接続すると

倍速動作になってしまうかもしれない

FPS調整(垂直同期信号による調整)

真面目にやるなら1フレームの経過時間を測って反映する

while(1) {
    now = time();        // 現在時刻(ミリ秒)
    dur = (now - old);   // 1フレームの経過時間(ミリ秒)

    // 1秒あたり100ピクセル動かしたい
    x += 100 * (dur / 1000.0f);

    old = now;
}

FPS調整(複合型)

  • 基本的にはディスプレイの垂直同期を利用する
  • 120Hzとかのディスプレイでも60fpsで動くように、
    適宜追加のスリープを入れてFPSの最大値に制限をかける

といった複合型の制御もアリ(Siv3DやProcessingなどは多分これ)


補足: JavaScriptのrequestAnimationFrame等は「複合型」ではなく純粋に垂直同期のみを取っているので、適宜調整を設けた方が良いと思われる

FPS調整(PyGame)

PyGameではclockというFPS調整用の機能がある

clock = pygame.time.Clock()

while True:
    clock.tick(60)      # 60fpsに調整

このように専用の調整機能が設けてある場合も多いので有効活用しよう

  • 例: PixiJSのTickerなど

参考: FPS調整(Windows API)

素のAPIなどでもFPS調整できるようになっておくと良いと思う

timeBeginPeriod(1);                // タイマの精度を高める
DWORD startTime = timeGettime();   // 開始時刻取得
int frameCount = 0;
while(1) {
    // ゲーム処理...

    DWORD nowTime = timeGettime();    // 現在時刻取得
    frameCount++;
    Sleep(startTime + frameCount * 1000 / 60 - nowTime);    // 調整
}

参考: FPS調整(プチコン)

プチコンはスリープと垂直同期待ち
両方の命令が用意されてたりする


WAIT 10

↑10ミリ秒待つ


VSYNC 1

↑垂直同期を1回待つ

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

キー入力

キー入力で動くようにしてみよう!

キー入力

キー入力はイベント駆動であることが多い

→押し込まれたとき、離されたときなどに通知が来る

onkeydown() {
    // キーが押された時に呼ばれる関数
}

onkeyup() {
    // キーが離された時に呼ばれる関数
}

キー入力

一方で、ゲームを作る上ではメインループの中でボタン情報を使いたい

→どこかに情報を取っておく

let btn = false;
while(1) {
    // メインループの処理
}

onkeydown() { btn = true; }
onkeyup() { btn = false; }

※元からこの機能があり、手でイベント処理を実装する必要が無い物もある

キー入力

左右キーで正方形を左右に移動できるようにしてみよう!

キー入力(JavaScript)

JavaScriptではonkeydown, onkeyupイベントでキー入力を受け取れる
(この辺の処理をやってくれるライブラリも存在するが、今日は使わない)

window.onkeydown = (e) => {
    // 押したときに呼ばれる処理
}
window.onkeyup = (e) => {
    // 離したときに呼ばれる処理
}

キー入力(JavaScript)

let key_left = false, key_right = false;

window.onkeydown = (e) => {
    if(e.key == "ArrowLeft") key_left = true;
    if(e.key == "ArrowRight") key_right = true;
}
window.onkeyup = (e) => {
    if(e.key == "ArrowLeft") key_left = false;
    if(e.key == "ArrowRight") key_right = false;
}

キー入力(JavaScript)

if(key_left) {
    x -= 5;
}
if(key_right) {
    x += 5;
}

キー入力(Siv3D)

Siv3Dはイベント処理を実装しなくても取れるようになっている
(裏にはイベント処理が隠れている)

if (KeyLeft.pressed()) {
    x -= 5;
}
if (KeyRight.pressed()) {
    x += 5;
}

キー入力(PyGame)

PyGameではメインループの中でイベントが取れる

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        pygame.quit()
        sys.exit()
    if event.type == pygame.KEYDOWN:
        # キーが押されたときの処理
    if event.type == pygame.KEYUP:
        # キーが離されたときの処理

キー入力(PyGame)

event.key で押された/離されたキーが取れる

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_LEFT:
        key_left = True
    if event.key == pygame.K_RIGHT:
        key_right = True
if event.type == pygame.KEYUP:
    if event.key == pygame.K_LEFT:
        key_left = False
    if event.key == pygame.K_RIGHT:
        key_right = False

キー入力(PyGame)

if key_left:
    x -= 5
if key_right:
    x += 5

キー入力(Processing)

ProcessingではkeyPressed(), keyReleased()でキー入力を受け取れる

boolean key_left = false, key_right = false;

void keyPressed() {
    if(keyCode == LEFT) key_left = true;
    if(keyCode == RIGHT) key_right = true;
}
void keyReleased() {
    if(keyCode == LEFT) key_left = false;
    if(keyCode == RIGHT) key_right = false;
}

キー入力(Processing)

if(key_left)
    x -= 5;
if(key_right)
    x += 5;

キー入力

左右に動けるようになったかな?

これまでの内容

  • メインループ
  • 描画
  • アニメーションの基本(パラパラ漫画の原理、画面クリア、FPS調整)
  • キー入力

これでもう必要な基礎知識は出揃ったので、あとは実装力勝負です

演習

  1. 四角形を左右だけでなく上下左右にキーで移動できるようにしてみよう
  2. 四角形の色を緑に変えてみよう
  3. 四角形が壁に跳ね返って往復するアニメーションを作ってみよう

小休憩

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

簡易物理の実装

物理エンジンも重要だが
大した演算をしないなら自力で衝突判定などを実装してしまった方が早い

わざわざ高度な物理エンジンを導入して、キャラが勝手に回転して欲しくないから
回転ロックかけるの、バカバカしくないですか?

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

速度の実装

さっきまでの左右移動の実装 (疑似コード)

while(...) {
    if (key_left)
        x -= 5;
    if (key_right)
        x += 5;

速度の実装

これからの左右移動の実装

int vx = 0, vy = 0;     // 速度の初期化
while(...) {
    if (key_left)       // 速度に手を加える
        vx = -5;
    else if (key_right)
        vx = +5;
    else
        vx = 0;
    x += vx;            // X,Y速度を加算する
    y += vy;

速度の実装

「速度を操る部分」と「速度を反映する」部分を分ける

// 速度vx, vyに手を加える
vx = ...
vy = ...

x += vx; // 速度を加算する
y += vy;

これは要するに、「ゲームロジック」と「物理演算」の分離

速度の実装(Siv3D)

int vx = 0, vy = 0;
if (KeyLeft.pressed())
    vx = -5;
else if (KeyRight.pressed())
    vx = +5;
else
    vx = 0;

x += vx;
y += vy;

速度の実装(PyGame)

vx = 0
vy = 0
if key_left:
    vx = -5
elif key_right:
    vx = +5;
else:
    vx = 0;

x += vx
y += vy

速度の実装(JavaScript)

let vx = 0, vy = 0;
if (key_left)
    vx = -5;
else if (key_right)
    vx = +5;
else
    vx = 0;

x += vx;
y += vy;

速度の実装(Processing)

int vx = 0, vy = 0;
if (key_left)
    vx = -5;
else if (key_right)
    vx = +5;
else
    vx = 0;

x += vx;
y += vy;

速度の実装

特に見た目に変化はないが、ちゃんと動いていればOK

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

重力の実装

毎フレームY速度を増加させる (↓疑似コード)

// ゲームロジック
if (key_left) { 
    // ...
}

vy += 1;    // 重力

x += vx;    // 速度の反映
y += vy;

重力の実装

定数は別にしておくと後で変更しやすい(マジックナンバーの回避)

const int gravity = 1;
vy += gravity;    // 重力

重力の実装(Siv3D)

const int gravity = 1;
vy += gravity;

重力の実装(PyGame)

GRAVITY = 1
vy += GRAVITY

重力の実装(JavaScript)

const GRAVITY = 1;
vy += GRAVITY;

重力の実装(Processing)

final int GRAVITY = 1;
vy += GRAVITY;

重力の実装

下に落ちていくようになった

速度の制限

無限に速度が上がっていくとバグの源になりがちなので、
適宜速度を制限するとよい

vx = (vx < -20 ? -20 : vx);     // vxを-20以上+20以下に調整
vx = (vx > +20 ? +20 : vx);
vy = (vy < -20 ? -20 : vy);     // vyを-20以上+20以下に調整
vy = (vy > +20 ? +20 : vy);

速度の制限

maxとminを使うとより簡単に書ける

vx = min(+20, max(-20, vx));
vy = min(+20, max(-20, vy));

言語やフレームワークによっては clamp() のような便利関数もある

vx = clamp(vx, -20, +20);
vy = clamp(vy, -20, +20);

速度の制限

定数定義

const int max_vx = 20, max_vy = 20;
vx = clamp(vx, -max_vx, +max_vx);
vy = clamp(vy, -max_vy, +max_vy);

速度制限(C++)

const int max_vx = 20, max_vy = 20;
vx = std::clamp(vx, -max_vx, +max_vx);
vy = std::clamp(vy, -max_vy, +max_vy);

速度制限(Python)

MAX_VX = 20
MAX_VY = 20
vx = min(+MAX_VX, max(-MAX_VX, vx))
vy = min(+MAX_VY, max(-MAX_VY, vy))

速度制限(JavaScript)

const MAX_VX = 20, max_vy = 20;
vx = Math.min(+MAX_VX, Math.max(-MAX_VX, vx))
vy = Math.min(+MAX_VY, Math.max(-MAX_VY, vy))

速度制限(Processing)

const int max_vx = 20, max_vy = 20;
vx = constrain(vx, -max_vx, +max_vx);
vy = constrain(vy, -max_vy, +max_vy);

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

タイルマップ

アクションゲームあるある: タイルマップ

  • 実装が楽
  • 計算量が低い
  • マップの作成が楽(同じ部品が使いまわせる)

物理エンジンの普及によって相対的な重要度は下がったが
それでも2Dアクションではタイル方式を採用するゲームが多い

タイルマップの定義

2次元配列でマップを定義しよう!


2次元配列: 2つの添え字で要素を指定する配列のこと 格子を表すには最適
traPのプログラミング基礎講習会に無かったので一応解説

タイルマップの定義(C++)

int tiles[8][10] = {
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 1, 1, 1, 1, 0, 0, 0, },
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1, },
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1, },
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, },
};

タイルマップの定義(Python)

tiles = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, ],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, ],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, ],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ],
]

タイルマップの定義(JavaScript)

const tiles = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, ],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, ],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, ],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ],
];

タイルマップの定義(Processing)

int[][] tiles = {
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
    {0, 0, 0, 1, 1, 1, 1, 0, 0, 0, },
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1, },
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1, },
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, },
};

タイルマップの表示(Siv3D)

2重ループでタイルを描画しよう!

const int block_size = 40;  // ブロックの大きさ定義
for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 10; j++) {
        if (tiles[i][j] == 1) {
            Rect{j * block_size, i * block_size,
                block_size, block_size}.draw(Palette::Gray);
        }
    }
}

タイルマップの表示(PyGame)

BLOCK_SIZE = 40    # ブロックの大きさ定義
for i in range(8):
    for j in range(10):
        if tiles[i][j] == 1:
            pygame.draw.rect(
                screen, pygame.Color('gray'),
                pygame.Rect(j * BLOCK_SIZE, i * BLOCK_SIZE,
                    BLOCK_SIZE, BLOCK_SIZE)
            )

タイルマップの表示(JavaScript)

const BLOCK_SIZE = 40;  // ブロックの大きさ定義
ctx.fillStyle = "gray";
for (let i = 0; i < 8; i++) {
    for (let j = 0; j < 10; j++) {
        if(tiles[i][j] == 1) {
            ctx.fillRect(j * BLOCK_SIZE, i * BLOCK_SIZE,
                BLOCK_SIZE, BLOCK_SIZE);
        }
    }
}

タイルマップの表示(Processing)

const int block_size = 40;  // ブロックの大きさ定義
fill(255, 255, 255);
for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 10; j++) {
        if (tiles[i][j] == 1) {
            rect(j * block_size, i * block_size,
                block_size, block_size);
        }
    }
}

タイルマップの表示

タイルが表示できたはず

簡易衝突判定

今日の講習会で一番面倒なところなので気合入れましょう

簡易衝突判定

注意!

ここでの実装は座標を整数で管理することを前提としています

全ての座標は整数で管理してください

サイズ設定

一旦自機のサイズをタイルより小さくする

const int me_w = 30, me_h = 30;

サイズ設定(Siv3D)

const int me_w = 30, me_h = 30;
Rect{ x, y, me_w, me_h }.draw(Palette::Red);

サイズ設定(PyGame)

ME_W = 30
ME_H = 30
pygame.draw.rect(screen, pygame.Color('red'),
                    pygame.Rect(x, y, ME_W, ME_H))

サイズ設定(JavaScript)

const ME_W = 30
const ME_H = 30
ctx.fillRect(x, y, ME_W, ME_H);

サイズ設定(Processing)

final int ME_W = 30
final int ME_H = 30
rect(x, y, ME_W, ME_H);

簡易衝突判定

現状は衝突判定がないので、単にすり抜けて落ちる

簡易衝突判定

少しこの場での用語の定義

  • 交差(intersection): 物体が重なる/めり込むこと
  • 衝突(collision): 動いている物体がぶつかり、跳ね返ったり接した状態になったりすること

「交差検出」はそこまで難しくない!
「衝突処理」が難しい!

  • めり込まないように位置を補正する
  • どの向きにどう跳ね返るか計算する

簡易衝突判定

基本的な考え方:

移動して、もしめり込んでいたら、めり込まない位置まで押し戻す

簡易衝突判定

まっ平らな床であれば判定は簡単!

y += vy;

if(y > floorY) {
    y = floorY;
}

問題はタイルとの判定

タイルとの交差判定

前準備

Q. ある座標(x,y)がタイルにめり込んでいる(交差している)
かどうかを調べるにはどうすればよいか?

タイルとの交差判定

対応する位置のタイルを取得

tiles[y / block_size][x / block_size];  // 切り捨て除算

タイルとの交差判定

タイルを取得して1なら交差していることが分かる

if (tiles[y / block_size][x / block_size] == 1) {
    ...
}

タイルとの交差判定

関数化

bool is_intersect_tile(int x, int y) {
    return tiles[y / block_size][x / block_size] == 1;
}

タイルとの交差判定(C++)

bool is_intersect_tile(int x, int y) {
    return tiles[y / block_size][x / block_size] == 1;
}

タイルとの交差判定(Python)

Pythonでは切り捨て除算の場合/ではなく//を使用する

def is_intersect_tile(x, y):
    return tiles[y // block_size][x // block_size] == 1

タイルとの交差判定(JavaScript)

JavaScriptに切り捨て除算は存在しないので

通常の除算の後にMath.truncで小数点以下を切り捨てる

function is_intersect_tile(x, y) {
    return tiles
        [Math.trunc(y / block_size)]
        [Math.trunc(x / block_size)] == 1;
}

タイルとの交差判定(Processing)

boolean is_intersect_tile(int x, int y) {
    return tiles[y / block_size][x / block_size] == 1;
}

補足

配列外参照に対する対処とかはサボっているので
必要に応じて適宜付け加えてください

床判定

下の辺がめり込んでいたら、めり込んでいない位置まで補正

下の角2点を調べればOK

床判定

右下の点は(x + w, y + h)ではなく(x + w - 1, y + h - 1)であることに注意

(補足: 浮動小数点で座標を実装する場合、-1ではなく-0.00001などを使うと良い)

床判定(C++)

if(is_intersect_tile(x, y + me_h - 1) ||
   is_intersect_tile(x + me_w - 1, y + me_h - 1)) {
    // 速度0にする
    vy = 0;
    // 押し戻し
    y = (y + me_h - 1) / block_size * block_size - me_h;
}

押し戻し処理の図解

床判定(Python)

if (is_intersect_tile(x, y + ME_H - 1) or
   is_intersect_tile(x + ME_W - 1, y + ME_H - 1)):
    vy = 0
    y = (y + ME_H - 1) // BLOCK_SIZE * BLOCK_SIZE - ME_H

床判定(JavaScript)

if (is_intersect_tile(x, y + ME_H - 1) ||
   is_intersect_tile(x + ME_W - 1, y + ME_H - 1)) {
    vy = 0;
    y = Math.truc((y + ME_H - 1) / BLOCK_SIZE) * BLOCK_SIZE - ME_H;
}

床判定(Processing)

if (is_intersect_tile(x, y + ME_H - 1) ||
   is_intersect_tile(x + ME_W - 1, y + ME_H - 1)) {
    vy = 0;
    y = ((y + ME_H - 1) / BLOCK_SIZE) * BLOCK_SIZE - ME_H;
}

床判定

床に乗れるようになった

天井判定

if(is_intersect_tile(x, y) ||
   is_intersect_tile(x + me_w - 1, y)) {
    vy = 0;
    y = (y / block_size * block_size) + block_size;
}

天井判定の図解

天井判定(Python)

if (is_intersect_tile(x, y) or
   is_intersect_tile(x + ME_W - 1, y)):
    vy = 0
    y = (y // BLOCK_SIZE * BLOCK_SIZE) + BLOCK_SIZE

天井判定(JavaScript)

if (is_intersect_tile(x, y) ||
   is_intersect_tile(x + ME_W - 1, y)) {
    vy = 0;
    y = (Math.truc(y / BLOCK_SIZE) * BLOCK_SIZE) + BLOCK_SIZE;
}

天井判定(Processing)

if (is_intersect_tile(x, y) ||
   is_intersect_tile(x + ME_W - 1, y)) {
    vy = 0;
    y = (y / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE;
}

簡易衝突判定

左右壁判定もやって完成...

と行きたいところだが、ここで重要な注意点

簡易衝突判定

処理順序がとても重要 以下のどちらかでないとダメ!!

x += vx;
(左右壁判定)
y += vy;
(天井・床判定)
y += vy;
(天井・床判定)
x += vx;
(左右壁判定)

簡易衝突判定

これはアウト!!

x += vx;
y += vy;
(左右壁判定)
(天井・床判定)

以下の記事に詳しい

2Dゲームの衝突判定と座標軸別移動衝突法(仮称)について

左右壁判定(C++)

if(is_intersect_tile(x, y) ||
   is_intersect_tile(x, y + me_h - 1)) {
    vx = 0;
    x = x / block_size * block_size + block_size;
}
if(is_intersect_tile(x + me_w - 1, y) ||
   is_intersect_tile(x + me_w - 1, y + me_h - 1)) {
    vx = 0;
    x = (x + me_w - 1) / block_size * block_size - me_w;
}

左右壁判定(Python)

if (is_intersect_tile(x, y) or
   is_intersect_tile(x, y + ME_H - 1)):
    vx = 0
    x = (x // BLOCK_SIZE * BLOCK_SIZE) + BLOCK_SIZE
if (is_intersect_tile(x + ME_W - 1, y) or
   is_intersect_tile(x + ME_W - 1, y + ME_H - 1)):
    vy = 0
    x = (x + ME_W - 1) // BLOCK_SIZE * BLOCK_SIZE - ME_W

左右壁判定(JavaScript)

if (is_intersect_tile(x, y) ||
   is_intersect_tile(x, y + ME_H - 1)) {
    vx = 0;
    x = (Math.truc(x / BLOCK_SIZE) * BLOCK_SIZE) + BLOCK_SIZE;
}
if (is_intersect_tile(x + ME_W - 1, y) ||
   is_intersect_tile(x + ME_W - 1, y + ME_H - 1)) {
    vx = 0;
    x = Math.truc((x + ME_W - 1) / BLOCK_SIZE) * BLOCK_SIZE - ME_W;
}

左右壁判定(Processing)

if (is_intersect_tile(x, y) ||
   is_intersect_tile(x, y + ME_H - 1)) {
    vx = 0;
    x = (x / BLOCK_SIZE * BLOCK_SIZE) + BLOCK_SIZE;
}
if (is_intersect_tile(x + ME_W - 1, y) ||
   is_intersect_tile(x + ME_W - 1, y + ME_H - 1)) {
    vx = 0;
    x = ((x + ME_W - 1) / BLOCK_SIZE) * BLOCK_SIZE - ME_W;
}

簡易衝突判定

壁にぶつかれるようになったことを確認しよう

これでこの講習会の山場は越えました!

簡易衝突判定

発展課題

  • タイルよりも主人公が大きい場合は問題が起きる。なぜか?
    また、どのような実装をすれば解決するか?
  • 速度が速すぎる場合はタイルをすり抜けてしまう。速度は最大でどこまでであればバグが起きないか?また、速度に関係なくすり抜けない実装は可能か?
  • 坂道タイルはどのようにすれば実装できるか?
  • 下からすり抜ける足場はどのようにすれば実装できるか?

今日の予定(前半)

  • メインループ
  • 描画
  • アニメーション
  • キー入力
  • 簡易物理の実装
    • 速度
    • 重力
    • タイルマップと簡易衝突判定
  • ジャンプの実装

ジャンプの実装

Y方向の速度を瞬間的に上げるとジャンプになる

スペースキーでジャンプできるようにしてみよう

「押した瞬間」判定

ゲームで頻繁に必要になるのが押した瞬間判定

「押した瞬間」判定(Siv3D)

Siv3Dでは KeySpace.down() のようにすると実は簡単に取れるが
ここではあえて手で実装してみる

bool key_space_old = false;
while(...) {
    bool key_space = KeySpace.pressed();
    bool key_space_down = !key_space_old && key_space;

    key_space_old = key_space;
}

実開発では普通に .down() 使ってね

「押した瞬間」判定(PyGame)

key_space = False
key_space_old = False
while True:
    # (イベント処理、スペースキー(K_SPACE)の情報を取得)
    key_space_down = !key_space_old and key_space

    key_space_old = key_space

PyGameはメインループ中でイベント取れるので
そのまま使っちゃえばいいかも...

「押した瞬間」判定(JavaScript)

let key_space = false, key_space_old = false;
function update() {
    const key_space_down = !key_space_old && key_space;

    key_space_old = key_space;
}

onkeydown() { /* ... */ }
onkeyup() { /* ... */ }

「押した瞬間」判定(Processing)

boolean key_space = false, key_space_old = false;
void draw() {
    boolean key_space_down = !key_space_old && key_space;

    key_space_old = key_space;
}

onkeydown() { /* ... */ }
onkeyup() { /* ... */ }

ジャンプの実装(C++)

スペースキーを押した瞬間に上方向Y速度を与える

if(key_space_down) {
    vy = -15;
}

ジャンプの実装(Python)

if key_space_down:
    vy = -15

ジャンプの実装(JavaScript)

if(key_space_down) {
    vy = -15;
}

ジャンプの実装(Processing)

if(key_space_down) {
    vy = -15;
}

ジャンプの実装

ジャンプできるようになった! しかし空中でもジャンプできてしまう!

地面でしかジャンプできないようにしよう

接地判定(C++)

床衝突判定に一工夫

bool onground = false;
if ( /* (略) */ ) {
    vy = 0;
    y = (y + me_h - 1) / block_size * block_size - me_h;
    onground = true;
} else {
    onground = false;
}

接地判定(C++)

接地中しかジャンプできないようにする

if(key_space_down && onground) {
    vy = -15;
}

接地判定(Python)

onground = False
if key_space_down and onground:
    vy = -15;
if (略):
    vy = 0
    y = (y + ME_H - 1) // BLOCK_SIZE * BLOCK_SIZE - ME_H
    onground = True
else:
    onground = False

接地判定(JavaScript)

let onground = false;
if (key_space_down && onground)
    vy = -15;
if (/* 略 */) {
    vy = 0;
    y = Math.truc((y + ME_H - 1) / BLOCK_SIZE) * BLOCK_SIZE - ME_H;
    onground = true;
} else {
    onground = false;
}

接地判定(Processing)

boolean onground = false;
if (key_space_down && onground)
    vy = -15;
if (/* 略 */) {
    vy = 0;
    y = ((y + ME_H - 1) / BLOCK_SIZE) * BLOCK_SIZE - ME_H;
    onground = true;
} else {
    onground = false;
}

接地判定

空中ジャンプできなくなったことを確かめよう

これでアクションゲームの原型が完成!!

ブレはあるが大体100行程度だと思う

ジャンプの実装

発展課題

  • ジャンプの最高到達点はおよそいくらになるか?
    • 中学校数学: 二次関数の頂点
  • ボタンを押す長さでジャンプ力を調整できるようにする
  • プログラマーが重力やジャンプ力などをもっと微調整できるようにする
    • 座標を浮動小数点管理にする
    • 座標を固定小数点管理にする(+1ドットを+256で表したりする)
  • 二段ジャンプの実装
  • 壁キックの実装

ジャンプの味付けは本当に幅が広いので色々なゲームを遊んでみましょう

前半終わり

休憩です