OBAQの面白くない話

目次

OBAQの面白くない話

 OBAQはプログラミング言語HSP3に同梱されている、2D専用物理演算モジュールです。 お手軽に物理表現を行うことができます。 サンプルをベースに感覚で調整して作っていくだけでも面白いものが出来たりします。

 しかし作っていると、どうしても厳密な意味や単位が気になって来ることがあります。 HDLやobaq.txtを読んでもあまり詳しく書かれていません。 ということでこのページでは、私調べのOBAQについての解説を書いてみます。

内部座標と外部座標

 まずは前提として理解しておいてほしい情報の確認です。 OBAQには内部座標外部座標の2つの座標系があります。 内部座標は、OBAQの内部で使用する仮想の座標系を持っています。 qposqgetposで取り扱う座標値は、内部座標を使っています。 内部座標に対して、画面に表示される座標系を外部座標と呼んでいます。 描画されるウィンドウの座標系で、通常のpos命令などで使用されている座標系です。

 内部座標と外部座標の関係はqview命令で設定することができます。 初期値は拡大率4.0倍、オフセット0.0です。 現在の設定がどうなっているかは取得できないので、自分で管理する必要があります。 qview命令については、後でもう少し詳しく説明します。

 内部座標と外部座標の相互変換は、描画時などに頻繁に使用することになります。qcnvaxis命令を使うと簡単に相互変換することができます。

 内部座標は、見た目の位置と数値が感覚的に一致しないのでデバッグ作業用に以下のようなスクリプトを入れておくと便利です。 マウス位置の座標を手軽に確認できます。

HSP3

; マウス座標
x = mousex
y = mousey
qcnvaxis qx, qy, x,y, 1
pos 50, 40
mes strf("内部座標:%6.2f, %6.2f", qx,qy)
mes strf("外部座標:%d, %d", x,y)

外壁 qborder

 qresetすると自動的に最初に作られるオブジェクトで、外壁になるオブジェクトです。オブジェクトIDは0。

 ウィンドウサイズに合わせて作成されますが、存在を認識しやすくするためか周囲に余白が設けられています。 使い方の確認を踏まえてウィンドウサイズいっぱいにサイズを変えてみます。 sample\obaq\test1.hsp のqresetの直後に以下の処理を追加してみてください。

HSP3

;	外壁を設定
;ウィンドウサイズいっぱい
qcnvaxis x1,y1, 0, 0, 1
qcnvaxis x2,y2, ginfo_winx-1, ginfo_winy-1, 1
qborder x1,y1,x2,y2
qpos 0, 0,0,0

 これでウィンドウサイズに合いました。 ウィンドウ内の左上座標と右下座標をOBAQ内部座標に変換してqborderに渡します。 外壁の初期位置はウィンドウ中央なので、qposで位置を再設定しています。 内部座標 0,0 は、初期値背はウィンドウ左上と一致するので位置がぴったりあうようになります。

 なお、qborderは外壁の設定を変えるだけで、外壁を追加や新規で作るわけではありません。 qdelしちゃった場合はqborderで作れないので注意が必要です。 qdelしてもID 0 が解放されない事も知っておくとよいかもしれません。

ビュー操作

 d3moduleやHGIMG4のカメラに相当する機能として、OBAQにはqview命令があります。 内部座標を外部座標に変換するパラメーターを設定する命令です。マニュアルを見ただけでは、設定値の意味が少しわかりにくい部分があります。

 qview命令の「表示オフセット」が内部座標なのか外部座標なのか、マニュアルには明記されていません。 実際に試せば動作で理解できますが、初見では迷いやすいところです。 結論としては、表示オフセットは「外部座標」で指定します。名前のとおり「表示」位置を調整するためのものなので、外部(画面)座標系ととして覚えておくとわかりやすいでしょう。

 ズーム倍率は、内部座標を拡大・縮小して外部座標に描画するための値です。 つまり、「内部座標 × ズーム倍率」を計算し、その結果を表示オフセットでずらして外部座標系に描画する、という流れです。

 では応用編としてマウス操作で拡大縮小を行うスクリプトを紹介します。

HSP3

;
;	ビューの操作
;
; ビューの移動と拡大縮小を行うサンプルです。
; マウス操作で動かすことができます。
; 任意の座標を中心に拡大縮小させています。
;
#include "obaq.as"

screen 0,640,480	; ウィンドウ初期化

qreset			; OBAQの初期化
qaddpoly my, 3, 96,20,0	; 三角形を追加
zm = 4.0		; ズーム倍率
offsetX = 0.0	; 表示オフセット
offsetY = 0.0
qview zm, zm, offsetX, offsetY

