【Unity/2D】実用レベルの移動&接地判定システム

【Unity】自作2D用Character Controllerでの坂道チェック

100点満点中90点ぐらいの、移動と接地判定を統合した、2D用Character Controller的な物。

  • 斜面対応(45度まで確認)。
  • ジャンプは高さ(ユニット単位)で指定可能。

画像素材は、無料アセットの[Ansimuz] Sunny Landを使用。
*同アセット内の坂道へTilemap Colliderを適用しても綺麗な斜面にならない事に注意。
(ドットが荒いのが原因?)

[Ansimuz] Sunny Landの坂道にはTilemap Colliderが上手く適用出来なかった

タッチ移動式のパターンも作りました。
(画面ボタン式ジャンプだと、UIへのクリックを除外しないといけませんが…)

2022/5/9: ピボット(原点)について説明するのを完全に忘れていたので追加。また、足元へ原点を合わせたパターンに、コードを変更。

サンプル動画

ジャンプの挙動チェック

  • Bのキャラクターは、AddForceベースで適当に作った物。
    重力適用のタイミングも完全に一致した。

斜面での移動の挙動チェック

  • Bのキャラクターは、AddForceベースで適当に作った物。
    移動スピードが遅過ぎて坂道が進めていない。
  • 残念ながら、坂道の角部分では進行方向となる斜面にプレイヤーが接触しない。
    ノーマルが取れないので、通常の移動となる。

使用手順

  • プレイヤー
    1. 空のオブジェクトを作成、Playerと命名。
    2. Rigidbody2Dをアタッチ。
      • Constraints -> Freeze Rotation -> Z:有効化
      • Collision Detection: Continuous
    3. 子の位置に、Spriteを作成して、キャラクター画像を設定。
    4. キャラ画像の位置を調整して原点の位置を足元に合わせる(後述)。
    5. CapsuleCollider2Dをアタッチして、サイズや位置を調整。
    6. Rigidbody2Dをアタッチ。
    7. Playerと命名して、新規スクリプトを作成。次項のコードを全文コピペ。
  • 地面
    • (Tilemapの場合)Tilemapへ、TilemapCollider2Dをアタッチ。
    • (Spriteの場合)PolygonCollider2D等をアタッチ。
    • Groundと命名したタグとレイヤーを設定しておく。

【重要】プレイヤーのピボット(原点)の位置を足元に合わせる

オブジェクトの原点を調整
  1. 左上のメニューバー下のアイコン群 -> オブジェクト移動モードに切り替え。
  2. 同アイコン群 -> Pivot表示モードに切り替え。
    (Pivotと表示されている時 = Pivot表示モード)
  3. ヒエラルキー -> Playerを選択し、Sceneビューでピボット(原点)を確認。
  4. ヒエラルキー -> 子のキャラ画像オブジェクトを選択。
  5. 原点の位置が足元になるように、子のキャラ画像のlocalPositionをズラす。

【コード】Player

詳細はコード内にコメントとして残しております。



//使わなかった為、一応コメントアウト。これをメインのスクリプトとして機能を追加していく場合は戻しておいてください。
//using System.Collections;
//using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{


//各コンポーネントをキャッシュしておく。インスペクターから紐付けすればAwake内の取得部分を消しても良い。
	public Transform tf;
	[SerializeField]
	Rigidbody2D rb2d;



//【任意の値】移動力。蓄積しないので、最大速度はmoveSpeed * Time.fixedDeltaTime(0.02)になる(つまり、この設定では0.2)。
	float moveSpeed = 10.0f;

//【任意の値】横方向への移動速度の上限。
	static float HorizontalMoveSpeedLimit = 1.0f;

//【任意の値】(ジャンプを除く)縦方向への移動速度の上限。
	static float VerticalMoveSpeedLimit = 1.0f;

//【任意の値】horizontalInputValueの加算率。
	float accelerateRate = 0.2f;

//【任意の値】horizontalInputValueの減衰率。
	float decayRate = 0.1f;


//【任意の値】接地判定を坂道に対応させる為にズラす値(大きい程、深い角度に対応)。
	static readonly float Tolerance = 0.08f;

