【Unity/2D】タッチ移動版CharacterController2D

【Unity/2D】真・タッチ移動版CharacterController2D

自作の『真・CharacterController2D』のタッチ移動Ver。
タッチした位置まで一定速度で歩きます(Y座標は無視)。
画面押しっぱなしで走り続ける事も可能。

VerUpして、ボタンUI上でのタッチ無視や、移動床との連動等、モロモロに対応!

使用手順は元記事の「設定方法」の項目を御覧ください。

サンプル動画

(音声なし)

コード

GameManager

インプット部分のサンプル。

  • スクリーン座標をワールド座標に変換して、X値だけPlayerの移動用メソッドへ渡す。
  • ドラッグも取得するので、Input.GetMouseButtonで判定。
  • EventSystemが必須なので、先に何らかのUI要素を作成しておく。
  • インスペクターからPlayerを紐付けしておく。


//このサンプルでは使わなかったので、一応コメントアウト。
//using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class GameManager : MonoBehaviour
{

	public static GameManager Instance = null;


//インスペクターからプレイヤーを紐付けしておく。
	[SerializeField]
	Player player;


	Camera mainCamera;
	Vector3 touchPosition;

//(EventSystem.currentから使っても負荷は無いかもしれないが)一応キャッシュしておく。EventSystemを複数使う場合は都度入れ替える事。
	EventSystem eventSystem;

//キャッシュしておく。EventSystemを複数使う場合は都度入れ替える事。
	PointerEventData pointer = new PointerEventData(EventSystem.current);

//タッチ位置のUI要素が返されるList。再確保されないようにキャパシティは、やや多めに指定しておく。
	List<RaycastResult> isPointerOverUIResults = new List<RaycastResult>(10);



	void Awake()
	{
		if (Instance == null)
			Instance = this;
		else if (Instance != this)
			Destroy(gameObject);    

		DontDestroyOnLoad(gameObject);



		mainCamera = Camera.main;
		eventSystem = EventSystem.current;
	}


	void Update()
	{
		CheckInput();
	}


	void CheckInput()
	{
		if (Input.GetMouseButton(0)) {
			touchPosition = Input.mousePosition;

//タップ位置がUI要素と被っていた場合は弾く。
			if (IsPointerOverUI())
				return;

			player.MoveToTouchPosition(mainCamera.ScreenToWorldPoint(touchPosition).x);
		}
	}


	bool IsPointerOverUI()
	{
		pointer.position = touchPosition;
		eventSystem.RaycastAll(pointer, isPointerOverUIResults);
		return 0 < isPointerOverUIResults.Count;
	}

}

Player

内容が重複するので、余り重要でないコメントは消しております。

  • 各コンポーネントを紐付けしておく。



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

public class Player : MonoBehaviour
{


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



//【任意の値】移動力(SmoothDampでの移動速度のリミット)。
	float moveSpeed = 10.0f * SmoothDampModifier;

//moveSpeedをSmoothDampのMaxSpeedに設定する場合に掛けると、通常使う時と同程度の速度になる。
	static readonly float SmoothDampModifier = 2.386f;

//【任意の値】横方向への移動速度の上限(Time.fixedDeltaTimeが掛かった後の値)。
	static readonly float HorizontalMoveSpeedLimit = 1.0f;

//【任意の値】(ジャンプを除く)縦方向への移動速度の上限(Time.fixedDeltaTimeが掛かった後の値)。
	static readonly float VerticalMoveSpeedLimit = 1.0f;



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

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

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

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



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

//プレイヤーColliderの衝突結果が入る。要素数は一応大きめに設定。宣言時の初期化が必須。
	ContactPoint2D[] groundContacts = new ContactPoint2D[5];


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

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

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


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

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

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


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

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


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


//ジャンプの初期加速度。平方根の計算は負荷が掛かるので、ジャンプ毎に計算せず、宣言時に結果を保持しておく。
//複数で出てくる敵キャラ等で使う場合は、GameManagerとかに出しておくべし。電卓で計算して定数化しておいても良いかも。
	float jumpInitialVelocity = Mathf.Sqrt(2 * DefaultGravityAbs * JumpHeight);
	float highJumpInitialVelocity = Mathf.Sqrt(2 * DefaultGravityAbs * HighJumpHeight);

	static readonly float DefaultGravityAbs = 9.81f;

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

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

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

//(プレイヤーColliderとの)衝突数。
	int contactCount;



	Vector2 previousPosition;
	Vector2 currentPosition;
	Vector2 nextMovement;

	Vector2 tempNormal;



//乗っている移動床の移動速度。
	[System.NonSerialized]
	public Vector2 platformVelocity;

	Vector2 previousNormal;

	static readonly float CeilingAngleThreshold = 45.0f;
	static readonly float WallAngleThreshold = 45.0f;

	static readonly float FixedDeltaTime = 0.02f;


//移動中フラグ。
	bool isMoving;

//移動処理関連。
	float currentPositionX;
	float nextPositionX;
	float moveTargetPositionX;
	Coroutine move;

//コルーチンのウェイト。毎回newしないで、キャッシュしておく。
	static readonly WaitForFixedUpdate FixedWait = new WaitForFixedUpdate();
//1フレームでの移動量が、この値以下だと移動終了。
	static readonly float MoveEndThreshold = 0.01f;

//SmoothDampし終わるまでの大体の時間。SmoothDampの仕様が謎なので変更しないでください(こっちを変更するとMaxSpeed通りにならなくなる)。
	static readonly float SmoothTime = 0.01f;
//SmoothDampでの加速度。
	float velocityX;


//【PC用】
//頻繁に使うStringは定数化しておくべし。
/*
	static readonly string JumpKey = "z";
*/



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



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



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



