接地判定

目次

はじめに

 人間は地面に接地していないと、足でジャンプできません。 物理設定を行ったプレヤーキャラクター(PC)も同様に、ジャンプは地面にいるときだけにしたい、と考えるかもしれません。 また、アイテム取得やイベントなどのために、「床を踏んでいる」事を検出したい事もあるでしょう。 床に接地している場合と、空中にいいる場合で、モーションを変えたい事があるかもしれませんね。

 ということで、今回はキャラクターが「地面に接地しているか」(接地判定)を調べてみたいと思います。 Unityの「isGrounded」のようなものなのですが、hgimg4でもやはり精度がよくないので少し工夫しています。

上下速度による接地判定

 まず手軽な実装として、上下(Y軸)方向の速度を監視し、変化がなければ地面に接地しているという方法が思いつきます。 速度は検出できないので、getposの座標変化を見ていれば良さそうです。

 しかし検出できない条件があります。 ひとつは傾斜がついた地面を登ったり下ったりしている場合です。接地しながら上下移動するため、この方法では正しく検出できません。 またジャンプした際、一番高いところにいる瞬間も上下の速度変化がありません。

 このような条件をクリアできる場合であれば、「上下速度が十分小さければ接地している」と判定する方式を採用してもいいと思います。 しかし、これだと物理設定を活かしたものを作るのは難しそうですね。

衝突情報による接地判定

 「接地=地面オブジェクトとの衝突」と考えることができます。 衝突した座標がキャラクターの足元だったら接地している。と判定すれば良さそうです。 Unityの「isGrounded」の検出はこの方式っぽいです。(ちゃんとは調べてないので確証はない。)

 gppcontact命令を使用すると、ノードオブジェクト同士の詳細な衝突情報を取得できるようになります。 衝突情報を得るためには条件を満たす必要があります。

  • 衝突する両方のノードに対して、物理設定が必要(gppbind命令)
  • コリジョングループの適切な設定が必要(setcoli命令)

 衝突が検出されたら、gppinfoで衝突した座標を取得できます。 簡単そうですね。早速サンプルを作ってみてみます。

 まずgppcontactで衝突を調べます。衝突していた場合は、gppinfoで衝突座標(接触している点)を調べて、箱の中心よりも下なら地面と接地していると判定しています。


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

;	箱ノード
gptexmat id_texmat, "res/qbox.png"
gpbox   id_box, 1, -1, id_texmat
setpos  id_box,	0, 2, 0
gppbind id_box, 1, 0.5

;	床ノード
;gpfloor id_floor, 30,30, $404040
gpclone  id_floor, id_box
setscale id_floor, 30,  0.10, 30
setpos   id_floor,	0, -0.05, 0
gppbind  id_floor, 0

;	コリジョングループを設定
; 何もしなければ、グループ値は 0
setcoli id_box,   2, 1
setcoli id_floor, 1, 0


*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: gppapply id_box, GPPAPPLY_IMPULSE,  0  ,10,  0	; スペース ジャンプ
	if key&32: gppapply id_box, GPPAPPLY_IMPULSE,  0  ,-0.1,  0	; Enter 下に打ち付ける


	; 地面設置変数を初期化
	isGrounded = 0

	; ------------------------------
	;	衝突情報による地面検出
	; ------------------------------
	gppcontact n, id_box
	if n > 0 {
		; 接触中にしかログが表示されないようにすると
		; 一瞬で消えてしまうので、最後に接触した記録を表示するようにしています。
		log_coli = " 衝突情報:あり●\n"
		log_coli+= " 接触個数:" + n + "\n"
		
		; 接触座標が、自機の足元だった場合は衝突と判定する。
		m = "○接地:なし\n"
		repeat n
			gppinfo fv, id, id_box, cnt
			if fv(1) < py {
				m = "●接地:あり\n"
				isGrounded = 1
				break
			}
			; log_coli += "ID = " + id + " / " + strf(" ( %5.3f, %5.3f, %5.3f )", fv(0), fv(1), fv(2)) + "\n"
		loop
		log_coli = m + log_coli
		log_coli+= "\n"
	} else {
		log_coli = "○接地:なし\n"
		log_coli+= " 衝突情報:なし○\n"
		log_coli+= " 接触個数:0\n"
	}


	;	カメラ操作
	getpos id_box,dx,dy,dz
	gplookat GPOBJ_CAMERA, dx,dy,dz		; カメラから指定した座標を見る

	redraw 0			; 描画開始
	gpdraw				; シーンの描画

	;	接地判定の結果出力
	color
	gmode 3,,,100
	gradf 4,4, 420, 250, 0;, $ff00ff, $ffffff
	gmode 0

	color 255,255,255
	pos 8,8:mes "HGIMG4 sample"
	mes "---------- gppcontact 衝突検出 ----------"
	mes log_coli
	mes "---------- 総合判定結果 ----------"
	if isGrounded {
		mes "●接地:あり"
	} else {
		mes "○接地:なし"
	}

	redraw 1			; 描画終了
	await 1000/60			; 待ち時間

	goto *main

 接地を検出してくれていますね。 ジャンプで空中にいる間は、ちゃんと「接地していない」と判定してくれました。 しかし、操作せずにしばらく放置すると接地を検出してくれなくなりました。 少しでも動くと、再び接地が検出されます。

 しばらく放置すると接地を検出しなくなる問題は、gppcontact命令がノードオブジェクト同士の「めり込み」がある場合にだけ衝突を検出する仕様であることに起因しています。 ジャンプや移動時に発生する小さなめり込みは、操作せずにしばらく放置していると「めり込み量」が小さくなっていき、やがてゼロ(単に接しているだけ)の状態になります。 単に接しているだけの状態では、gppcontact命令は衝突を検出しません。 箱ノードの質量を大きくしても、この問題は解決できません。

 何とかする方法を考えてみました。 まず接地状況と座標を監視します。接地が検出されず、場所も移動していないという状態が暫く続いた場合、キャラクターに上または下方向の小さい力を加えてみて「意図的にめり込みを作る動き」をさせます。一度めり込めば、数秒は接地を検出できる状態になります。 これで何とかなりはしたものの、操作しないでいると突然ピクッと動く事になるのであまり気が進みません。出来れば避けたいところです。 後半に実装例があります。

