🔰

疑似3Dでかんたんアウトラン

2024/06/17に公開

アウトランとは!?

アウトランは、1980年代にゲームセンターを大いに賑わせた大人気カーレースゲームです。

今回は疑似3Dで、アウトランの様なスピード感ある演出に挑戦してみます。(せ〜がぁ〜♬)
※重要でない箇所は解説を省略しておりますので予めご了承下さい


完成イメージ

ここでは、描画にOpenProcessingを利用します。
使い方に関しては、p5.jsをかじる本を参考にして頂けると幸いです。

疑似3Dとは

疑似3Dとは、3D座標(x,y,z)を2D座標(x,y)に変換する手法の事を言います。
文字通り、"擬似的に3Dを表現する"という意味ですね。

疑似3Dでは、次の手順に従って2D座標への変換処理を行います。
(x座標についても同様に計算します)

  1. スクリーンまでの距離を3D空間のz座標で割り算しその比を求める
  2. 3D空間のy座標に先程求めた比を掛け算する
  3. 結果として求められた値が描画すべき2D座標となる


疑似3Dを解説する定番の図

上記の変換処理を一つの関数にまとめておきます。
引数に3D空間上の座標(x,y,z)を与えると、2D座標に変換された座標を返す関数です。

main.js
// 3D座標を2D座標に変換する関数
function project(x, y, z){
	const s = SCREEN / z;
	const _x = x*s + width/2;
	const _y = y*s + height/2;
	return [_x, _y, s];
}
定数名 説明
SCREEN スクリーンまでの距離
width キャンバスの横幅
height キャンバスの高さ

以上の理屈で疑似3Dを実現します。

配置して確かめる

先程作った関数をさっそく試してみましょう。
まず最初に3D空間のx-z平面に対し、碁盤の目状にプロットした配列を用意します。

main.js
// x-z平面に10x10の点を用意する
const pad = 30;
for(let i=-5; i<5; i++){
    for(let j=0; j<10; j++){
        const x = i * pad;
        const y = 0;
        const z = j * pad;
        const point = {x: x, y: y, z: z}
        points.push(point);
    }
}

次に、配列から順番に座標を取り出し2D座標に変換します。

main.js
// キャンバスに描画する
for(const point of points){
    // 3D座標を2D座標に変換
    const [x, y, s] = project(point.x-eyeX, point.y+eyeY, point.z-eyeZ);
    circle(x, y, 10);
}

全体のコードは次の通りです。

3D空間に点を配置

実行すると次の様な結果になります。(既に道っぽい!!)

綺麗に配置出来ました。(やりました!!)

こちらで動作確認をする事ができます。

いよいよアウトラン

疑似3Dの理屈がわかったところで、いよいよ道路を作ります。

地平線まで続く道路は、下のイメージの様に横線を使って表現する事にしましょう。


地平線まで続く横線

先ずは、道幅と横線の間隔(z軸方向)を格納する定数を用意します。
(この値は固定値です)

定数名 説明
R_WIDTH 道の横幅
R_DEPTH 道の奥行

次に、横線の役目をするクラス"MyLine"を準備します。
このクラスにはメンバ変数として座標、横幅、カーブ度合い、バンク度合いを持たせておきます。(後で使いますよ)
"project()"関数には、3D座標から2D座標に変換する処理を実装してあります。

main.js
// ラインオブジェクト(台形の横線で使う)
class MyLine{

	constructor(){
		this._x = 0;// 線の中心座標x
		this._y = 0;// 線の中心座標y
		this._w = 0;// 線の横幅
		this._c = 0;// 線のカーブ度合い
		this._b = 0;// 線のバンク度合い
	}

	project(x, y, z){// 2D空間の座標に変換
		const s = SCREEN / z;
		this._x = x*s + width/2;
		this._y = y*s + height/2;
		this._w = R_WIDTH * s;
	}

	get x(){return this._x;}
	get y(){return this._y;}
	get w(){return this._w;}

	set curve(n){this._c = n;}
	get curve(){return this._c;}

	set bank(n){this._b = n;}
	get bank(){return this._b;}
}

重要な定数とクラスが揃いました。
あとは、配列にある横線オブジェクトをまとめて描画するだけです。

完成コードを見てください。

横線を地平線まで

実行すると次の様な結果になります。


さっきみたやつですw

一歩近づきました。(やりました!!)

