斜面にキャラクターを這わせる。第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を読んで登坂可能な角度の斜面なら面の法線を記録します。
ちなみに、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を回転する、という作戦にしてあります。