レイによる接地判定

 丸影を作るときに使った方法を応用します。 つまりキャラクターから地面方向にレイを飛ばして、地面までの距離を調べる方法です。 Unityの「isGrounded」の精度が悪いという問題を回避する場合にも、レイを地面に飛ばす方法が用いられることもあるようです。どうやら一般的な解決方法のひとつのようですね。

 次の要領で検出するサンプルを作成しました。

  1. レイの起点となるヌルノードを準備し、箱ノードの位置にセットします。
  2. gppraytestで下方向を調べます。
  3. 検出結果が、箱ノード自身(無視したいノード)だった場合は検出をやり直します。
    基点を箱ノードの内側にしていますが、箱ノードも検出してしまうようです。
  4. 検出をやり直す場合は、前回検出位置から少しだけ離れた場所からやり直します。
    検出位置からだと、まったく同じ位置で再検出してしまいます。
  5. 検出が成功したら距離(地面からの高さ)を調べて、十分低い距離なら接地していると判定します。

  #include "hgimg4.as"
  title "HGIMG4 Test"
  ;	カーソルキーで箱を動かすことができます
  ;
  gpreset
  setcls CLSMODE_SOLID, $8080FF
  setpos GPOBJ_CAMERA, 0,2,10
  
  ;	箱ノード
  gptexmat id_texmat, "res/qbox.png"
  gpbox   id_box, 1, -1, id_texmat
  setpos  id_box,	0, 2, 0
  gppbind id_box, 1, 0.5
  
  ;	作業用ヌルノード
  gpnull id_null
  setang id_null, -M_PI/2.0, 0, 0
  
  
  ;	床ノード
  ;gpfloor id_floor, 30,30, $404040
  gpclone  id_floor, id_box
  setscale id_floor, 30,  0.10, 30
  setpos   id_floor,	0, -0.05, 0
  gppbind  id_floor, 0
  
  
  ;	箱で床を作成
  ; 隙間を作成するために設置
  x = 2.10
  gpclone  id_boxc, id_box
  setscale id_boxc, 4, 1, 4
  setpos   id_boxc, x, 0, 0
  gppbind  id_boxc, 0
  setcoli  id_boxc,  1, 0
  
  gpclone  id_boxc, id_box
  setscale id_boxc,  4, 1, 4
  setpos   id_boxc, -x, 0, 0
  gppbind  id_boxc,  0
  setcoli  id_boxc,  1, 0
  
  ;	コリジョングループを設定
  ; 何もしなければ、グループ値は 0
  setcoli id_box,   2, 1
  setcoli id_floor, 1, 0
  
  
  *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: gppapply id_box, GPPAPPLY_IMPULSE,  0  ,10,  0	; スペース
  
  
    ; 地面設置変数を初期化
    isGrounded = 0
  
    ; ------------------------------
    ;	レイによる地面検出
    ; ------------------------------
    getpos id_box,  px, py, pz
    setpos id_null, px, py, pz		; 自機の中心
    ; setpos id_null, px, py-0.6, pz	; 自機の下  台の中に入ってしまう。
    ; setpos id_null, px, py-0.5, pz	; 自機の底面 ちょうどいいみたい。
    ; 自分自身など検出対象が出たら、次にぶつかるまで探す。
    repeat 5
      ; レイの衝突を検出
      gppraytest objid, id_null, 4
      if objid <= 0 : break
      ; コリジョングループを使って、自機を除外する
      getobjcoli cg, objid, 0
      if cg ! 2 : break
      ; 次の検索開始位置
      ; 少し下から再検索
      getwork id_null, x, y, z
      ; setpos  id_null, x, y, z	; 検出結果がちらつく
      setpos  id_null, x, y-0.001, z
    loop
    ; 検出結果が自機なので、検出しなかったことにする。
    if cg = 2 : objid = 0
    
    ;	検出結果を使用
    ; 高さなど各種情報を取得してみます。
    if objid > 0 {
      getobjcoli cg, objid, 0
      ; 地面座標
      getwork id_null, x, y, z
      ; 地面からの高さ
      posH_Ground = py - y
      ; ログ
      log_ray = "     地面検出:あり●\n"
      log_ray+= "  地面からの高さ:" + posH_Ground + "\n"
      log_ray+= "  レイの衝突座標:" + strf(" ( %5.3f, %5.3f, %5.3f )", x, y, z) + "\n"
      log_ray+= " 相手のグループ値:" + cg
  
      ; 一定の高さ以下なら接地
      if posH_Ground <= 0.6 {
        isGrounded = 1
        log_ray = "●接地:あり\n" + log_ray
      } else {
        log_ray = "○接地:なし\n" + log_ray
      }
    }
    if objid <= 0 {
      log_ray = "○接地:なし\n"
      log_ray+= "     地面検出:なし○\n"
      log_ray+= "  地面からの高さ:\n"
      log_ray+= "  レイの衝突座標:\n"
      log_ray+= " 相手のグループ値:"
    }
  
  
    ;	カメラ操作
    getpos id_box,dx,dy,dz
    gplookat GPOBJ_CAMERA, dx,dy,dz		; カメラから指定した座標を見る
  
    redraw 0			; 描画開始
    gpdraw				; シーンの描画
  
    ;	接地判定の結果出力
    color
    gmode 3,,,100
    gradf 4,4, 420, 250, 0;, $ff00ff, $ffffff
    gmode 0
  
    color 255,255,255
    pos 8,8:mes "HGIMG4 sample"
    mes "---------- gppraytest レイ    ----------"
    mes log_ray
    mes "---------- 総合判定結果 ----------"
    if isGrounded {
      mes "●接地:あり"
    } else {
      mes "○接地:なし"
    }
  
    redraw 1			; 描画終了
    await 1000/60			; 待ち時間
  
    goto *main

 実行してみると、接地を検出しません。少し横に移動してみると接地を検出するようになります。 台と台の間の隙間に、箱の中心が来ると接地を検出しません。

 検出されない理由はこうです。レイ(直線の線分)は、箱の中心から地面に向けて飛ばしています。 サンプルの最初の位置では、レイが台と台の間の谷間を通って一番下の地面に衝突しています。このため箱の下の地面は、左右の台ではなく一番下の地面の方を検出する結果となっています。 一番下の地面から箱の中心までの距離は、1.0あり高すぎるので接地していないと判定されます。

 ならレイの数を増やせばいいんじゃないか?!箱の四つ角に1本ずつ追加だ!!