こちらで動作確認をする事ができます。

道を進む

道が出来たので後は進むだけです。

グローバル変数に仕込んであった"視点z座標"に小さい値を加算していきます。
すると、結果として視点が前方に移動して前に進んでいく表現になります。

変数名 説明
eyeX 視点x座標
eyeY 視点y座標
eyeZ 視点z座標

"eyeZ"の座標から前方だけ描画すれば良いので、配列から必要な数だけ横線オブジェクトを取り出し描画します。

次のコードを確認してみてください。

main.js
function draw(){
	background(BLACK);

	eyeZ += 2;// 視点を前へ
	
	// キャンバスに描画する(eyeZの位置から前方80個分だけ取り出す)
	const start = floor(eyeZ/R_DEPTH) + 1;
	for(let i=start; i<start+80; i++){
		const iA = i % lines.length;
		const lA = lines[iA];
		lA.project(eyeX, eyeY, R_DEPTH*i-eyeZ);
		line(lA.x-lA.w/2, lA.y, lA.x+lA.w/2, lA.y);
	}
}

完成コードは次のとおりです。

道を前に進む

実行すると次の様な結果になります。


道を進む

進んでいる表現が出来ました。(やりました!!)

こちらで動作確認をする事ができます。

曲がったり上下したり

真っすぐだけの道では味気がありません。
横線オブジェクトに少しづつ変化を与える事で、カーブやバンク(坂道)する表現を加える事ができます。

コースを作る箇所(ラインオブジェクトを作る箇所)でカーブ度合いとバンク度合いを加減します。
"curve"で曲がり具合を、"bank"で上り下り具合を決めます。

コースを作る(抜粋)
main.js
function setup(){
	//createCanvas(windowWidth, windowHeight);
	createCanvas(480, 320);
	angleMode(DEGREES); frameRate(60);
	noFill();
	stroke(WHITE); strokeWeight(1);

	// ラインオブジェクトを用意
	for(let i=0; i<100; i++){
		const line = new MyLine();

		if(20<i && i<40){
			line.curve = 0.8;
			line.bank = 0.8;
		}
		if(40<i && i<60){
			line.curve = -0.5;
			line.bank = -0.2;
		}

		line.project(eyeX, eyeY, R_DEPTH*i-eyeZ);
		lines.push(line);
	}
}

次に、コースを描画する箇所でカーブ度合いやバンク度合いを加味しつつ描画します。

コースを描画する(抜粋)
main.js
function draw(){
	background(BLACK);

	eyeZ += 2;// 視点を前へ

	// カーブ
	let oX = 0;
	let dX = 0;
	// バンク
	let oY = 0;
	let dY = 0;
	
	// キャンバスに描画する
	const start = floor(eyeZ/R_DEPTH) + 1;
	for(let i=start; i<start+80; i++){
		const iA = i % lines.length;
		const lA = lines[iA];

		oX += dX;
		dX += lA.curve;// 次に描画するカーブの差分

		oY += dY;
		dY += lA.bank;// 次に描画するバンクの差分

		lA.project(eyeX-oX, eyeY-oY, R_DEPTH*i-eyeZ);
		line(lA.x-lA.w/2, lA.y, lA.x+lA.w/2, lA.y);
	}
}

ここまでの完成コードは次の通りです。

曲がったり上下したり

実行すると次の様な結果になります。

紆余曲折していていい感じ!?

こちらで動作確認をする事ができます。

シマシマにしてよりそれらしく

最後は仕上げです。
配列に格納された横線オブジェクトを2つづつ取り出して塗りつぶします。
この時、シマシマ柄にする事でより疾走感のある表現になります。

最終的な完成コードは次の通りです。

シマシマ柄にして疾走感

実行すると次の様になります。


完成

お疲れ様でした!!

こちらで動作確認をする事ができます。

最後に

今回は、アウトランの様な表現を擬似3Dを使って挑戦してみました。
3Dライブラリを使わずに自力で座標を求めるはメチャクチャ楽しいですよー。
是非挑戦してみて下さいね。ޱ(ఠ皿ఠ)ว

他にも、ゲームに使えるかもしれない記事も御座いますのでよろしければ!!

迷路アルゴリズムであっさりダンジョン
経路探索3種であっさりダンジョン攻略

ここまで読んでいただき有難うございました。

Discussion