;-----------------------------
;	メインループ
;-----------------------------
*main
	redraw 0		; 画面の更新を開始
	color 0,0,0:boxf	; 画面をクリア
	qexec			; OBAQによるオブジェクトの更新

	;-----------------------------
	;	入力
	;-----------------------------

	;	マウス
	mousePosX  = double(mousex)
	mousePosY  = double(mousey)
	mouseWheel = mousew

	;	マウスで表示位置を移動
	key0 = key
	stick key, $100

	;-----------------------------
	;	マウスドラッグ操作で移動
	;-----------------------------
	; ドラッグ開始位置から終了位置までのシフト量を offsetX1,offsetY1 に記録
	; offsetX1,offsetY1 : 
	;     移動操作中のオフセット量
	;     誤差蓄積を避ける目的で使用します。
	
	; OFF->ON
	if key & $100 { ; $100 マウスの左ボタン
		if (key0 & $100) = 0 {
			; ドラッグ開始座標
			dragStartX = mousePosX
			dragStartY = mousePosY
		}
		offsetX1 = mousePosX - dragStartX
		offsetY1 = mousePosY - dragStartY
	}
	
	; ON->OFF
	; オフセット量を確定
	if ((key0 & $100)!0) & ((key & $100)=0) {
		offsetX += offsetX1
		offsetY += offsetY1
		offsetX1 = 0.0
		offsetY1 = 0.0
	}
	
	;-----------------------------
	;	ホイール操作で拡大縮小
	;-----------------------------
	zm0 = zm
	if mouseWheel > 0 : zm *= 1.1
	if mouseWheel < 0 : zm /= 1.1
	if (zm < 0.1) | (zm > 10.0) : zm = zm0

	; 拡大縮小操作中
	if zm0 ! zm {
		; マウス位置を中心に拡大させる
		; オフセット量を調整して、マウスで押さえている位置が動かないように調整
		; ドラッグしながらの拡大縮小にも対応

		; 現在の全体オフセット(確定+ドラッグ中)
	    curOffsetX = offsetX + offsetX1
	    curOffsetY = offsetY + offsetY1
	    ; ズーム補正
		offsetX = (curOffsetX - mousePosX) * zm / zm0 + mousePosX - offsetX1
		offsetY = (curOffsetY - mousePosY) * zm / zm0 + mousePosY - offsetY1
	}

	;-----------------------------
	;	表示サイズと位置を変更
	;-----------------------------
	; ・ズーム倍率は、左上が基準
	; ・オフセット量はウィンドウのピクセルサイズ(OBAQの内部座標ではない)
	qview zm, zm, offsetX + offsetX1, offsetY + offsetY1
	
	;-----------------------------
	;	描画
	;-----------------------------
	qdraw			; オブジェクトの描画
	pos 50,50 : color 255,255,255
	mes "マウスドラッグでビューを移動"
	mes "マウスホイールで拡大縮小"
	mes "オフセット量:" + (offsetX + offsetX1) + ", " + (offsetY + offsetY1)
	mes "拡大率:" + zm
	
	redraw 1		; 画面の更新を終了
	await 16		; 一定時間待つ
	goto *main

 内部座標の拡大率やオフセット量は自分でコントロールしているので、qcnvaxis命令を使用していません。

 パン操作は、毎フレームドラッグ量を保存すると誤差蓄積でずれることがあるので、ドラッグ開始位置を保存しています。 拡大縮小はマウス位置を中心にしたかったので、オフセット量も拡大率の影響を受けます。この辺りも考慮してオフセット量を補正しています。

HSP3

if mouseWheel > 0 : zm *= 1.1
if mouseWheel < 0 : zm /= 1.1
if (zm < 0.1) | (zm > 10.0) : zm = zm0

 拡大率の変更は、拡大も縮小も 1.1 倍ずつとしています。縮小する際に zm *= 0.9 としないのは元の倍率に戻らないからです。 1/1.1 ≠ 0.9 だからです。

 拡大率の範囲指定にlimitfを使用しないのも同じ理由です。任意の倍率にしてしまうと、初期設定の倍率に戻せなくなります。

力(距離、質量、時間)

 OBAQのパラメータは、フィーリングで適当に設定して使うのが一番良いと思っています。 物理的な正確性はゲームではあまり意味がありませんし、むしろ現実的ではない設定値の方が爽快なプレイ感につながることがあります。

 といいつつも、OBAQの内部ではどのような処理が行われているかを把握しておくと、パラメータ調整の悩み解決につながるかもしれません。 そこで基本的なパラメータについての考え方を調べてみました。公式の見解ではなく私調べなので嘘や間違いがあるかもしれません。その点はご理解の上、読み進んでください。