→ 箱の底面の辺の部分だけで台に乗ってしまうと検出しません。
レイを何本増やしても判定領域は点なので、どうしても検出できない状況に遭遇しえます。

 gppraytestをしているrepeat 5~loopの処理について補足。

 レイの衝突判定後に、コリジョングループを使って地面か地面以外かを判定しています。 地面以外(自機)が検出されたら、0.001下から再度検索を再開しています。 最初からレイの検出対象をコリジョングループなどで制限をかけることができたらいいのですが、今のところそういう機能はないのでこういう実装をしています。 0.001下というのも検出漏れが出そうで嫌なので無くしたいのですが、この値より小さくすると自機が再度検出されてしまいます。 値の根拠は今回の条件での調整結果です。状況が変われば最適値が変わる可能性があるので要注意です。

 またループ回数も5としていますが、除外したいノードは1つだけなので2でもいいはずです。 しかし2にすると接地を検出しない事があります。今回の例では、3以上にすると動作が安定しました。 サンプルでは、念のため5としています。この値も状況に応じで調整が必要です。

総合判定

 どちらの方法も一長一短があり、困ったものです。仕方がないので両方採用します。 gppcontactgppraytestのどちらかで接地を検出したら接地していると判定するようにすれば精度が向上するはずです。

 ついでに、めり込みがゼロを防止する処理も追加しておきます。


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