	void FixedUpdate()
	{

//重力を先に適用し、若干めり込ます事で、移動床での挙動が安定。
//空中ならば重力を加算し、地面とのノーマルをリセット。地上ならば縦方向への移動量をリセット。
		if (!isGrounded || !isHitGround) {

			verticalMovement += Physics2D.gravity.y * FixedDeltaTime;


			groundNormal = Vector2.up;
		} else if (!isJumpHappened) {
			verticalMovement = 0;
		}


		if (!isJumpHappened) {

			contactCount = rb2d.GetContacts(groundContactFilter, groundContacts);


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



//タイルマップのような複数の正方形Colliderで出来た地面上でガタつくのを補正。
				if (contactCount == 1) {
					tempNormal = groundContacts[0].normal;
//(歩行可能な角度の)地上な場合、地面とのノーマル(垂直なベクトル)を保持しておく。
					if (Vector2.Angle(Vector2.up, tempNormal) <= slopeAngleLimit) {
						isHitGround = true;
						groundNormal = tempNormal;
						previousNormal = tempNormal;
					} else if (0 < verticalMovement && Vector2.Angle(Vector2.down, tempNormal) <= CeilingAngleThreshold) {

						verticalMovement = 0;
					} else if (0 < velocityX && Vector2.Angle(Vector2.left, tempNormal) <= WallAngleThreshold) {

						StopMove();
					} else if (velocityX < 0 && Vector2.Angle(Vector2.right, tempNormal) <= WallAngleThreshold) {

						StopMove();
					}
				} else {
					for (int i = 0; i < contactCount; i++) {
						tempNormal = groundContacts[i].normal;

						if ((previousNormal == Vector2.zero || tempNormal == previousNormal) && Vector2.Angle(Vector2.up, tempNormal) <= slopeAngleLimit) {
							isHitGround = true;
							groundNormal = tempNormal;
							previousNormal = tempNormal;
							break;
						} else if (0 < verticalMovement && Vector2.Angle(Vector2.down, tempNormal) <= CeilingAngleThreshold) {

							verticalMovement = 0;
						} else if (0 < velocityX && Vector2.Angle(Vector2.left, tempNormal) <= WallAngleThreshold) {

							StopMove();
						} else if (velocityX < 0 && Vector2.Angle(Vector2.right, tempNormal) <= WallAngleThreshold) {

							StopMove();
						}
					}
				}
			} else {
				previousNormal = Vector2.zero;
			}

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



		if (isMoving) {
			if (groundNormal == Vector2.up) {
				nextMovement = Vector2.right * Mathf.Clamp(nextPositionX - currentPositionX, -HorizontalMoveSpeedLimit, HorizontalMoveSpeedLimit);
			} else {
//地面とのノーマル(垂直なベクトル)を90度傾けた方向に向かって、横の移動を適用。
				projectOnPlane.Set(groundNormal.y, -groundNormal.x);
				nextMovement = projectOnPlane * Mathf.Clamp(nextPositionX - currentPositionX, -HorizontalMoveSpeedLimit, HorizontalMoveSpeedLimit);
			}
		}


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


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



		if (platformVelocity == Vector2.zero) {
			rb2d.position = currentPosition;
		} else {
			rb2d.position = currentPosition + platformVelocity;
			platformVelocity = Vector2.zero;
		}
		nextMovement = Vector2.zero;


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


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


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


//JumpHeight = 1で実行すると、Y = 0.9996482に到達する精度のジャンプ(バラツキあり)。
		verticalMovement = jumpInitialVelocity + jumpVelocityModifier;
	}


	public void MoveToTouchPosition(float x)
	{
//多重実行を防止。
		if (move != null) {
			StopCoroutine(move);
		}

		moveTargetPositionX = x;
		move = StartCoroutine(Move());
	}

	IEnumerator Move()
	{
		if (!isMoving) {
			isMoving = true;
			velocityX = 0;
		}

//移動時に壁に引っ掛かった(nextPositionXのみが目的座標にある)場合、瞬間移動してしまうのを防ぐ。
		nextPositionX = rb2d.position.x;

		while (true) {
			currentPositionX = rb2d.position.x;
			nextPositionX = Mathf.SmoothDamp(nextPositionX, moveTargetPositionX, ref velocityX, SmoothTime, moveSpeed, FixedDeltaTime);

//一定スピード以下になると終了。
			if (-MoveEndThreshold <= velocityX && velocityX <= MoveEndThreshold) {
				isMoving = false;
				move = null;
				yield break;
			}


			yield return FixedWait;


//空中で壁に移動した時に、歩行時間分浮いてしまう問題への対処。
			if (-MoveEndThreshold <= (currentPositionX - rb2d.position.x) && (currentPositionX - rb2d.position.x) <= MoveEndThreshold) {
				isMoving = false;
				move = null;
				yield break;
			}
		}
	}


	void StopMove()
	{
		if (move != null) {
			StopCoroutine(move);
			isMoving = false;
			velocityX = 0;
		}
	}
}

ご活用ください。

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