1フレーム当たりの計算回数

 HSP3で作られるゲームでは、通常60fps(1フレーム約16.7ms)や30fps(1フレーム約33ms)の周期(フレームレート)で描画が行われます。 このフレームレートを基準に時間が進行すると、現実世界では当然衝突するはずの「壁にボールを投げる」という動作でも、壁の厚み・ボールの大きさ・速度の関係によっては、ボールが壁をすり抜けてしまうことがあります。 たとえば、壁が薄い・ボールが小さい・移動速度が速いといった条件では、この問題が発生しやすくなります。 このような現象を「トンネル現象(tunneling)」と呼びます。

 トンネル現象については、この辺の記事を読んでおくと理解が深まると思います。

 さて、この問題を完全に防ぐのは難しいため、OBAQでは1フレーム内で物理演算を複数回行う「サブステップ方式」が採用されています(Unityで言うところの Fixed Timestep に相当)。 1フレームあたりの物理演算回数を増やすことで、描画負荷を大きく変えずに、物体のすり抜け(トンネル現象)の発生を抑えることができます。

 OBAQのデフォルト設定では、1フレームにつき4回の物理計算を実行します。 この計算回数は qsetreq 命令で変更でき、現在の設定値は qgetreq 命令で取得できます。

OBAQの単位

 現実の世界では、長さ[m]・質量[kg]・時間[s]を基準としたSI単位系(MKS単位系)が使われていますが、ゲームの中では現実と同じスケールを使う必要はありません。 物理演算ライブラリの内部では、処理の安定性や精度を保つため、必ずしもSI単位系をそのまま使うとは限りません。 OBAQでも、ゲーム表現に適した独自のスケールを採用しています。

 OBAQの内部では、長さはメートルではありませんし、重さはキログラムではありません。時間もではありません。じゃあ何なんだよ…となるので、これからその話をします。

OBAQの時間

 OBAQ内の時間の単位は、計算回数です。単位時間は1回の計算を指します。 デフォルトでは1フレームにつき4回計算するので、時間は1フレームにつき「4」経過していることになります。 では、その確認ができるプログラムを見てみましょう。(マニュアルへの記載はありませんが多分あってる。それを証明する方法が思いつかないので、ここでは確認としています。そう考えても問題なかろうという確認です。)

HSP3

;
;	obaqの速度
;
; ・obaqの速度は、1回の計算当たりに移動する距離です。
; ・obaqの速度を1フレーム間の移動距離から算出する場合は、1フレームの経過時間(=1フレーム当たりの計算回数)で割る必要があります。
;
#include "obaq.as"

qreset			; OBAQの初期化

; 1フレームあたりの物理計算回数
; qsetreq REQ_PHYSICS_RATE, 1	; 回数を変えても同じ動きになる
qgetreq rate, REQ_PHYSICS_RATE

; 重力加速度(重力の強さ)
qgravity 0.0, 0.0

; オブジェクトを追加
; 重力と抵抗をなしにして初速を与えます。
qaddpoly my1, 3, 20,60,0
qinertia my1, 1.0	; 外的要因を除去
qspeed my1, 0.1		; 初速

*main
	redraw 0
	color 0,0,0:boxf
	qexec

	;	移動速度を取得
	qgetspeed my1, qvxA, qvy, qva
	
	;	移動速度を位置変化から計算
	qpx0 = qpx
	qgetpos my1, qpx, qpy, qpa
	; 速度 = 1フレームの移動距離 / 1フレームの経過時間
	;
	; 1フレームの移動距離 = qpx - qpx0
	; 1フレームの経過時間 = rate
	qvxB = (qpx - qpx0) / rate

	;	結果を描画
	pos 50, 50
	color 255,255,255
	mes "移動速度"
	mes strf("qgetspeed 取得値:%f [dot/計算]", qvxA)
	mes strf("qgetpos   測定値:%f [dot/計算]", qvxB)
	
	qdraw
	redraw 1
	await 16	; 60fps
	goto *main

 このプログラムは、速度をOBAQの命令で取得する方法と、自分で計算して算出する方法との結果を比較したものです。実行すると命令で取得した速度と、自前での計算結果とがよく一致しています。計算方法に間違いないとみていいでしょう。

 qgravityqinertiaは、速度が変化する外的要因を排除するために指定しています。 測定対象となるオブジェクトは、初速だけを与えているので等速直線運動をしています。壁にぶつかると反発係数の影響で少し減速していますね。

 さて、自前での速度算出は、次の式で行っています。

HSP3
; 速度 = 1フレームの移動距離 / 1フレームの経過時間
qvxB = (qpx - qpx0) / rate

 rateは、qgetreqで取得した1フレーム当たりの計算回数。つまり1フレーム当たりの経過時間です。 qsetreq命令で1フレーム当たりの計算回数を変更してみても同じ結果が得られることが確認できます。