;	箱ノード
gptexmat id_texmat, "res/qbox.png"
gpbox   id_box, 1, -1, id_texmat
setpos  id_box,	0, 2, 0
gppbind id_box, 1, 0.5

;	作業用ヌルノード
gpnull id_null
setang id_null, -M_PI/2.0, 0, 0


;	床ノード
;gpfloor id_floor, 30,30, $404040
gpclone  id_floor, id_box
setscale id_floor, 30,  0.10, 30
setpos   id_floor,	0, -0.05, 0
gppbind  id_floor, 0


;	箱で床を作成
; 隙間を作成するために設置
x = 2.10
gpclone  id_boxc, id_box
setscale id_boxc, 4, 1, 4
setpos   id_boxc, x, 0, 0
gppbind  id_boxc, 0
setcoli  id_boxc,  1, 0

gpclone  id_boxc, id_box
setscale id_boxc,  4, 1, 4
setpos   id_boxc, -x, 0, 0
gppbind  id_boxc,  0
setcoli  id_boxc,  1, 0

;	コリジョングループを設定
; 何もしなければ、グループ値は 0
setcoli id_box,   2, 1
setcoli id_floor, 1, 0


isGrounded0 = 1

*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: gppapply id_box, GPPAPPLY_IMPULSE,  0  ,10,  0	; スペース ジャンプ
	if key&32: gppapply id_box, GPPAPPLY_IMPULSE,  0  ,-0.1,  0	; Enter 下に打ち付ける


	; ------------------------------
	;	めり込みゼロを防止
	; ------------------------------
	; 接地していないのに移動がない場合は、めり込みが無くなった可能性がある。
	; 下に打ち付けて様子を見る。
	; ジャンプ時の放物線の頂点でもこの条件を満たすことがあるが、下向きの力の大きさが十分小さければ影響は少ない。
	if (isGrounded0 = 0) & (isGrounded = 0) {
		getpos id_box, px_checkGround, py_checkGround, pz_checkGround
		f = 0
		f |= px_checkGround0 = px_checkGround
		f |= py_checkGround0 = py_checkGround
		f |= pz_checkGround0 = pz_checkGround
		if f {
			; 下に打ち付ける
			gppapply id_box, GPPAPPLY_IMPULSE,  0  ,-0.1,  0
		}
	}
	; 前回座標
	px_checkGround0 = px_checkGround
	py_checkGround0 = py_checkGround
	pz_checkGround0 = pz_checkGround
	; 地面設置変数
	isGrounded0 = isGrounded
	isGrounded = 0

	; 常に地面に押し付けるとめり込みゼロを回避できます。
	;gppapply id_box, GPPAPPLY_FORCE, 0, -0.001, 0


	; ------------------------------
	;	レイによる地面検出
	; ------------------------------
	getpos id_box,  px, py, pz
	setpos id_null, px, py, pz		; 自機の中心
	; setpos id_null, px, py-0.6, pz	; 自機の下  台の中に入ってしまう。
	; setpos id_null, px, py-0.5, pz	; 自機の底面 ちょうどいいみたい。
	; 自分自身など検出対象が出たら、次にぶつかるまで探す。
	repeat 5
		; レイの衝突を検出
		gppraytest objid, id_null, 4
		if objid <= 0 : break
		; コリジョングループを使って、自機を除外する
		getobjcoli cg, objid, 0
		if cg ! 2 : break
		; 次の検索開始位置
		; 少し下から再検索
		getwork id_null, x, y, z
		; setpos  id_null, x, y, z	; 検出結果がちらつく
		setpos  id_null, x, y-0.001, z
	loop
	; 検出結果が自機なので、検出しなかったことにする。
	if cg = 2 : objid = 0
	
	;	検出結果を使用
	; 高さなど各種情報を取得してみます。
	if objid > 0 {
		getobjcoli cg, objid, 0
		; 地面座標
		getwork id_null, x, y, z
		; 地面からの高さ
		posH_Ground = py - y
		; ログ
		log_ray = "     地面検出:あり●\n"
		log_ray+= "  地面からの高さ:" + posH_Ground + "\n"
		log_ray+= "  レイの衝突座標:" + strf(" ( %5.3f, %5.3f, %5.3f )", x, y, z) + "\n"
		log_ray+= " 相手のグループ値:" + cg

		; 一定の高さ以下なら接地
		if posH_Ground <= 0.6 {
			isGrounded = 1
			log_ray = "●接地:あり\n" + log_ray
		} else {
			log_ray = "○接地:なし\n" + log_ray
		}
	}
	if objid <= 0 {
		log_ray = "○接地:なし\n"
		log_ray+= "     地面検出:なし○\n"
		log_ray+= "  地面からの高さ:\n"
		log_ray+= "  レイの衝突座標:\n"
		log_ray+= " 相手のグループ値:"
	}

	; ------------------------------
	;	衝突情報による地面検出
	; ------------------------------
	gppcontact n, id_box
	if n>0 {
		; 接触中にしかログが表示されないようにすると
		; 一瞬で消えてしまうので、最後に接触した記録を表示するようにしています。
		log_coli = " 衝突情報:あり●\n"
		log_coli+= " 接触個数:" + n + "\n"
		
		; 接触座標が、自機の足元だった場合は衝突と判定する。
		m = "○接地:なし\n"
		repeat n
			gppinfo fv, id, id_box, cnt
			if fv(1) < py {
				m = "●接地:あり\n"
				isGrounded = 1
				break
			}
			; log_coli += "ID = " + id + " / " + strf(" ( %5.3f, %5.3f, %5.3f )", fv(0), fv(1), fv(2)) + "\n"
		loop
		log_coli = m + log_coli
		log_coli+= "\n"
	} else {
		log_coli = "○接地:なし\n"
		log_coli+= " 衝突情報:なし○\n"
		log_coli+= " 接触個数:0\n"
	}



	;	カメラ操作
	getpos id_box,dx,dy,dz
	gplookat GPOBJ_CAMERA, dx,dy,dz		; カメラから指定した座標を見る

	redraw 0			; 描画開始
	gpdraw				; シーンの描画

	;	接地判定の結果出力
	color
	gmode 3,,,100
	gradf 4,4, 420, 250, 0;, $ff00ff, $ffffff
	gmode 0

	color 255,255,255
	pos 8,8:mes "HGIMG4 sample"
	mes "---------- gppcontact 衝突検出 ----------"
	mes log_coli
	mes "---------- gppraytest レイ    ----------"
	mes log_ray
	mes "---------- 総合判定結果 ----------"
	if isGrounded {
		mes "●接地:あり"
	} else {
		mes "○接地:なし"
	}

	redraw 1			; 描画終了
	await 1000/60			; 待ち時間

	goto *main

 接地検出精度が向上しました。谷間にいる場合は、一瞬検出なしになりますが、ほぼ問題ないレベルです。これなら十分使えそうですね。

 接地不検出時に押し付ける以外の方法もあります。 常に小さい力で地面に押し付けることで、めり込みゼロを回避します。 まずは、以下の行をコメントにします。


  if f {
    ; 下に打ち付ける
    gppapply id_box, GPPAPPLY_IMPULSE,  0  ,-0.1,  0
  }

 次にこのような記述を追加します。


	; 常に地面に押し付けるとめり込みゼロを回避できます。
	gppapply id_box, GPPAPPLY_FORCE, 0, -0.001, 0

 どちらの方法も、現実の物理的にはありえないので正直どちらを使ってもいいと思います。 瞬間的に押し付けると反作用で跳ねてしまうので、通常は後者の方法が無難な気もします。

 常に地面に押し付けておけば、絶え間なく接地を検出できるんだからレイを飛ばす必要なくね? そう思いますよね。しかし、坂を下る際に接地が外れてしまう問題が発生することもあるようです。 詳しくは、UnityのisGroundedとレイについて検索するとすぐに出てくるので探してみてください。 やはりレイによる判定機能は別途必要です。

 なお、「-0.001」という値は、今回のサンプルでの調整値です。条件が変われば数値を変える必要があるかもしれませんので要注意です。

まとめ

 接地を検出するには、まずgppcontactgppraytestの両方を使って検出し、それでもめり込みがゼロになって検出不良を起こすことがあるので地面に押し付けて回避する。 この手順で検出できるようになりました。

 接地の有無を調べるだけで結構な行数が必要になってしまいました。 長くはなりましたが、gppraytestによる地面検出結果は、丸影を設置する際にも使います。 gppinfoも衝突座標まで取れるので、何かに使えそうです。

関連記事

  1. 地表を歩く 目次 地表を歩かせよう サンプル 大まかな手順 gppray...
  2. コライダーのようなもの 目次 はじめに カプセル コライダー カプセルコライダーを再...
  3. 地形データの制限 目次 はじめに 平面床をポリゴンで作る 床面を作る場合の対策...