//【要調整】接地判定をプレイヤーの原点からズラす値(原点が足元の場合、radiusの値 - Tolerance)。
	Vector2 offset = new Vector2(0, 0.25f - Tolerance);

//【要調整】接地判定の円の半径。プレイヤーより一回り小さい値(壁を床と判定しない為)。
	float radius = 0.25f;

//【任意の値】ジャンプの高さ(ユニット単位)。
	static readonly float JumpHeight = 3.0f;
	static readonly float HighJumpHeight = 5.0f;

//【任意の値】歩行可能な傾斜の制限(度単位)。歩行不可能な角度の坂は重力によって滑り落ちる。
	float slopeLimit = 45.0f;



//衝突結果が入る。OverlapCircleNonAllocでの使用なので、宣言時の初期化が必須。
	Collider2D[] results = new Collider2D[1];

//(物理的な)衝突結果が入る。要素数は一応大きめに設定。宣言時の初期化が必須。
	ContactPoint2D[] groundContacts = new ContactPoint2D[10];


//地面のレイヤーマスク。[SerializeField]を設定して、インスペクターからレイヤーを指定しても良い。
	LayerMask groundLayerMask;

//地面のコンタクトフィルター(レイヤーマスクと大体同じ)。[SerializeField]を設定して、インスペクターからレイヤーを指定しても良い。
	ContactFilter2D groundContactFilter;

//地面のタグ。
	static readonly string GroundTag = "Ground";


//メインの接地フラグ(falseで空中)。
	bool isGrounded;

//地面と物理的に接触しているかのフラグ(キャラクターが地面からoffsetの分浮くのを防ぐ)。
	bool isHitGround;

//ジャンプが発生したフレームに接地判定をしない為のフラグ。
	bool isJumpHappened;


//プレイヤーが左を向いた時の角度(使用する画像によっては左右逆かも)。
	Quaternion leftRotation = Quaternion.Euler(0, 180, 0);
//プレイヤーが右を向いた時の角度。
	Quaternion rightRotation = Quaternion.Euler(0, 0, 0);

//Rigidbodyのvelocity相当の値。
	Vector2 velocity;

//移動(左右)の入力度合い。
	float horizontalInputValue = 0;

//縦方向への移動量(ジャンプと重力)。
	float verticalMovement = 0;


//ジャンプの初期加速度。平方根の計算は負荷が掛かるので、ジャンプ毎に計算せず、宣言時に結果を保持しておく。
	float jumpInitialVelocity = Mathf.Sqrt(2 * DefaultGravityAbs * JumpHeight);
	float highJumpInitialVelocity = Mathf.Sqrt(2 * DefaultGravityAbs * HighJumpHeight);

//Physics2D.gravity.yを使うと宣言時に計算出来ないので、正数にして定数化。
	static readonly float DefaultGravityAbs = 9.81f;

//重力適用のタイミング故か誤差が出るので、若干補正。
	float jumpVelocityModifier = DefaultGravityAbs * 0.01f;

//地面とのノーマル(垂直なベクトル)。
	Vector2 groundNormal;

//地面とのノーマル(垂直なベクトル)を90度回転させたベクトル、斜面での進行方向。
	Vector2 projectOnPlane;

//(物理的な)衝突数。
	int contactCount;


//その他、細々とした物。
	Vector2 previousPosition;
	Vector2 currentPosition;
	Vector2 nextMovement;

	Vector2 tempNormal;



	void Awake()
	{
		tf = transform;
		rb2d = GetComponent<Rigidbody2D>();



//地面のレイヤーを設定。
		groundLayerMask = 1 << LayerMask.NameToLayer(GroundTag);
		groundContactFilter.SetLayerMask(1 << LayerMask.NameToLayer(GroundTag));
	}