OBAQの長さ

 OBAQ内での長さの単位はユーザーが決めることができます。ですが、重力加速度のデフォルト値から単位長さがどのくらいに設定されているのか調べることができるので計算してみました。 重力加速度のデフォルト値が0.005なので、この値が9.8m/s2と仮定した場合、物理世界での1mが、OBAQ内での長さでいくつになるかを計算してみます。

 加速度から距離を求める式が「距離 = 1/2 × g × t^2」なので、1秒後の距離で計算して比較してみます。OBAQの長さ単位をここでは便宜上「ポイント」と呼ぶことにします。

現実世界の距離:9.8 / 2 [m]
OBAQ世界の距離:0.005 * (rate * fps)^2 / 2 [ポイント]

 この2つの距離は同じ長さで、単位が異なっているだけです。1mの長さが何ポイントになるか計算してみます。

0.005 * (rate * fps)^2 / 2 / (9.8 / 2)
= 0.005 * (rate * fps)^2 / 9.8

 ここで、rateをデフォルト値である4、fpsを60と仮定して計算することにします。

0.005 * (4 * 60)^2 / 9.8 ≒ 29.4

 1m ≒ 29.4ポイント。OBAQの内部座標の長さを29.4で割ればメートルに換算出来るというわけです。 単位長さ1ポイントは、1/29.4mとなります。3.4cmぐらいですね。 一つ注意が必要なのはfps値やフレーム当たりの計算回数が変わると、この換算値も変化するという点です。OBAQで使用されている絶対的な単位換算値ではない点に注意してください。

 OBAQ内での長さの単位はユーザーが決めていいので、1mあたりのポイント数から重力加速度を設定しなおした方が作業性はいいと思います。次のようにして設定するといいでしょう。

HSP3

grav_si  = 9.8	; 重力加速度 [m/s2]
pt_meter = 29.4	; [ポイント/m]
fps      = 60.0	; fps値
qgetreq rate, REQ_PHYSICS_RATE
grav = grav_si * pt_meter / powf(rate * fps, 2)
qgravity 0.0, grav

 確認用のサンプルプログラムを置いておきます。オブジェクトを自由落下させるプログラムです。

HSP3

;
;	obaqの重力加速度
;
#include "obaq.as"

;screen 0, 640, 480
screen 0, 640, 800
qreset			; OBAQの初期化

; 1フレームあたりの物理計算回数
; qsetreq REQ_PHYSICS_RATE, 1	; 回数を変えても同じ動きになる
qgetreq rate, REQ_PHYSICS_RATE

; 重力加速度(重力の強さ)
grav = 0.005	; 見やすくするために小さく設定してます。
; grav = 9.8*29.4/powf(4*60,2)
qgravity 0.0, grav

; オブジェクトを追加
; 重力と抵抗をなしにして初速を与えます。
qaddpoly my1, 3, 120, 20, 0
qinertia my1, 1.0	; 外的要因を除去
qy0 = 20.0

frm = 0
*main
	redraw 0
	color 0,0,0:boxf
	qexec

	;	移動速度を取得
	qgetspeed my1, qvxA, qvyA, qvaA
	
	;	加速度から速度を計算
	; v = v0 * gt
	qvyB = grav * frm * rate
	frm++

	;	位置取得
	qgetpos my1, qxA, qyA, qaA

	;	加速度から位置を計算
	; x = 1/2 * g * t^2 + x0
	qyB = 0.5 * grav * powf(frm * rate, 2) + qy0
	
	;	結果を描画
	pos 50, 50
	color 255,255,255
	mes "速度"
	mes strf("qgetspeed  取得値:%f [dot/計算]", qvyA)
	mes strf("重力加速度から算出:%f [dot/計算]", qvyB)
	mes "位置"
	mes strf("qgetpos   取得値:%f [dot/計算]", qyA)
	mes strf("重力加速度から算出:%f [dot/計算]", qyB)
	mes strf("落下距離:%f m", (qyA-qy0)/29.4)
	qdraw

	; 見逃し防止のために適当なところで停止
	if frm = 60 : redraw 1 : stop

	redraw 1
	await 16	; 60fps
	goto *main

 重力加速度から速度と位置を求めて、計算方法の確認をしています。 1秒後に停止して数値を確認できるようにしています。重力加速度が9.8m/s2なら4.9m落下しているはずです。

OBAQの質量

 これもOBAQ内部では定義されているわけではありません。 重力加速度のような距離の換算値を求めるヒントもありません。これについては、素直にキログラムと考えてしまって良いと思います。

qpush命令の「力」

 qpushは、任意の場所に力を与える命令です。力と書いてあるので、SI単位系でいうところのニュートン(N)で表される(Force)だと考えてしまいそうになるのですが、実際には力積(Impulse)を与える命令です。

 力積とは、物体に作用する力とその力の作用する時間とをかけ合わせた値のことです。正確には、力の積分値。 例えば、あるオブジェクトに1フレームの間に同じ力Aを加え続ける場合、qpushで指定する力積は「A×1フレーム当たりの計算回数」となります。 力積は、衝突処理を安定に行えるメリットがある一方、継続的な力の表現には向かず誤差が生じやすいデメリットがあります。

 確認用のプログラムを書いてみたので載せておきます。デメリットが出やすい使い方ではありますが、なかなかいい精度が出ていると思います。

