👋

【Unity】上下に通り抜け可能な足場を作る

2021/06/28に公開

概要

下から上に通り抜ける足場の作り方は多く存在するのですが、上に乗ってる状態から下に降りる足場の作り方は探してもあんま見つからない感じです。
今回はUnityで楽に上下両方から通り抜け可能な足場を作ってみようと思います。イメージとしてはスマブラの足場ですね。

ちなみに完成するとこんな感じになります。

要点

  • コリジョンとトリガーを用意する
  • すり抜け時にコリジョンを無効にする
  • 下側からのすり抜けはトリガーを使用する
  • 上側からのすり抜けはOverlap関数を使用する

実装手順

1. とりあえず動くキャラクターと適当にオブジェクトを配置する

  • 地面と動かせるものを準備する
    • 地面はGround、動かせるものはPlayerとかわかりやすいようにしておく
    • 移動だけでなく、ジャンプも忘れないように
  • 通り抜け用の足場を作成する
    • 名前はPlatformとか安直適当に付けておく
  • プレイヤーと足場それぞれにタグ・レイヤーを設定する
    • レイヤー名はそれぞれPlayerPlatform
    • タグの設定はプレイヤーのみでOK

2.足場にコリジョンとトリガーをつける

  • 足場用のコリジョンを追加する
    • オブジェクトの見た目と同じ大きさでOK
  • プレイヤー判定用のトリガーを追加する
    • オブジェクトの下にはみ出るように設定する
    • 上にははみ出ないようにすること

3.コリジョンの有効設定を行う処理を追加する

  • コリジョンを一時的に無効にする処理を追加する
    • プレイヤーが通り抜けるタイミングでコリジョンを無効にしたいため
    • 保険として一定時間経過後に有効にする処理を入れておくと良い(コルーチンやUniRx等)
Platform.cs
public void DisablePlatform() {
	if (!boxCollider.enabled) { return; }

	boxCollider.enabled = false;

	// 念の為、一定時間後に足場を有効化する
	WaitProcess(5.0f, () =>	boxCollider.enabled = true);
}

private IEnumerator WaitProcess(float time, Action action) {
	yield return new WaitForSeconds(time);
	action();
}

4.トリガー侵入/脱出時にコリジョンの有効設定を行う

下側から通り抜けできるようにするため、トリガーを使用した処理を追加する。

  • トリガーに侵入したらコリジョンを無効にする
  • トリガーから脱出したらコリジョンを有効にする
Platform.cs
private void OnTriggerEnter(Collider col) {
	if (col.gameObject.tag == "Player") {
		boxCollider.enabled = false;
	}
}

private void OnTriggerExit(Collider col) {
	if (col.gameObject.tag == "Player") {
		boxCollider.enabled = true;
	}
}

5.プレイヤーがコリジョン接地時にコリジョンの有効設定を行う

上側から通り抜けできるようにするため、コリジョン接地時の処理を追加する

  • OverlapSphere等を使用してコリジョンを取得する
  • 降りる操作をした時、コリジョンを無効にする
    • 今回はしゃがみ中にジャンプ操作をしたら降りる仕様にする
Player.cs
private void Update() {
	moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, 0);
	isJump = Input.GetButtonDown("Jump");
	isCrouch = Input.GetAxis("Vertical") <= -0.1;		// 下ボタン入力時

	Jump();
	DescendPlatform();
}

private void DescendPlatform() {
	// 地面に接着 かつ しゃがみ状態でジャンプ入力時に降りる
	if (isCrouch && isJump) {
		var radius = 1.0f;
		var layerNo = LayerMask.NameToLayer("Platform");
		var platforms = Physics.OverlapSphere(transform.position + groundOffset, radius, 1 << layerNo)
								.Select(platform => platform.GetComponent<Platform>())
								.OrderBy(platform => platform.transform.position.x)
								.ToList();

		if (!platforms.Any()) { return; }

		// 範囲内の足場を全て無効化する
		foreach (var platform in platforms) {
			platform.DisablePlatform();
		}
	}
}

注意点

今回はお手軽に実装するため、足場のコリジョン自体を無効にする手法にしました。
この実装方法では複数キャラクターに対応していないため、以下の点に考慮する必要があります。

  • 敵を落ちないようにしたいなら敵専用のコリジョンを別途追加する(レイヤーで分ける)
  • キャラクター別に対応したい場合は足場ではなく、キャラクターのコリジョンを無効にする手法を考える

ソースコード

Player.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class Player : MonoBehaviour {

	[SerializeField] private Rigidbody rbody;
	[SerializeField] private float moveSpeed;
	[SerializeField] private float jumpForce;
	[SerializeField] private Vector3 groundOffset;

	private Vector3 moveDirection;
	private bool isJump;
	private bool isCrouch;

	/// <summary>
	/// 更新処理
	/// </summary>
	private void Update() {
		moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, 0);
		isJump = Input.GetButtonDown("Jump");
		isCrouch = Input.GetAxis("Vertical") <= -0.1;		// 下ボタン入力時

		Jump();
		DescendPlatform();
	}

	/// <summary>
	/// 更新処理(一定間隔)
	/// </summary>
	private void FixedUpdate() {
		transform.position += moveDirection * moveSpeed * Time.deltaTime;
	}

	/// <summary>
	/// ジャンプ
	/// </summary>
	private void Jump() {
		if (isJump && !isCrouch) {
			rbody.AddForce(new Vector3(0, jumpForce, 0), ForceMode.Acceleration);
		}
	}

	/// <summary>
	/// 足場から降りる
	/// </summary>
	private void DescendPlatform() {
		// しゃがみ状態でジャンプ入力時に降りる
		if (isCrouch && isJump) {
			var radius = 1.0f;
			var layerNo = LayerMask.NameToLayer("Platform");
			var platforms = Physics.OverlapSphere(transform.position + groundOffset, radius, 1 << layerNo)
									.Select(platform => platform.GetComponent<Platform>())
									.OrderBy(platform => platform?.transform.position.x)
									.ToList();

			if (!platforms.Any()) { return; }

			// 範囲内の足場を全て無効化する
			foreach (var platform in platforms) {
				platform.DisablePlatform();
			}
		}
	}
}
Platform.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Platform : MonoBehaviour {
	[SerializeField] private BoxCollider boxCollider;		// 足場用のコリジョン

	/// <summary>
	/// 起動前初期化
	/// </summary>
	private void Start() {
		boxCollider.enabled = true;
	}

	/// <summary>
	/// 足場を無効化する
	/// </summary>
	public void DisablePlatform() {
		if (!boxCollider.enabled) { return; }

		boxCollider.enabled = false;

		// 念の為、一定時間後に足場を有効化する
		WaitProcess(5.0f, () =>	boxCollider.enabled = true);
	}

	/// <summary>
	/// 待機用コルーチン
	/// </summary>
	/// <param name="time">待機時間</param>
	/// <param name="action">待機後の処理</param>
	private IEnumerator WaitProcess(float time, Action action) {
		yield return new WaitForSeconds(time);
		action();
	}

	/// <summary>
	/// トリガー判定(Enter)
	/// </summary>
	/// <param name="col">コリジョン</param>
	private void OnTriggerEnter(Collider col) {
		if (col.gameObject.tag == "Player") {
			boxCollider.enabled = false;
		}
	}

	/// <summary>
	/// トリガー判定(Exit)
	/// </summary>
	/// <param name="col">コリジョン</param>
	private void OnTriggerExit(Collider col) {
		if (col.gameObject.tag == "Player") {
			boxCollider.enabled = true;
		}
	}
}

Discussion