チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

斜面にキャラクターを這わせる。第02回

ベースクラス

前回の続き。派生した実装クラスを無視してベースクラス経由で操作できると気楽かなー*1という事でサンプル的にはベースクラスを作ります。

public abstract class BasicPlaneNormalTracer : MonoBehaviour {
	
	// public members
	public bool autoTraceOnUpdate = true;	// call TraceGround() On Update switch
	public string traceObjectName;
	
	// public methods
	public void Trace() { Trace(null); }
	public void Trace( GameObject groundTraceObject ) {
		if ( groundTraceObject == null ) {
			Transform trans = gameObject.transform.FindChild(traceObjectName);
			if ( trans != null ) groundTraceObject = trans.gameObject;
		}
		if ( groundTraceObject == null ) return;
		traceImpl(groundTraceObject);
	}
	
	// protected constructer
	protected BasicPlaneNormalTracer(){}
	protected BasicPlaneNormalTracer( string initialTraceObjectName ){
		traceObjectName = initialTraceObjectName;
	}
	
	// protected methods
	protected abstract void traceImpl( GameObject groundTraceObject );
	
	// private methods
	void Update () {
		if ( autoTraceOnUpdate ) Trace( null );
	}
}

修正のお知らせ 20121028*2
とくに説明する部分もないですが、処理の実態はともかくとして、子オブジェクトを名前で検索して斜面をトレースしてくれるとか、スイッチが入っていたら自動で斜面トレースするとか、それだけのクラスにしました。実際の記述作業では、ベースクラスも派生クラスも同時進行で書いていて、わりと頻繁に相互に修正されちゃうので、あまり深く考えずに進めます。

「2.CharacterContorollerやRigidbodyの衝突イベントから、接地面の法線ベクトルを取得する」のパターンの実装

[RequireComponent (typeof (CharacterController))]
public class CharacterContorollerPlaneNormalTracer : BasicPlaneNormalTracer {
	
	// public members
	public float animationAngle = 270f;
	public float traceAngleLimit = float.PositiveInfinity;
	public Vector3 currentPlaneNormal {
		get { return m_currentPlaneNormal; }
	}
	
	// public constructer
	public CharacterContorollerPlaneNormalTracer() {}
	
	// protected methods
	protected override void traceImpl (GameObject groundTraceObject) {
		
		Quaternion q = Quaternion.FromToRotation(Vector3.up, transform.InverseTransformDirection(m_currentPlaneNormal));
		
		if ( float.IsNaN(animationAngle) == false && float.IsInfinity(animationAngle) == false )
			q = Quaternion.RotateTowards( groundTraceObject.transform.localRotation, q, animationAngle * Time.deltaTime );
		
		if ( (float.IsNaN(traceAngleLimit) == false) && (float.IsInfinity(traceAngleLimit) == false) )
			q = Quaternion.RotateTowards( Quaternion.Euler(0f,0f,0f), q, traceAngleLimit );
		
		groundTraceObject.transform.localRotation = q;
	}
	
	// private members
	private Vector3 m_currentPlaneNormal = Vector3.up;
	
	// private methods
	void OnControllerColliderHit (ControllerColliderHit hit) {
		if ( Mathf.Cos( hit.controller.slopeLimit * Mathf.Deg2Rad ) <= hit.normal.y ) m_currentPlaneNormal = hit.normal;
	}
}

とりあえず「CharacterControllerコンポーネントがセットされているGameObject用」のスクリプトです(なので冒頭にRequireComponent属性をつけています)。

CharacterControllerからOnControllerColliderHit()が飛んでくるので、CharacterController.slopeLimitを読んで登坂可能な角度の斜面なら面の法線を記録します。

Mathf.Cos( hit.controller.slopeLimit * Mathf.Deg2Rad ) <= hit.normal.y

2ベクトルの角度の差は「ベクトルのなす角」「内積」をググれば分かります。ここで右辺のhit.normal.yは、Vector3.upとhit.normalの内積です。「hit.normalは長さ1に正規化されているであろう」として、Vector3.upとの内積計算を展開するとただのhit.normal.yになります。それをslopeLimitのCosと比較しています。

ちなみに、CharacterControllerの限界登坂制御は結構適当な模様で、落っこちてくるぶんにはslopeLimitを超えるアングルにも容赦なく着地可能となっています。また、階段のように細かい段差の場合は90度でも登りますので、slopeLimitが45度だろうと80度くらいの傾斜の途中に立つという状況はいくらでも発生します*3。例えば「85度のわずかな段差の壁面に乗った時」などに、85度をそのままトレースしてものすごい上向きになったり下向きになったりするのを防ぐため、角度の判定処理を行っています。

なおOnControllerColliderHit()に飛んでくるhit.normalは「いくらかの補間が効いた値」と考えると良いようです。このような判定式によってslopeLimit以上の傾斜は何もせずハジく設定にしても、それで反応しないはずの急斜面に降り立った時は、だいたいslopeLimitくらいの急斜面に立ったような挙動を見せます。

traceImpl()の方は、法線をグローバル座標からローカル座標に戻して、Vector3.upからローカル法線へ回転するQuaternionを計算しています。そしてドタバタしないで多少モッサリさせるための補間アニメーションの計算を行います。ついでに角度制限処理もオマケしておきます(でもこの機能はあまり使い道がないかも…)。

使い方

CharacterContorollerPlaneNormalTracerをUnity上でセット、あるいはAddComponentした後、traceObjectNameを設定するとそれなりに動きます。

次回は「1.足元の点をいくつかサンプリングして、各点の地面の高さを得て、それらを結ぶ面の傾きを計算する」パターンの実装。

追記 [2012.09.29]
GameObjectの構成を明記するのを失念しておりましたので補記。名前はとりあえず何でも良いですが

[OuterGameObject]
└[InnerGameObject]

こんな親子関係のGameObjectを作り、スクリプトやCharacterControllerを[OuterGameObject]にセット。スクリプトのtraceObjectNameプロパティに"[InnerGameObject]"と任意の名前を設定すると、[OuterGameObject]のスクリプトが地面の傾きを検知して[InnerGameObject]のローカルなtransformを回転する、という作戦にしてあります。

*1:後々外部から機能を呼び出す時に実装別の部分を意識しなくて済むので

*2:Transform trans = gameObject.transform.FindChild(traceObjectName);のところ、Transform trans = gameObject.transform.Find(traceObjectName);になっとりました。申し訳ありません。

*3:slopeLimitを無視できるほど「細かい」段差ってどんな定義やねんってよく見たらstepOffsetてパラメーターで許容段差高が設定できるんですね…知らなかった。