HSP3

;
;	qpush
;
; ・左側のオブジェクトは、obaqの重力設定で落下しています。
; ・右側のオブジェクトは、qpushで動かしています。
; ・同じ速度で動いているので、同じ大きさの力が働いていることがわかります。
;
; ・qpushは力積を設定する命令です。
;
#include "obaq.as"

screen 0,640,480	; ウィンドウ初期化
qreset			; OBAQの初期化

; 1フレームあたりの物理計算回数
; qsetreq REQ_PHYSICS_RATE, 1	; 回数を変えても同じ動きになる
qgetreq rate, REQ_PHYSICS_RATE

; 重力加速度(重力の強さ)
grav = 0.0005
qgravity 0.0, grav

; オブジェクト1を追加
; - 惰性無効化
qaddpoly my1, 3, 70,20,0
qinertia my1, 1.0, 1.0

; オブジェクト2を追加
; - 惰性無効化
; - 重力無効化
qaddpoly my2, 3, 90,20,0
qinertia my2, 1.0, 0.0


*main
	redraw 0
	color 0,0,0:boxf
	qexec

	; 落下する加速度を実測する
	; - 前フレームとの速度差から算出
	; - 1フレーム当たりrate回の計算が行われているので、rateで割って1回の計算当たりの加速度に変換
	; - 設定してある重力加速度と同じ値が得られた
	; よって重力加速度の時間単位も計算回数である。
	qvy0 = qvy
	qgetspeed my1, qvx, qvy, qva


	; 疑似重力でオブジェクトを動かす
	qgetweight my2, mass, mmoment
	qgetpos my2, x,y,a
	; - my2に重力相当の力Fを加えたい。Fは次式で算出。
	; 	F = mass * grav
	; - qpushに F * rate を設定すると自由落下と同じ動きをする。
	; よって、qpushに設定する値は、1フレーム当たりの力積である。
	qpush my2, x,y, 0.0, mass * grav * rate

	
	pos 50, 50
	color 255,255,255
	mes strf("落下加速度:%f", (qvy - qvy0) / rate)
	
	qdraw
	redraw 1
	await 12
	goto *main

 フレームごとに、mass * gravの力をrate時間分継続して入力しています。 なので、mass * grav * rateとしています。

 瞬間的な衝撃を与える場合は、この例のように接触している間だけqpushし続ける・ボタンを押している間だけqpushし続ける…という使い方ではなく、接触したら1回だけqpushするとか、ボタンを押したら1回だけqpushするとかといった運用がいいでしょう。接触時間やボタンを押している時間に左右されずに安定した衝撃力を与えることができます。

OBAQの回転角度

 qaddpoly命令やqpos命令にある通り、角度はラジアンでの取り扱いが基本です。

 ラジアンがわからない…? 度をラジアンに変換するにはdeg2radを使う。 ラジアンを度に変換するにはrad2degを使う。 これら関数を使うと出てくるヨクワカラン数字が「ラジアン」という理解で十分です。

 数学的意味?自分で調べてください。

OBAQの回転速度

 qspeedには、「回転速度パラメーター(実数)」としか書かれていません。 回転速度…角速度は一般的には[rad/s]とか[度/秒]とか[rpm]が使われます。 しかし、ここでの時間の単位は計算回数なので、[rad/計算1回]がqspeedで指定する回転速度の単位です。

 回転速度を与えるプログラムを書いてみました。

HSP3

;
;	角速度
;
; 回転速度を与えています。
; 回転速度の確認のために、qgetposで速度を実測しています。
;
#include "obaq.as"

; OBAQの設定
fps = 60
qreset					; OBAQの初期化
qgetreq rate, REQ_PHYSICS_RATE

qaddpoly my, 3, 80,60,0	; 三角形を追加
qinertia my, 1.0, 1.0	; 惰性なし
qtype my, type_bindX | type_bindY	; 位置を固定

; 回転速度を設定 [rad/計算]
qvaA = deg2rad(120.0 / (fps * rate))	; 120 度/秒
qspeed my,,, qvaA, 2

