はじめに

 せっかくの物理設定、ステージ上に配置したオブジェクトだけではなくプレイヤーキャラクターにも適用したい。 そう考えて作っていたのですが色々と問題が…。 というわけで、解決策を考えたのでまとめ。

カプセル コライダー

 hgimg4は、物理設定を行うだけで質量や力、衝突判定など物理的にリアルな動きをさせることができます。 人形3Dモデルなどのように複雑な形状モデルでも、gppbind命令でGPPBIND_MESHを指定すれば形状にあった衝突判定を行ってくれます。

 しかし、計算負荷の低減の観点からポリゴン数が多すぎるモデルにはGPPBIND_MESHを適用したくはありません。 また、複雑な形状での衝突判定結果は、ゲームをする際には動きが複雑になりすぎる問題もあります。

 Unityなどでは、物理計算にキャラクターモデルを直接使用するのではなく、別途シンプルな形状の衝突判定用オブジェクト(コライダー)を使用したりするようです。 物理設定をしたコライダー(ノードオブジェクト)と同じ座標に、物理設定していないキャラクターモデルを置いているだけというイメージです。

 コライダーでよく見かけるのがCapsule Collider(薬のカプセル剤のような形状の衝突判定用オブジェクト)です。 縦に長いので、人形キャラクターとよく重なります。真中付近は円柱状なので、壁にぶつかった際、想像通りのわかりやすい跳ね返り方をします。 また、上端・下端・側面が丸い形状なので、ちょっとした段差や突起程度ならぶつかっても上手に滑ってかわしてくれるので、引っかかるということがありません。 カプセルは、球体や箱型よりも非常に便利な形状をしています。

 hgimg4でも、カプセル形状モデルを使って同じような実装を行うことができます。 hgimg4標準にはカプセルモデルを作る機能はないので、Blender等でカプセル形状モデルを作成してHSP3で読み込んで使用します。 物理設定時にgppbind命令でGPPBIND_MESHを指定します。 カプセルに力を入力して動かします。あとはカプセルからgetposした位置に、キャラクターモデルをsetposで移動するだけです。 お手軽にカプセル コライダーを再現できます。

カプセルコライダーを再現実装

 Blenderをぐりぐりしてカプセル形状のモデルを作成しました。 作り方はこちら。すぐに情報が手に入る便利な世の中。動画を上げてくれた方に感謝。

【Blender2.9】カプセル錠剤を作ろう【初心者向けチュートリアル】
https://www.youtube.com/watch?v=Mp_8XHBUTpg

 早速カプセルを使ったサンプルを作ってみました。ファイルが必要なので、カプセルの3Dモデルを下記リンクからダウンロードしたらresフォルダに入れてください。
カプセルモデル ダウンロード


#include "hgimg4.as"
;
;	カーソルキーで箱を動かすことができます。
;
gpreset
setcls CLSMODE_SOLID, $8080F0

;	カメラ位置を設定
setpos GPOBJ_CAMERA, 0,5,10

;	自機オブジェクト(物理)
if 0 {
	; 1 カプセル
	gpload id_myobj,"res/capsule"		; モデル読み込み
	if id_myobj < 0 {
		dialog "3Dモデルの読み込みに失敗しました。"
		end
	}
} else {
	; 0 箱
	gptexmat id_texmat, "res/qbox.png"
	gpbox   id_myobj, 0.5, -1, id_texmat
}
setpos  id_myobj, 0, 0.5, 0
gppbind id_myobj, 1, 0.5, GPPBIND_MESH
gppset  id_myobj, GPPSET_ANGULAR_FACTOR, 0,0,0
gppset  id_myobj, GPPSET_ANISOTROPIC_FRICTION,  1, 1, 0	; Z方向の摩擦小

;	床ノード(物理)
gpfloor id_floor, 5, 5, $404040
gppbind id_floor, 0