//【PC用】ジャンプの入力取得。
/*
	void Update()
	{
		if (Input.GetKeyDown("z")) {
			Jump();
		}
	}
*/


	void FixedUpdate()
	{

//物理的に衝突しているかのチェック(OnCollisionEnter2DとOnCollisionStay2Dを合わせたような感じ)。
		contactCount = rb2d.GetContacts(groundContactFilter, groundContacts);

		if (0 < contactCount) {
			isHitGround = false;
			groundNormal = Vector2.up;

			for (int i = 0; i < contactCount; i++) {
				tempNormal = groundContacts[i].normal;
//(歩行可能な角度の)地上な場合、地面とのノーマル(垂直なベクトル)を保持しておく。
				if (Vector2.Angle(Vector2.up, tempNormal) <= slopeLimit) {
					isHitGround = true;
					groundNormal = tempNormal;
				}
			}
		}



		if (isJumpHappened) {
			isJumpHappened = false;
		} else {
//指定位置を中心として、円形に衝突判定(ゴミが出ない版)。Groundとの接触判定が1つでもあれば、isGroundedをtrueに。
//GetContactsだけの判定では何故か上手くいかないので、これで二重にチェック。
			isGrounded = 0 < Physics2D.OverlapCircleNonAlloc(rb2d.position + offset, radius, results, groundLayerMask);
		}


//空中ならば重力を加算し、地面とのノーマルをリセット。地上ならば縦方向への移動量をリセット。
		if (!isGrounded || !isHitGround) {
//重力の加算は移動量の適用より前に実行。
			verticalMovement += Physics2D.gravity.y * Time.fixedDeltaTime;
			groundNormal = Vector2.up;
		} else {
			verticalMovement = 0;
		}



//【PC用】左右への移動入力の取得。上書きする為、これを有効にするとスマホ用の方は機能しなくなるのに注意。
//        horizontalInputValue = Input.GetAxis("Horizontal");

//地面が平らな場合は通常通りに移動。傾いている場合は、坂道に沿ったベクトルに変える。
		if (groundNormal == Vector2.up) {
//Clampでスピード制限。
			nextMovement = Vector2.right * Mathf.Clamp(horizontalInputValue * moveSpeed * Time.fixedDeltaTime, -HorizontalMoveSpeedLimit, HorizontalMoveSpeedLimit);
		} else {
//地面とのノーマル(垂直なベクトル)を90度傾けた方向に向かって、横の移動を適用。
			projectOnPlane.Set(groundNormal.y, -groundNormal.x);
			nextMovement = projectOnPlane * Mathf.Clamp(horizontalInputValue * moveSpeed * Time.fixedDeltaTime, -HorizontalMoveSpeedLimit, HorizontalMoveSpeedLimit);
		}


		nextMovement.y += verticalMovement * Time.fixedDeltaTime;
		nextMovement.y = Mathf.Clamp(nextMovement.y, -VerticalMoveSpeedLimit, VerticalMoveSpeedLimit);


		previousPosition = rb2d.position;
		currentPosition = rb2d.position + nextMovement;
		velocity = (currentPosition - previousPosition);


		rb2d.MovePosition(currentPosition);
		nextMovement = Vector2.zero;


//Xの移動量が0の場合は今の向きのまま、移動量があれば向きを合わせる(本体の向きを変えたくないなら、spriteRenderer.flipXを切り替えても良い)。
		if (velocity.x != 0) {
			if (velocity.x < 0) {
				tf.rotation = leftRotation;
			} else {
				tf.rotation = rightRotation;
			}
		}

//X方向の入力値の減衰。
		horizontalInputValue = Mathf.Lerp(horizontalInputValue, 0, decayRate);
	}


//【スマホ用】移動ボタン(左)へ、リスナー登録。
	public void MoveLeft()
	{
		horizontalInputValue = Mathf.Lerp(horizontalInputValue, -1.0f, accelerateRate);
	}


//【スマホ用】移動ボタン(右)へ、リスナー登録。
	public void MoveRight()
	{
		horizontalInputValue = Mathf.Lerp(horizontalInputValue, 1.0f, accelerateRate);
	}


//【共通】スマホ時には、ジャンプボタンへ、リスナー登録。
	public void Jump()
	{
		if (!isGrounded)
			return;

		isGrounded = false;
		isHitGround = false;
		isJumpHappened = true;

//JumpHeight = 1で実行すると、Y = 0.9996482に到達する精度のジャンプ。
		verticalMovement = jumpInitialVelocity + jumpVelocityModifier;
	}

}

2D Tips

プレイヤー画像のサイズを修正

  • 対象の画像 -> インスペクター -> Pixel Per Unit: 大体の画像サイズ(32等)に変更。

スマホでの移動ボタン押しっ放しの判定

タイトルとURLをコピーしました