;	メインループ
*main
	redraw 0			; 画面の更新を開始
	color 0,0,0:boxf	; 画面をクリア
	qexec				; OBAQによるオブジェクトの更新

	; 現在角度から回転速度を算出
	; 1フレーム当たりの角度から1回の計算当たりの角度に変換
	qva0 = qva
	qgetpos my, qvx, qvy, qva
	qvaB = (qva - qva0) / rate
	
	qdraw				; オブジェクトの描画

	color 255,255,255
	pos 50, 50
	mes "角速度"		; 回転速度
	mes strf("qspeed  設定値:%f [度/計算]", rad2deg(qvaA))
	mes strf("qgetpos 測定値:%f [度/計算]", rad2deg(qvaB))
	mes strf("qgetpos 測定値:%f [度/秒]"  , rad2deg(qvaB * fps * rate))
	mes strf("qgetpos 測定値:%f [RPM]"    , rad2deg(qvaB * fps * rate * 60.0 / 360.0))

	redraw 1			; 画面の更新を終了
	await 1000/fps		; 一定時間待つ
	goto *main

 三角形が1秒間に1/3回転するように回転させています。 360度÷3を、1秒間あたりの経過時間fps * rateで割って、ラジアンに換算ですね。 deg2rad(120.0 / (fps * rate))

 あとはqgetposで実測して、設定値と測定値が一致していることを確認しています。

 qspeedは、設定オプションのデフォルト値が 0 になっているので注意が必要です。 任意の速度を設定したい場合は、 2 を指定してください。

衝突判定

 OBAQには標準で衝突判定を行う機能が装備されています。 衝突時の詳細な情報(衝突した座標や負荷さ)を取得したり、あえて衝突しないようにするといった事も可能です。 この設定は、オブジェクトごとにコリジョングループという値を割り当てて行います。グループ単位にすることで管理をしやすくし、処理の負荷の低減を行っているのだと思います。

 コリジョングループに関する設定は、qaddpoly命令などでオブジェクトを作成したときや、後からqgroupで設定することができます。 コリジョングループ値は、1,2,4,8,…のビット値を使用します。複数指定する場合は、足し算した値かorで連結する方法をとります。

 コリジョングループで設定できる内容について確認です。

mygroup : 自分が属するコリジョングループ値
オブジェクトごとに所属するコリジョングループ値を指定します。自機は1、敵は2、障害物は4という感じで、衝突判定処理を分けたいグループには別の値を設定します。
exgroup : 衝突を除外するグループ値
これを指定すると、一致するコリジョングループ値とは衝突しなくなります。 出来るだけ除外しておいた方が負荷は減らせるはずです。
loggroup : コリジョンログを作成するグループ値
qcollision命令での詳細な衝突判定結果を取得する際に使用します。 衝突時の詳細な情報が必要な場合にのみ指定するようにしてください。 必要ないなら設定しないほうが、負荷を減らせるはずです。

 衝突判定は個数が増えるほど負荷が大きくなります。 衝突を検出する必要がないものは、できるだけ除外するグループに設定したり、コリジョンログから除外したりした方が良いと思います。

 ここらで使い方の例を見てみます。

HSP3

;
;	衝突判定
;
; 以下の項目のテスト
; ・衝突判定
; ・コリジョングループ
; ・コリジョンログ
;
; 各オブジェクトのグループID
; ・三角形 1
; ・四角形 2
; ・五角形 4
#include "obaq.as"

screen 0,640,480	; ウィンドウ初期化

; ------------------------------
;	OBAQ
; ------------------------------
qreset			; OBAQの初期化
dim objID, 10


; ------------------------------
; マウスで動かすオブジェクト
; ------------------------------
; ・四角形(グループ2)を衝突から除外
;		→ 衝突しなくなる
; ・五角形(グループ4)をコリジョンログから除外
;		→ 五角形(4)は、qcollision命令で検出できない
; ・コリジョンログを設定することでqcollision、qgetcolに対応
qaddpoly objID(0), 3,  80, 60, 0, 10, 10
qtype  objID(0), type_bind
qgroup objID(0), 1, 2, 1|2


; ------------------------------
; 地面に並べたオブジェクト
; ------------------------------
; ・同じ形はコリジョングループを同じに設定
; ・大きい方は、マウスで動かすオブジェクトグループ(1)を衝突から除外
;		→ 大きい方は三角形(グループ1)と衝突しない
qaddpoly objID(1), 3,  30,90,0, 10,10,, 1,0	; 三角形
qaddpoly objID(2), 3,  50,90,0, 12,12,, 1,1
qaddpoly objID(3), 4,  70,90,0, 10,10,, 2,0	; 四角形
qaddpoly objID(4), 4,  90,90,0, 12,12,, 2,1
qaddpoly objID(5), 5, 110,90,0, 10,10,, 4,0	; 五角形
qaddpoly objID(6), 5, 140,90,0, 12,12,, 4,1