*main
	stick key,15
	if key&128 : end

	;	カーソルキーで箱を動かす
	if key&1 : gppapply id_myobj, GPPAPPLY_IMPULSE, -0.1, 0, 0
	if key&4 : gppapply id_myobj, GPPAPPLY_IMPULSE, 0.1, 0, 0
	if key&8 : gppapply id_myobj, GPPAPPLY_IMPULSE, 0,0, 0.1
	if key&2 : gppapply id_myobj, GPPAPPLY_IMPULSE, 0,0, -0.1

	;	位置リセット
	; 床より下に落下した場合、初期位置に戻す。
	; 箱の場合は有効ですが、gploadで読み込んだモデルでは正しく動きません。
	getpos id_myobj, x,y,z
	if y < -5 {
		gppset id_myobj, GPPSET_ENABLE, 0
		setpos id_myobj, 0, 3, 0
		gppset id_myobj, GPPSET_LINEAR_VELOCITY ,0,0,0
		gppset id_myobj, GPPSET_ANGULAR_VELOCITY,0,0,0
		gppset id_myobj, GPPSET_ENABLE, 1
	}

	;	カメラ
	getpos id_myobj, dx,dy,dz
	gplookat GPOBJ_CAMERA, dx,dy,dz

	redraw 0
	gpdraw

	color 255,255,255
	pos 8,8:mes "HGIMG4 sample"

	getpos id_myobj, x,y,z
	mes strf("位置:%g, %g, %g", x,y,z)

	redraw 1
	await 1000/60
	goto *main

 そのままの実行すると箱を動かせます。床から落下すると、初期位置に戻ってくる仕組みです。 「if 0 {」を「if 1 {」に書き換えると、カプセルを使ったサンプルになります。とても簡単でしたね!

 地面や壁の凹凸もスムーズに躱せます。この問題はこれで解決!


 …そんなふうに考えていた時期が私にもありました。

 カプセルの場合、床から落下しても元の位置に戻ってくれません。 どうやらsetposが機能していないようです。 HSP3.7β3現在、gploadで読み込んだモデルに物理挙動設定を行った後は、GPPSET_ENABLEしてもsetposが効かない不具合があるようです。

 つまりお手正カプセル コライダーには、setposが使えません。 setposを使わないように作れば済む話ですが、どうしても必要な場合もあるのでちょっと困ります。 ここでは、この対策として考えた方法について説明していきます。

 hgimg4で手軽に作れる立体は、gpboxで作成する箱のみです。この箱を使う以外の選択肢はありません。箱でなんとかしてみることにしました。

 箱に物理設定して動かしてみると、小さな段差に躓いたり、ちょっとした高低差を登ることができないことに気が付くとおもいます。 底が平で側面は垂直、角は地面にピッタリついている状態なのでさもありなんです。 また、地面との摩擦の影響で回転した際に、重心位置が回転中心にならないこともあります。 地面と面で接触しているので、地面へのめり込み分布の中心が箱の真ん中とは限らない点も問題です。

 そこで、箱の向きを傾けることにしました。

斜めに立てた箱

 このようにすると小さな段差やちょっとした高低差程度は、下側の斜めの部分にぶつかって箱は上に押し上げられます。 後は勢いがあればそのまま段差を超えることができます。

 また地面との接点も1箇所だけなので、接地しているときのY軸周りの回転は、接地している1点を中心に回転することが保証されます。 地面へのめり込みも、この1点を中心に発生します。 ただ置くだけよりもかなり問題が改善されそうです。

箱を斜めに立てる

 まずは箱を斜めに立てる必要があります。そのためにはどの軸方向に、何度回転させればいいかを調べる必要があります。 今回は箱をこのような向きに回転させます。

斜めに立てた箱は正面から見ると左右対称

 赤・緑・青の線が、回転前のX・Y・Zの線です。つまり箱のローカル座標系です。 正面(Z軸側)からだけではよくわからないので、横(X軸側)からも見てみます。

斜めに立てた箱は横から見ると左右非対称

 地面側の頂点の真上に上側の頂点が来るように回転します。 正面から見た場合は、左右対称だったので45度回転すれば良さそうでした。 しかし横から見ると45度回転ではダメそう。角度を計算する必要があります。

 箱の1辺を長さ1と仮定した場合、2頂点の距離…つまり箱を斜めに立てたときの高さは √3 です。 寸法と角度を図にしてみました。

箱の各場所の長さと傾き角は関係している。

 cosθ = √3 / √2 なので…、 どうやらacos(sqrt(2/3))ラジアン回転すればこの向きになりそうです。 やってみましょう。


#include "hgimg4.as"
gpreset
setcls CLSMODE_SOLID, $8080F0

; カメラ位置を設定
setpos GPOBJ_CAMERA, 0,0.5,2

; 箱ノード(物理)
;acos(sqrt(2/3)) = 0.6154797087
gptexmat id_texmat, "res/qbox.png"
gpbox   id_box, 0.5, -1, id_texmat
setpos  id_box,	0, 0.5, 0
setang  id_box, -0.6154797087, 0, deg2rad(45)
gppbind id_box, 1, 0.5
gppset  id_box, GPPSET_ANGULAR_FACTOR, 0, 1, 0	; 回転制約

; 床ノード(物理)
gpfloor id_floor, 30,30, $404040
gppbind id_floor, 0


*main
	stick key,15
	if key&128 : end
	;gppapply id_box, GPPAPPLY_TORQUE, 0.0, 0.1, 0.0
	redraw 0
	gpdraw
	redraw 1
	await 1000/60
	goto *main

 Y軸周りの回転以外の回転トルクを受けると、箱は倒れてしまうのでGPPSET_ANGULAR_FACTORで「0, 1, 0」としています。

 想定通りの向きになったことを確認するため、ちょっと回転させてみます。


  gppapply id_box, GPPAPPLY_TORQUE, 0.0, 0.1, 0.0

 回転しても上の頂点位置が動きません。想定通りの向きにできたようです。

摩擦方向

 斜めに立てた箱に対して「異方性の摩擦(方向によって異なる大きさの摩擦)」を設定するとどのような動きになるのでしょうか。 希望としては、前後方向(Z方向)の摩擦はほぼゼロ、左右方向(X方向)には摩擦大になってくれると動かしやすいキャラクターになります。すなわち自動車みたいなイメージです。

 先に結果を言ってしまうと、「異方性の摩擦」は、ノードオブジェクトのローカル座標…つまり、XYZ方向にゼロ度回転を設定した状態の向きに対して設定されます。 箱ノードは回転した状態で使うので、摩擦方向も一緒に回転してしまっています。 これは困った。摩擦の向きってどうなってるんだ…。

 こういうときは試してみるほうが早いです。


#include "hgimg4.as"
title "HGIMG4 Test"
;
;	カーソルキーで箱を動かすことができます
;
gpreset
setcls CLSMODE_SOLID, $8080F0

; カメラ位置を設定
setpos GPOBJ_CAMERA, 0,5,10

; 箱ノード(物理)
gptexmat id_texmat, "res/qbox.png"
gpbox   id_box, 0.5, -1, id_texmat
setpos  id_box,	0, 0.5, 0
gppbind id_box, 1, 0.5
gppset  id_box, GPPSET_ANGULAR_FACTOR, 0,0,0

; 床ノード(物理)
gpfloor id_floor, 30,30, $404040
gppbind id_floor, 0

;	箱ノードクローン(物理)
dim id_boxc, 5
dz = 2.0
repeat 5
	gpclone id_boxc(cnt), id_box			; 最初の箱をクローン
	dx = 1.*cnt-2
	;acos(sqrt(2/3)) = 0.6154797087
	setang  id_boxc(cnt), -0.6154797087, 0, deg2rad(45)
	setpos  id_boxc(cnt), dx, sqrt(3)/2, dz			; 落とす位置を微調整
	gppbind id_boxc(cnt), 1, 0.5					; 箱の物理設定を行なう
	gppset  id_boxc(cnt), GPPSET_ANGULAR_FACTOR, 0,0,0
	gppset  id_boxc(cnt), GPPSET_FRICTION, 1.5
	;gppset  id_boxc(cnt), GPPSET_FRICTION, 10	; 摩擦を大きくする
loop

;	摩擦に重み付け
; 左から順に指定しています。
gppset id_boxc(0), GPPSET_ANISOTROPIC_FRICTION,  1, 1, 1	; 変化なし
gppset id_boxc(1), GPPSET_ANISOTROPIC_FRICTION,  1, 1, 0	; Z方向の摩擦小
gppset id_boxc(2), GPPSET_ANISOTROPIC_FRICTION,  1, 1, -0.5	; Z方向の摩擦なし
gppset id_boxc(3), GPPSET_ANISOTROPIC_FRICTION,  0, 0, 0	; 摩擦なし

;	初速を与える
;摩擦の程度を確認します。
;比較のため、一番右の箱は動かしません。
repeat 4
	gppapply id_boxc(cnt), GPPAPPLY_IMPULSE, 0, 0, 1
loop


*main
	stick key,15
	if key&128 : end

	;	カーソルキーで箱を動かす
	if key&1 : gppapply id_box,GPPAPPLY_IMPULSE, -0.1, 0, 0
	if key&4 : gppapply id_box,GPPAPPLY_IMPULSE, 0.1, 0, 0
	if key&8 : gppapply id_box,GPPAPPLY_IMPULSE, 0,0, 0.1
	if key&2 : gppapply id_box,GPPAPPLY_IMPULSE, 0,0, -0.1

	;	スペースキーで一斉に同じ力で押す
	if key&16 {
		repeat 4
			gppapply id_boxc(cnt), GPPAPPLY_IMPULSE, 0, 0, 1
		loop
	}

	;	カメラ
	getpos id_box,dx,dy,dz
	gplookat GPOBJ_CAMERA, dx,dy,dz

	redraw 0
	gpdraw

	color 255,255,255
	pos 8,8:mes "HGIMG4 sample"

	redraw 1
	await 1000/60
	goto *main

 実行直後に1回だけ +Z方向に力が与えられます。 各ノードの摩擦の大きさを観察してください。 その後は、自機を移動して体当りさせて動きを確認してください。

 箱のローカルXYZの向きは、この様になっています。

サンプルの箱にローカル座標系を書き込んだ

X:赤 Y:緑 Z:青

「異方性の摩擦」については、左から順に以下のように設定しています。


  ;	摩擦に重み付け
  ; 左から順に指定しています。
  gppset id_boxc(0), GPPSET_ANISOTROPIC_FRICTION,  1, 1, 1	; 変化なし
  gppset id_boxc(1), GPPSET_ANISOTROPIC_FRICTION,  1, 1, 0	; Z方向の摩擦小
  gppset id_boxc(2), GPPSET_ANISOTROPIC_FRICTION,  1, 1, -0.5	; Z方向の摩擦なし
  gppset id_boxc(3), GPPSET_ANISOTROPIC_FRICTION,  0, 0, 0	; 摩擦なし
  • 1つ目:比較用に摩擦設定変更なし。
  • 2つ目:Z方向の摩擦をゼロに設定。
  • 3つ目:Z方向の摩擦を -0.5に設定。
  • 4つ目:比較用に全方向摩擦なし。
  • 5つ目:比較用に摩擦設定変更なし+移動なし。

 「Z方向の摩擦をゼロ」は思ったより摩擦が残っていますね。 一方、「Z方向の摩擦を -0.5」と「全方向摩擦なし」の結果が同じです。

 斜めに立てた箱をZ方向に移動する場合、箱のローカルZ方向以外にローカルXY方向にも摩擦が発生しています。 斜めに立てた箱を真上から見ると、箱のローカルX,Y,Z軸が水平方向に伸びているのが見えることからもわかります。 このYZ方向の摩擦を打ち消すためには、「Z方向の摩擦を -0.5に設定」を設定するとちょうど打ち消せます。(計算上も正しい。)

 ではZは「-0.5」が一番いい設定かというとそうでもありません。 設定は、上下方向の摩擦にも影響するはずです。 また実装して使ってみると、傾斜がついた地面で使用した場合に思いもしない方向に滑ったり、水平面でもなにもないところで小さく跳ねたりといった現象が発生します。 Zは「0.0」を最小値として利用したほうが良さそうです。

段差テスト

 最初の方の説明で、箱を斜めに立てると段差を超えやすくなるに違いあるまい!と書きました。 箱を斜めに立てることができるようになったので、実際に段差を超えやすくなったことを確認してみます。

 まずは、箱を斜めにせずそのまま使用した場合。段差に引っかかることを確認します。


#include "hgimg4.as"
title "HGIMG4 Test"
;
;	カーソルキーで箱を動かすことができます
;
gpreset
setcls CLSMODE_SOLID, $8080F0

;	カメラ位置を設定
setpos GPOBJ_CAMERA, 0,2,5

;	箱ノード(物理)
gptexmat id_texmat, "res/qbox.png"
gpbox   id_box, 0.5, -1, id_texmat

; 斜めに立たせる場合
;setang  id_box, -0.6154797087, 0, deg2rad(45)
;setpos  id_box, 0, sqrt(3)/2, 0
;gppbind id_box, 1, 0.5
;gppset  id_box, GPPSET_ANGULAR_FACTOR, 0,0,0	; Y軸回転も停止
;;gppset id_box, GPPSET_ANISOTROPIC_FRICTION,  1, 1, 0	; Z方向の摩擦0

; 普通に置くだけの場合
setpos  id_box, 0, 0.5, 0
gppbind id_box, 1, 0.5

;	床ノード(物理)
gpfloor id_floor, 30,30, $404040
gppbind id_floor, 0

;	箱ノードクローン(物理)
; 障害物として設置
dim id_boxc, 10
y = -0.25
repeat 10
	gpclone id_boxc(cnt), id_box
	x = 1.*cnt-5
	setang  id_boxc(cnt), 0, 0, 0
	setpos  id_boxc(cnt), x, y, 2
	gppbind id_boxc(cnt), 0
	y = y + 0.04
loop


*main
	stick key,15
	if key&128 : end

	;	カーソルキーで箱を動かす
	if key&1 : gppapply id_box, GPPAPPLY_IMPULSE, -0.1, 0, 0
	if key&4 : gppapply id_box, GPPAPPLY_IMPULSE, 0.1, 0, 0
	if key&8 : gppapply id_box, GPPAPPLY_IMPULSE, 0,0, 0.1
	if key&2 : gppapply id_box, GPPAPPLY_IMPULSE, 0,0, -0.1

	;	カメラ
	getpos id_box,dx,dy,dz
	gplookat GPOBJ_CAMERA, dx,dy,dz

	redraw 0
	gpdraw

	color 255,255,255
	pos 8,8:mes "HGIMG4 sample"

	redraw 1
	await 1000/60
	goto *main

 左から高さゼロ。右に行くと0.04ずつ段差が高くなります。 高さ0.5の箱は、そのままだと勢いをつけても0.2の段差を超えられませんね。その他の低い段差もある程度速度をあげないと超えられません。 またもしX,Z軸まわりの回転を止めて使用した場合、一番低い段差でも超えることができません。

 自機の箱を斜めに立たせてみます。


  ; 斜めに立たせる場合
  setang  id_box, -0.6154797087, 0, deg2rad(45)
  setpos  id_box, 0, sqrt(3)/2, 0
  gppbind id_box, 1, 0.5
  gppset  id_box, GPPSET_ANGULAR_FACTOR, 0,0,0

 高さ0.32でも難なく超えることができました。 これで段差を超えやすいことが確認できました。

まとめ

 プレイヤーキャラクターに物理設定を行いたい場合は、gpboxで作った箱を本体にする。


  setang  id_box, -0.6154797087, 0, deg2rad(45)

 このように回転して、gppbindで物理設定。


  gppset  id_box, GPPSET_ANGULAR_FACTOR, 0,1,0

 でY軸以外の回転を止めることで転倒防止。さらに


  gppset id_box, GPPSET_ANISOTROPIC_FRICTION,  1, 1, 0

 とすれば前後(Z軸方向)の摩擦を比較的小さく、左右(X軸方向の)の摩擦を比較的大きくできる。

 しかしsetposが使えない不具合が解消されれば、このような対策は不要になります。 この対策では不便な部分もあるので、今後の修正に期待です。 修正されても、この記事では異方性摩擦に関する知見が得られたので無駄にはならないのです。