*main
	redraw 0			; 画面の更新を開始
	color 0,0,0:boxf	; 画面をクリア
	qexec				; OBAQによるオブジェクトの更新

	; マウス座標
	pxMx = mousex
	pxMy = mousey
    qcnvaxis qpxMx, qpxMy, pxMx, pxMy, 1

	; オブジェクトを操作
	qpos objID(0), qpxMx, qpxMy

	; 衝突結果を取得
	; ・事前にコリジョンログを設定していたオブジェクトIDだけが使用できる。
	; ・衝突した点についての情報をqgetcol、qgetcol2、qgetcol3命令で取得できる。
	qcollision objID(0), -1
	repeat
		qgetcol id, qcx, qcy
		if id < 0 : break
		qcnvaxis cx, cy, qcx, qcy, 0
		r = 5
		color 255,255,255
		circle cx-r, cy-r, cx+r, cy+r
		pos cx, cy-25
		mes "" + id	; オブジェクトID
	loop

	; ------------------------------
	;	描画
	; ------------------------------
	qdraw			; オブジェクトの描画

    color 255,255,255
    pos 40, 40
    mes "マウス ウィンドウ座標:" + strf("%6.2f, %6.2f", pxMx, pxMy)
    mes "マウス    OBAQ座標:" + strf("%6.2f, %6.2f", qpxMx, qpxMy)

	; オブジェクトのIDを描画
	qfind
	repeat
		qnext id
		if id < 0 : break
		qgetpos id, qx, qy, qa
		qcnvaxis x, y, qx, qy, 0
		pos x, y-16
		mes "" + id
	loop
	
	redraw 1		; 画面の更新を終了
	await 16		; 一定時間待つ
	goto *main

 コリジョングループで設定した内容が少し複雑なので表に整理。あとは触りながら確認してもらえればと思います。

オブジェクトID123456
図形三角形三角形四角形四角形五角形五角形
グループ値112244
衝突除外-1-1-1
自機の衝突除外--22--
自機のコリジョンログ--
自機との衝突××××

 衝突の除外は、どちらかが除外を指定していると衝突しなくなります。 コリジョンログは、今回は自機以外では必要ないので指定していません。

 qcollisionqgetcolの使い方が少し特殊な点に注意が必要かもしれません。 qcollisionで探す条件を指定して、qgetcolで条件に一致したIDを取り出すという流れになります。 条件に一致したIDは複数ある事がありますが、結果は1個ずつ取り出してくれます。 もう一度qgetcolを実行すると先ほど見つけたIDとは別の次の該当するIDを返します。 該当するIDがなくなったら -1 を返します。 ですので実際に使う際は次のような書き方が基本となります。

HSP3

qcollision objID
repeat
	qgetcol id, qx, qy
	if id < 0 : break
	; ここに衝突時の詳細情報を使用した処理を書く。
loop

OBAQのスプライト

 OBAQでは、celloadで読み込んだ画像をオブジェクトに張り付けることができます。 celloadで読み込んで、celdivでサイズ指定。qmatでオブジェクトに関連付けをするという使い方です。 qmatでタイミングよく画像を差し替えればアニメーションさせることも可能です。

 OBAQのスプライトを使う最低限のサンプルを書いてみたのでどうぞ。

HSP3

;
;	スプライト表示
;
#include "obaq.as"

;	スプライト画像を準備
celload "parts.png"	; 素材画像を読み込む
tex = stat
celdiv tex, 40,40, 20,20	; パーツのサイズと中心位置の設定

;	OBAQの初期化
qreset

; オブジェクト配置
; スプライト付き
qaddpoly objID, 4, 96,20,deg2rad(30)
qmat objID, mat_spr, tex, 6

; qmat objID, mat_spr2, tex, 6
; qmat2 objID,,, 2.0, 2.0

;	メインループ
*main
	redraw 0
	gradf ,,,,1,0,128
	color 0,0,0
	qexec			; OBAQによるオブジェクトの更新
	
	qdraw			; オブジェクトの描画
	redraw 1		; 画面の更新を終了
	await 16		; 一定時間待つ
	goto *main

 このようにお手軽に画像を貼れて非常に便利なのですが、一つだけ注意点があります。qmat命令の以下の説明です。

スプライトは、形状全体をカバーする形で矩形が貼り付けられます。

 そうです。多角形や任意の形状のオブジェクトになると画像がオブジェクト形状からはみ出してしまうんです。はみ出す部分を透明にした画像を準備すれば解決するのですが、テクスチャのように塗りつぶす物ではないので理解が必要です。 スプライトは元々、正方形の画像を張り付けるものです。つまりこれは当然の仕様です。中を画像で塗りつぶすテクスチャとは違うものなので十分理解して使いましょう。

 OBAQのスプライトは手軽な反面、描画性能があまりよくありません。 アニメーションの管理機能がないので自前で作成する必要があります。 オブジェクトとの関連付けが必須なので、オブジェクトと関係ないところでは別の手段での描画処理が必要です。このあたりの問題を理解したうえで利用すれば、とても手軽な描画機能です。

 アニメーション機能は自作するとすごく大変なので、 最近はHSP3Dishの標準スプライト(es_系命令)との組み合わせがいいのではないかと思っています。 アニメーションのほか高度な機能が盛りだくさんです。 OBAQのズーム倍率との連携部分を上手く作ればいいだけなので、十分検討に値すると思っています。

オブジェクトの頂点座標

 OBAQオブジェクトは形状によってはスプライトの利用が適さない場合があります。例えば長方形だと画像が伸びてしまいますし、多角形ならで形状を活かしたいなら四角い画像と合わせるのが手間です。 輪郭線を描画したい場合、オブジェクトの描画は自力で実装する必要があります。qdrawのデバッグモードのままというのはさすがに避けたいですしね。

 オブジェクトの輪郭を描画するには、頂点座標が必要です。 しかし、qaddmodelqaddpolyで作成したオブジェクトの頂点座標を取得する命令はありません。 qaddmodelは、作成時の座標データを保持しておけばいいのですが、qaddpolyでは同じ方法が使えません。 これは自作するしか選択肢はありません。

 ということで作りました。

HSP3

;
;	頂点の輪郭を取得
;
; 頂点に丸を描画
;
#include "obaq.as"

; ##############################################################################
#module

;	obaqオブジェクトの頂点を取得
; 
; qaddpolyで作成した図形の頂点座標を取得します。
;
; array v_x, v_y   : 頂点座標を受け取る配列変数
; int p_shape(3) : 頂点数
; double p_qposX(0.0) : 配置X座標(実数)
; double p_qposY(0.0) : 配置Y座標(実数)
; double p_ang(0.0)   : 配置角度(実数)(単位はラジアン)
; double p_qszX(10.0) : 配置Xサイズ(実数)
; double p_qszY(10.0) : 配置Yサイズ(実数)
;
#define global cnvQPoly2Vertex(%1, %2, %3=3, %4, %5, %6, %7=10.0, %8=10.0) cnvQPoly2Vertex_ %1, %2, %3, %4, %5, %6, %7, %8
#deffunc cnvQPoly2Vertex_ array v_x, array v_y, int p_shape, \
	double p_qposX, double p_qposY, double p_ang, \
	double p_qszX, double p_qszY

	; 引数チェック
	if p_shape < 3 : return 1
	
	ndiv = p_shape	; 頂点数
	dt = M_PI * 2.0 / ndiv
	startAngle = dt / 2.0
	t = startAngle
	x = double(p_qszX) * cos(t)	; 書き始めの頂点
	y = double(p_qszY) * sin(t)
	
	; オブジェクトの向きに合わせる
	cosr = cos(p_ang)
	sinr = sin(p_ang)
	repeat ndiv+1
		t = dt * cnt + startAngle
		x = double(p_qszX) * cos(t)	; 頂点
		y = double(p_qszY) * sin(t)
		; 現在の向きに回転
		xx = x * cosr - y * sinr
		yy = y * cosr + x * sinr
		v_x(cnt) = xx + p_qposX
		v_y(cnt) = yy + p_qposY
	loop

	return

#global
; ##############################################################################


screen 0,640,480	; ウィンドウ初期化

; cnvQPoly2Vertexの結果取得用配列
ddim xx, 10
ddim yy, 10

; OBAQの初期化
qreset

; 図形配置
shape = 5			; 頂点数
sx = 30 : sy = 20	; サイズ
qaddpoly my, shape, 80,60,deg2rad(30), sx, sy
; 図形形状を保存
; 頂点数, Xサイズ, Yサイズ
quser my, shape, sx, sy

	
*main
	;	メインループ
	;
	redraw 0		; 画面の更新を開始
	color 0,0,0:boxf	; 画面をクリア
	qexec			; OBAQによるオブジェクトの更新
	qdraw			; オブジェクトの描画

	; 図形の位置形状を取得
	qgetpos my, qx, qy, ang
	qgetuser my, vertex, qrx, qry
	; 位置と形状から図形の頂点座標を取得
	cnvQPoly2Vertex xx, yy, vertex, qx, qy, ang, qrx, qry

	; 頂点を描画
	color 255
	r = 10
	repeat vertex
		qcnvaxis x, y, xx(cnt), yy(cnt), 0
		circle x + r, y + r, x - r, y - r, 0
	loop

	redraw 1		; 画面の更新を終了
	await 16		; 一定時間待つ
	goto *main

 qaddpolyは、配置サイズで指定した矩形に収まる楕円上に頂点を置きます。 そこさえ押さえておけば作る事ができます。

 それにしても、頂点情報はOBAQ内部では持ってるはずなので、取得できるようにしてほしいですね。

おわりに

 以上、最近OBAQを使っていて得られた情報をまとめてみました。 まとまってない気もしますが、私の備忘録なので気にしない。

関連記事

  1. スパイダーアドベンチャー 前の画像 次の画像 珠音のスパイダーアドベンチャー Ver.1.02 (2025/11/05) ダウ...