さて、前回で音をつけるところまでいきました。今回からちょっとギミックを追加していきたいと思います。今回は動画のほうがわかりやすいかもしれません。
この記事は本のように順を追って解説しています。この記事は途中のページになります。
この記事を見ていて、現在の状況がわからない場合や忘れてしまった事などが出てきたら↓のリンクから目次ページへ飛べますので立ち戻って見てください。
<下からのみすり抜ける床の作り方>
様々なギミックの床を作る前に、下から一方通行ですり抜ける床を作っておかないと各種ギミック床が下から乗ることができないので、まずは下からのみすり抜けられるようにします。
とりあえず、適当に下書きで書いた床を配置して、Box Collider 2Dをくっつけます。
※タイルマップではなく、スプライトを直接置いてください
Add ComponentでPlatform Effector 2Dというのを貼り付けます。
そしてボックスコライダーのインスペクターでUsed By Effectorにチェックを入れましょう。
こうすることで、PlatformEffector2Dを使用することができるようになります。
このPlatformEffector2Dはコライダーを一方通行にしてくれる便利なコンポーネントです。これをくっつけるだけで一方通行になるのでUnity様様ですね。
↓のような感じになります。
さて、Unityの機能によって、すり抜ける床になりましたが、現在のプレイヤーのスクリプトは頭に地面がぶつかったらジャンプをやめる処理が入っています。そのため、頭にある設置判定を、すり抜ける床に関しては反応しないようにしないといけません。
この床を判別するために、タグを追加しましょう。Add TagからGroundPlatformというのを追加します。そして、床のタグを GroundPlatform にします。
そして、接地判定のスクリプトで、頭の判定と、足の判定を分けなければいけないため、インスペクターでどっちか選択できるようにします。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GroundCheck : MonoBehaviour { [Header("エフェクトがついた床を判定するか")] public bool checkPlatformGroud = true;private string groundTag = "Ground";
private string platformTag = "GroundPlatform";
private bool isGround = false;
private bool isGroundEnter, isGroundStay, isGroundExit;
//接地判定を返すメソッド
public bool IsGround()
{
if (isGroundEnter || isGroundStay)
{
isGround = true;
}
else if (isGroundExit)
{
isGround = false;
}
isGroundEnter = false;
isGroundStay = false;
isGroundExit = false;
return isGround;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundEnter = true;
}
else if(checkPlatformGroud && collision.tag == platformTag)
{
isGroundEnter = true;
}
}
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundStay = true;
}
else if (checkPlatformGroud && collision.tag == platformTag)
{
isGroundStay = true;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundExit = true;
}
else if (checkPlatformGroud && collision.tag == platformTag)
{
isGroundExit = true;
}
}
}
そして、頭の判定はインスペクターで、チェックを外し、足の方の判定はチェックをつけた状態にします。
これで、↓のように一方通行にすり抜けることができます。
PlatformEffector2Dはパラメータが複雑なので、基本的にデフォルトのまま置いておいた方がいいかもしれません。
↑の半円がなんなのかとか、パラメータをいじってみたい方は↓の記事を参考にしてください。
ここで1つポイントがあります。
必ず下からのみすり抜ける床の当たり判定をプレイヤーの接地判定より薄くする事です。
こうしないと地面の中で足の判定の方が接地判定になってしまってストンと落ちたり地面の中で多段ジャンプができるようになってしまいます。
<動く床の作り方>
動く床のスクリプト解説
それでは動く床を実装していきます。
ただ床を動かすスクリプトを作っても汎用性に欠けるので、オブジェクトを指定した経路通りに動くスクリプトを作成します。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveObject : MonoBehaviour { [Header("移動経路")]public GameObject[] movePoint; [Header("速さ")]public float speed = 1.0f; private Rigidbody2D rb; private int nowPoint = 0; private bool returnPoint = false; private void Start() { rb = GetComponent<Rigidbody2D>(); if (movePoint != null && movePoint.Length > 0 && rb != null) { rb.position = movePoint[0].transform.position; } } private void FixedUpdate() { if(movePoint != null && movePoint.Length > 1 && rb != null) { //通常進行 if (!returnPoint) { int nextPoint = nowPoint + 1; //目標ポイントとの誤差がわずかになるまで移動 if (Vector2.Distance(transform.position, movePoint[nextPoint].transform.position) > 0.1f) { //現在地から次のポイントへのベクトルを作成 Vector2 toVector = Vector2.MoveTowards(transform.position, movePoint[nextPoint].transform.position, speed * Time.deltaTime); //次のポイントへ移動 rb.MovePosition(toVector); } //次のポイントを1つ進める else { rb.MovePosition(movePoint[nextPoint].transform.position); ++nowPoint; //現在地が配列の最後だった場合 if (nowPoint + 1 >= movePoint.Length) { returnPoint = true; } } } //折返し進行 else { int nextPoint = nowPoint - 1; //目標ポイントとの誤差がわずかになるまで移動 if (Vector2.Distance(transform.position, movePoint[nextPoint].transform.position) > 0.1f) { //現在地から次のポイントへのベクトルを作成 Vector2 toVector = Vector2.MoveTowards(transform.position, movePoint[nextPoint].transform.position, speed * Time.deltaTime); //次のポイントへ移動 rb.MovePosition(toVector); } //次のポイントを1つ戻す else { rb.MovePosition(movePoint[nextPoint].transform.position); --nowPoint; //現在地が配列の最初だった場合 if (nowPoint <= 0) { returnPoint = false; } } } } } }
スクリプト内のコメントを読んでもらえればほぼわかると思いますが、一応解説すると
if (Vector2.Distance(transform.position, movePoint[nextPoint].transform.position) > 0.1f)
Vector2.Distanceというのは2つの位置の距離を測るメソッドです。現在位置と次の位置との距離を測って、距離が小さくなければという判定をしています。
Time.deltaTimeを使用する場合、ぴったりな値になりづらく誤差が生じるのでこのようにちょっと幅を持たせています。
//現在地から次のポイントへのベクトルを作成 Vector2 toVector = Vector2.MoveTowards(transform.position, movePoint[nextPoint].transform.position, speed * Time.deltaTime);
これはコメントに書いてある通りなんですが、現在地から次のポイントへのベクトルを作成しています。speed * Time.deltaTimeというのがここで生成される最大のベクトルの長さになるので、少しずつ移動することを表しています。
//次のポイントへ移動 rb.MovePosition(toVector);
さらに↑も重要ポイントです。動く床はコライダーを持っているのでTransform系で移動させると重くなってしまいます。その為物理演算系で移動させる為Rigidbody2Dで移動させています。
今までと同じようにvelocityを使いたいところですが、velocityだと「速さ」である為、正確に位置が取りづらい為MovePositionを使用しています。
MovePositionはその位置までオブジェクトを移動させるという意味になります。
ちなみに
rb.position
と
rb.MovePosition
の2種類が存在するのですが、上が空間転移で下が瞬間移動です。
瞬間移動の話は↓の記事でまとめてあります。興味があったら見てみてください。
空間転移は移動経路にコライダーがあっても無視します。瞬間移動は移動経路にコライダーがあると補間されます。
この辺の表現は解説されている方によってマチマチですね。自分は物理計算が入らない移動を空間転移、物理計算が入る移動を瞬間移動と表現しています。
動く床のスクリプトの使用方法
では、このスクリプトの使い方を説明します。
ステージ管理の時に解説した事と同じことをします。
まずは、カラのゲームオブジェクトを作成し、ゲームオブジェクトの左上の灰色の箱みたいな奴をクリックしてください。
↓のようなメニューが出てきたと思います。
適当に好きな色の奴を選んでください。できたら上2段の横長の物がいいと思います。
そうすると、カラだったゲームオブジェクトがシーンビュー上で映るようになります。
床は、このゲームオブジェクトの位置を順番に移動するようになります。
↑のように設置しました。
設置したゲームオブジェクトをインスペクターで設定します↓
Rigidbody2Dもつけましょう。
回転しないようにFreezeRotationのZにもチェックを入れます。
ポイントとしては、Body TypeをKinematicにしましょう。
Kinematicについての解説は↓の記事で行なっていますので詳しく知りたい方は参考にしてみてください。
何故Kinematicにするのか
Kinematicというのは簡単に言うとその物体には物理演算が適用されません。そもそも動く床が物理の法則を無視しているので、適用しない方がいいですよね。
あれ?それって意味あるのって話ですが、「その物体には」物理演算が適用されないのであって、その物体は物理演算が適用されている物体には影響します。
この場合、動く床がプレイヤーにぶつかったとき、プレイヤーは動く床に押し出されますが、逆にプレイヤーが動く床にぶつかったとき、動く床はビクともしません。
動く床 → プレイヤー に対しては物理演算が適用されるが
プレイヤー → 動く床 には物理演算は適用されないということです。
これで、プレイヤーが乗った重みなどで、動く床が沈んで行ってしまう等の物理演算をシカトしつつ、上に乗ることができます。
ちなみに、プレイヤーは物理演算の計算結果を無視するためにvelocityで動かしていると以前解説しましたが、このKinematicとは全く違うものになります。
プレイヤーをvelocityで動かすというのは物理演算は行うけど、速度に関する結果は破棄するという意味になります。つまり、衝突はするけど、衝突の際に発生した弾き飛ばし等のプレイヤーが速度をもってしまう結果を破棄しているということです。
Kinematicは相手が物理演算で動いていないと衝突さえしません。動く床同士や、動く床とタイルマップは重なってもすり抜けます。こういう違いがあるわけですね。
ちなみに、プレイヤーの物理演算を破棄している理由はこのような動く床などの物理の法則を無視したギミックが2Dアクションにはしばしば見られるからです。物理の法則を無視したヤツから物理的な介入を受けて予測不能な動きをしてしまうと困るからですね。
動く床を動作させてみる
設定が完了した状態で再生すると↓のようになります。
さて、これでオブジェクトを指定した経路通りに動かす事はできました。これは床以外の物にも使用できるので便利です。
もし、床がガクガクして動く場合は床のRigidobody 2DのInterpolateのところをInterpolateにしてください。
物理演算と描画のズレを補間してくれます。
さて、これで動く床を作ることができました。
しかし、動く床で使用する場合、上に乗っているプレイヤーが滑ってしまっているのがわかるかと思います。
ちょっと動く床としてはアレなので動く床なりの工夫をします。
動く床で滑らないようにする
さて、動く床で滑らないようにする対策として、プレイヤーを床の子オブジェクトにするというやり方が有名です。
しかしながら、床の子オブジェクトにする場合、Transform操作する必要が出てくるため、物理演算の再計算が走るため、筆者は余りおススメしません。
あとこのサイトではキャラクターの反転をスケールでしているので子オブジェクトにしてしまうと大きさが変になってしまいます。また、Surface Effector2Dを使う対策も存在するのですが、うまく動作しませんでした。また、Physic Material 2Dで摩擦係数を上げて滑らなくする方法もあるのですが、これはプレイヤーが横に動きづらくなってしまいます。
その為、何か他のものに頼るのではなく、動く床の上に載っている場合、ちゃんと速度計算してあげる必要がありそうです。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveObject : MonoBehaviour { [Header("移動経路")] public GameObject[] movePoint; [Header("速さ")] public float speed = 1.0f;private Rigidbody2D rb;
private int nowPoint = 0;
private bool returnPoint = false;
private Vector2 oldPos = Vector2.zero;
private Vector2 myVelocity = Vector2.zero;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
if (movePoint != null && movePoint.Length > 0 && rb != null)
{
rb.position = movePoint[0].transform.position;
oldPos = rb.position;
}
}
public Vector2 GetVelocity()
{
return myVelocity;
}
private void FixedUpdate()
{
if (movePoint != null && movePoint.Length > 1 && rb != null)
{
//通常進行
if (!returnPoint)
{
int nextPoint = nowPoint + 1;
//目標ポイントとの誤差がわずかになるまで移動
if (Vector2.Distance(transform.position, movePoint[nextPoint].transform.position) > 0.1f)
{
//現在地から次のポイントへのベクトルを作成
Vector2 toVector = Vector2.MoveTowards(transform.position, movePoint[nextPoint].transform.position, speed * Time.deltaTime);
//次のポイントへ移動
rb.MovePosition(toVector);
}
//次のポイントを1つ進める
else
{
rb.MovePosition(movePoint[nextPoint].transform.position);
++nowPoint;
//現在地が配列の最後だった場合
if (nowPoint + 1 >= movePoint.Length)
{
returnPoint = true;
}
}
}
//折返し進行
else
{
int nextPoint = nowPoint - 1;
//目標ポイントとの誤差がわずかになるまで移動
if (Vector2.Distance(transform.position, movePoint[nextPoint].transform.position) > 0.1f)
{
//現在地から次のポイントへのベクトルを作成
Vector2 toVector = Vector2.MoveTowards(transform.position, movePoint[nextPoint].transform.position, speed * Time.deltaTime);
//次のポイントへ移動
rb.MovePosition(toVector);
}
//次のポイントを1つ戻す
else
{
rb.MovePosition(movePoint[nextPoint].transform.position);
--nowPoint;
//現在地が配列の最初だった場合
if (nowPoint <= 0)
{
returnPoint = false;
}
}
}
myVelocity = (rb.position - oldPos) / Time.deltaTime;
oldPos = rb.position;
}
}
}
↓この2つの変数を追加して速度を求めます。
private Vector2 oldPos = Vector2.zero;
private Vector2 myVelocity = Vector2.zero;
oldPosに前のフレームの位置を保存します。そして現在の位置から引く事で進んだ距離が出せます。
myVelocity = (rb.position - oldPos) / Time.deltaTime;
速さ = 道のり / 時間 なので、Time.deltaTimeで割ってあげれば床の速さが出ます。
ここでタグを追加しましょう。Add Tagから
MoveFloorというのを追加します。
そして、動く床のタグをMoveFloorにします。
今度は接地判定にこのMoveFloorも地面だと認識させます。そして、プレイヤーの足元で床に触れた場合、その床の速度をとってくるようにします。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GroundCheck : MonoBehaviour { [Header("エフェクトがついた床を判定するか")]public bool checkPlatformGroud = true;private string groundTag = "Ground";
private string platformTag = "GroundPlatform";
private string moveFloorTag = "MoveFloor";
private bool isGround = false;
private bool isGroundEnter, isGroundStay, isGroundExit;
//接地判定を返すメソッド
public bool IsGround()
{
if (isGroundEnter || isGroundStay)
{
isGround = true;
}
else if (isGroundExit)
{
isGround = false;
}
isGroundEnter = false;
isGroundStay = false;
isGroundExit = false;
return isGround;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundEnter = true;
}
else if(checkPlatformGroud && (collision.tag == platformTag || collision.tag == moveFloorTag))
{
isGroundEnter = true;
}
}
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundStay = true;
}
else if (checkPlatformGroud && (collision.tag == platformTag || collision.tag == moveFloorTag))
{
isGroundStay = true;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundExit = true;
}
else if (checkPlatformGroud && (collision.tag == platformTag || collision.tag == moveFloorTag))
{
isGroundExit = true;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
#region//インスペクターで設定する
[Header("移動速度")] public float speed;
[Header("重力")] public float gravity;
[Header("ジャンプ速度")] public float jumpSpeed;
[Header("ジャンプする高さ")] public float jumpHeight;
[Header("ジャンプする長さ")] public float jumpLimitTime;
[Header("接地判定")] public GroundCheck ground;
[Header("天井判定")] public GroundCheck head;
[Header("ダッシュの速さ表現")] public AnimationCurve dashCurve;
[Header("ジャンプの速さ表現")] public AnimationCurve jumpCurve;
[Header("踏みつけ判定の高さの割合(%)")] public float stepOnRate;
[Header("ジャンプする時に鳴らすSE")] public AudioClip jumpSE;
[Header("やられた鳴らすSE")] public AudioClip downSE;
[Header("コンティニュー時に鳴らすSE")] public AudioClip continueSE;
#endregion
#region//プライベート変数
private Animator anim = null;
private Rigidbody2D rb = null;
private CapsuleCollider2D capcol = null;
private SpriteRenderer sr = null;
private MoveObject moveObj = null;
private bool isGround = false;
private bool isJump = false;
private bool isHead = false;
private bool isRun = false;
private bool isDown = false;
private bool isOtherJump = false;
private bool isContinue = false;
private bool nonDownAnim = false;
private float jumpPos = 0.0f;
private float otherJumpHeight = 0.0f;
private float dashTime = 0.0f;
private float jumpTime = 0.0f;
private float beforeKey = 0.0f;
private float continueTime = 0.0f;
private float blinkTime = 0.0f;
private string enemyTag = "Enemy";
private string deadAreaTag = "DeadArea";
private string hitAreaTag = "HitArea";
private string moveFloorTag = "MoveFloor";
#endregion
void Start()
{
//コンポーネントのインスタンスを捕まえる
anim = GetComponent<Animator>();
rb = GetComponent<Rigidbody2D>();
capcol = GetComponent<CapsuleCollider2D>();
sr = GetComponent<SpriteRenderer>();
}
private void Update()
{
if (isContinue)
{
//明滅 ついている時に戻る
if (blinkTime > 0.2f)
{
sr.enabled = true;
blinkTime = 0.0f;
}
//明滅 消えているとき
else if (blinkTime > 0.1f)
{
sr.enabled = false;
}
//明滅 ついているとき
else
{
sr.enabled = true;
}
//1秒たったら明滅終わり
if (continueTime > 1.0f)
{
isContinue = false;
blinkTime = 0.0f;
continueTime = 0.0f;
sr.enabled = true;
}
else
{
blinkTime += Time.deltaTime;
continueTime += Time.deltaTime;
}
}
}
void FixedUpdate()
{
if (!isDown && !GManager.instance.isGameOver)
{
//接地判定を得る
isGround = ground.IsGround();
isHead = head.IsGround();
//各種座標軸の速度を求める
float xSpeed = GetXSpeed();
float ySpeed = GetYSpeed();
//アニメーションを適用
SetAnimation();
//移動速度を設定
Vector2 addVelocity = Vector2.zero;
if (moveObj != null)
{
addVelocity = moveObj.GetVelocity();
}
rb.velocity = new Vector2(xSpeed, ySpeed) + addVelocity;
}
else
{
rb.velocity = new Vector2(0, -gravity);
}
}
/// <summary>
/// Y成分で必要な計算をし、速度を返す。
/// </summary>
/// <returns>Y軸の速さ</returns>
private float GetYSpeed()
{
float verticalKey = Input.GetAxis("Vertical");
float ySpeed = -gravity;
//何かを踏んだ際のジャンプ
if (isOtherJump)
{
//現在の高さが飛べる高さより下か
bool canHeight = jumpPos + otherJumpHeight > transform.position.y;
//ジャンプ時間が長くなりすぎてないか
bool canTime = jumpLimitTime > jumpTime;
if (canHeight && canTime && !isHead)
{
ySpeed = jumpSpeed;
jumpTime += Time.deltaTime;
}
else
{
isOtherJump = false;
jumpTime = 0.0f;
}
}
//地面にいるとき
else if (isGround)
{
if (verticalKey > 0)
{
if (!isJump)
{
GManager.instance.PlaySE(jumpSE);
}
ySpeed = jumpSpeed;
jumpPos = transform.position.y; //ジャンプした位置を記録する
isJump = true;
jumpTime = 0.0f;
}
else
{
isJump = false;
}
}
//ジャンプ中
else if (isJump)
{
//上方向キーを押しているか
bool pushUpKey = verticalKey > 0;
//現在の高さが飛べる高さより下か
bool canHeight = jumpPos + jumpHeight > transform.position.y;
//ジャンプ時間が長くなりすぎてないか
bool canTime = jumpLimitTime > jumpTime;
if (pushUpKey && canHeight && canTime && !isHead)
{
ySpeed = jumpSpeed;
jumpTime += Time.deltaTime;
}
else
{
isJump = false;
jumpTime = 0.0f;
}
}
if (isJump || isOtherJump)
{
ySpeed *= jumpCurve.Evaluate(jumpTime);
}
return ySpeed;
}
/// <summary>
/// X成分で必要な計算をし、速度を返す。
/// </summary>
/// <returns>X軸の速さ</returns>
private float GetXSpeed()
{
float horizontalKey = Input.GetAxis("Horizontal");
float xSpeed = 0.0f;
if (horizontalKey > 0)
{
transform.localScale = new Vector3(1, 1, 1);
isRun = true;
dashTime += Time.deltaTime;
xSpeed = speed;
}
else if (horizontalKey < 0)
{
transform.localScale = new Vector3(-1, 1, 1);
isRun = true;
dashTime += Time.deltaTime;
xSpeed = -speed;
}
else
{
isRun = false;
xSpeed = 0.0f;
dashTime = 0.0f;
}
//前回の入力からダッシュの反転を判断して速度を変える
if (horizontalKey > 0 && beforeKey < 0)
{
dashTime = 0.0f;
}
else if (horizontalKey < 0 && beforeKey > 0)
{
dashTime = 0.0f;
}
xSpeed *= dashCurve.Evaluate(dashTime);
beforeKey = horizontalKey;
return xSpeed;
}
/// <summary>
/// アニメーションを設定する
/// </summary>
private void SetAnimation()
{
anim.SetBool("jump", isJump || isOtherJump);
anim.SetBool("ground", isGround);
anim.SetBool("run", isRun);
}
/// <summary>
/// コンティニュー待機状態か
/// </summary>
/// <returns></returns>
public bool IsContinueWaiting()
{
if (GManager.instance.isGameOver)
{
return false;
}
else
{
return IsDownAnimEnd() || nonDownAnim;
}
}
//ダウンアニメーションが完了しているかどうか
private bool IsDownAnimEnd()
{
if (isDown && anim != null)
{
AnimatorStateInfo currentState = anim.GetCurrentAnimatorStateInfo(0);
if (currentState.IsName("player_down"))
{
if (currentState.normalizedTime >= 1)
{
return true;
}
}
}
return false;
}
/// <summary>
/// コンティニューする
/// </summary>
public void ContinuePlayer()
{
GManager.instance.PlaySE(continueSE);
isDown = false;
anim.Play("player_stand");
isJump = false;
isOtherJump = false;
isRun = false;
isContinue = true;
nonDownAnim = false;
}
//やられた時の処理
private void ReceiveDamage(bool downAnim)
{
if (isDown)
{
return;
}
else
{
if (downAnim)
{
anim.Play("player_down");
}
else
{
nonDownAnim = true;
}
isDown = true;
GManager.instance.PlaySE(downSE);
GManager.instance.SubHeartNum();
}
}
#region//接触判定
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.collider.tag == enemyTag)
{
//踏みつけ判定になる高さ
float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
//踏みつけ判定のワールド座標
float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
foreach (ContactPoint2D p in collision.contacts)
{
if (p.point.y < judgePos)
{
ObjectCollision o = collision.gameObject.GetComponent<ObjectCollision>();
if (o != null)
{
otherJumpHeight = o.boundHeight; //踏んづけたものから跳ねる高さを取得する
o.playerStepOn = true; //踏んづけたものに対して踏んづけた事を通知する
jumpPos = transform.position.y; //ジャンプした位置を記録する
isOtherJump = true;
isJump = false;
jumpTime = 0.0f;
}
else
{
Debug.Log("ObjectCollisionが付いてないよ!");
}
}
else
{
ReceiveDamage(true);
break;
}
}
}
//動く床
else if (collision.collider.tag == moveFloorTag)
{
//踏みつけ判定になる高さ
float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
//踏みつけ判定のワールド座標
float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
foreach (ContactPoint2D p in collision.contacts)
{
//動く床に乗っている
if (p.point.y < judgePos)
{
moveObj = collision.gameObject.GetComponent<MoveObject>();
}
}
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if (collision.collider.tag == moveFloorTag)
{
//動く床から離れた
moveObj = null;
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == deadAreaTag)
{
ReceiveDamage(false);
}
else if (collision.tag == hitAreaTag)
{
ReceiveDamage(true);
}
}
#endregion
}
接触判定でタグが動く床だった場合、足元で接触しているかどうかを見て、その床についているMoveObjectをとってくるようにしています。
//動く床
else if (collision.collider.tag == moveFloorTag)
{
//踏みつけ判定になる高さ
float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
//踏みつけ判定のワールド座標
float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
foreach (ContactPoint2D p in collision.contacts)
{
//動く床に乗っている
if (p.point.y < judgePos)
{
moveObj = collision.gameObject.GetComponent<MoveObject>();
}
}
}
そして、取得した床から速度を持ってきて、プレイヤーの速度に加算してあげます。
//移動速度を設定
Vector2 addVelocity = Vector2.zero;
if(moveObj != null)
{
addVelocity = moveObj.GetVelocity();
}
rb.velocity = new Vector2(xSpeed, ySpeed) + addVelocity;
そして床から離れたらnullを入れてあげる事で加算する速度を無しにします。
private void OnCollisionExit2D(Collision2D collision)
{
if (collision.collider.tag == moveFloorTag)
{
//動く床から離れた
moveObj = null;
}
}
これで動く床にちゃんと計算された形で乗る事ができるようになりました。
こういった物理法則を無視するオブジェクトにちゃんと対応できるようにするためにAddForceなどを使わずvelocityで計算していたわけですね。
<落ちる床の作り方>
落ちる床を作成する際にする工夫
続いて落ちる床を実装していきます。
落ちる床を作る際にちょっとした工夫を施します。
↑のように2つのゲームオブジェクトに分けます。
このようにオブジェクトを分けるのは演出をする為です。
いきなりパッと落下されるとプレイヤーも困ると思うので、ちょっとふるふると震動してから落ちるようにしたいと思います。
完成イメージとしては↓のような感じです。
この時、コライダーまで揺れてしまうと、プレイヤーも震動してしまうので、これを回避するために、コライダーとレンダラーを分けています。
ではこれらのゲームオブジェクトの設定を順に解説していきます。
まず、スプライトレンダラーで落ちる床の絵だけを表示するオブジェクトを作成します。
見た目だけ表示したいだけなのでSprite Rendererで絵を表示させて終わりです。
これを適当にカラのゲームオブジェクトをつくり、子オブジェクトにします。
次に↑のカラのゲームオブジェクトにコライダーの判定をつけていきます。
動く床の時と同じようにBox Collider 2DとPlatform Effector 2DとRigidbody2Dをつけ、Kinematicにします。
下準備の部分は動く床と一緒です。ただしSprite Rendererだけ無い状態です。
そして、ここに、以前作成したプレイヤーが踏んだことを通知するスクリプトをつけます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectCollision : MonoBehaviour
{
[Header("これを踏んだ時のプレイヤーが跳ねる高さ")] public float boundHeight;
/// <summary>
/// このオブジェクトをプレイヤーが踏んだかどうか
/// </summary>
[HideInInspector] public bool playerStepOn;
}
そして、↑のスクリプトから踏まれたことを検知して床を落下させるようにします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FallDownFloor : MonoBehaviour
{
[Header("スプライトがあるオブジェクト")] public GameObject spriteObj;
[Header("振動幅")] public float vibrationWidth = 0.05f;
[Header("振動速度")] public float vibrationSpeed = 30.0f;
[Header("落ちるまでの時間")] public float fallTime = 1.0f;
[Header("落ちていく速度")] public float fallSpeed = 10.0f;
[Header("落ちてから戻ってくる時間")] public float returnTime = 5.0f;
private bool isOn;
private bool isFall;
private bool isReturn;
private Vector3 spriteDefaultPos;
private Vector3 floorDefaultPos;
private Vector2 fallVelocity;
private BoxCollider2D col;
private Rigidbody2D rb;
private ObjectCollision oc;
private SpriteRenderer sr;
private float timer = 0.0f;
private float fallingTimer = 0.0f;
private float returnTimer = 0.0f;
private float blinkTimer = 0.0f;
private void Start()
{
//初期設定
col = GetComponent<BoxCollider2D>();
rb = GetComponent<Rigidbody2D>();
oc = GetComponent<ObjectCollision>();
if (spriteObj != null && oc != null && col != null && rb != null)
{
spriteDefaultPos = spriteObj.transform.position;
fallVelocity = new Vector2(0, -fallSpeed);
floorDefaultPos = gameObject.transform.position;
sr = spriteObj.GetComponent<SpriteRenderer>();
if (sr == null)
{
Debug.Log("fallDownFloor インスペクターに設定し忘れがあります");
Destroy(this);
}
}
else
{
Debug.Log("fallDownFloor インスペクターに設定し忘れがあります");
Destroy(this);
}
}
private void Update()
{
//プレイヤーが1回でも乗ったらフラグをオンに
if (oc.playerStepOn)
{
isOn = true;
oc.playerStepOn = false;
}
//プレイヤーがのってから落ちるまでの間
if (isOn && !isFall)
{
//震動する
spriteObj.transform.position = spriteDefaultPos + new Vector3(Mathf.Sin(timer * vibrationSpeed) * vibrationWidth, 0, 0);
//一定時間たったら落ちる
if (timer > fallTime)
{
isFall = true;
}
timer += Time.deltaTime;
}
//一定時間たつと明滅して戻ってくる
if (isReturn)
{
//明滅 ついている時に戻る
if (blinkTimer > 0.2f)
{
sr.enabled = true;
blinkTimer = 0.0f;
}
//明滅 消えているとき
else if (blinkTimer > 0.1f)
{
sr.enabled = false;
}
//明滅 ついているとき
else
{
sr.enabled = true;
}
//1秒たったら明滅終わり
if (returnTimer > 1.0f)
{
isReturn = false;
blinkTimer = 0f;
returnTimer = 0f;
sr.enabled = true;
}
else
{
blinkTimer += Time.deltaTime;
returnTimer += Time.deltaTime;
}
}
}
private void FixedUpdate()
{
//落下中
if (isFall)
{
rb.velocity = fallVelocity;
//一定時間たつと元の位置に戻る
if (fallingTimer > returnTime)
{
isReturn = true;
transform.position = floorDefaultPos;
rb.velocity = Vector2.zero;
isFall = false;
timer = 0.0f;
fallingTimer = 0.0f;
}
else
{
fallingTimer += Time.deltaTime;
isOn = false;
}
}
}
}
基本的にコメントを見ていただけるとほとんどわかると思いますが、プレイヤーが乗ったという判定を受け取ったら横に震動するようにします。
//震動する spriteObj.transform.position = spriteDefaultPos + new Vector3(Mathf.Sin(timer * vibrationSpeed) * vibrationWidth,0,0);
ここで震動させています。Rigidbody2Dを使っていないのは、絵だけに分離したため物理的な挙動で動かす必要が無い為です。
Mathf.Sin(timer * vibrationSpeed)
これで震動させています。三角関数のSinは角度によって-1〜1の範囲をくるくる回る性質があります。
正弦波でググっていただけるとわかると思いますが、角度に時間を入れてあげることによって-1〜1の間で震動しているように見えます。
これに自分で設定した震動幅をかけてあげることによって震動を実装する事ができます。
一定時間震動したら、今度は落下します。
rb.velocity = fallVelocity;
落下はコライダーが関係するのでRigidbody2Dで動かします。FixedUpdateに書いている点にも注意が必要です。
コンティニューや戻ってしまう場合も考えて、落下してしばらくしたら位置を戻してあげなければいけません。
パッと戻られても困るので、プレイヤーの時と同じく明滅するようにします。
そして、新たにFallFloorというタグを作り、コライダーがついている方のゲームオブジェクトのタグを変更します。
そして、プレイヤーが落下床を踏んだ時、落下床に対して踏んづけた通知する処理を追加します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GroundCheck : MonoBehaviour
{
[Header("エフェクトがついた床を判定するか")] public bool checkPlatformGroud = true;
private string groundTag = "Ground";
private string platformTag = "GroundPlatform";
private string moveFloorTag = "MoveFloor";
private string fallFloorTag = "FallFloor";
private bool isGround = false;
private bool isGroundEnter, isGroundStay, isGroundExit;
//接地判定を返すメソッド
public bool IsGround()
{
if (isGroundEnter || isGroundStay)
{
isGround = true;
}
else if (isGroundExit)
{
isGround = false;
}
isGroundEnter = false;
isGroundStay = false;
isGroundExit = false;
return isGround;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundEnter = true;
}
else if (checkPlatformGroud && (collision.tag == platformTag || collision.tag == moveFloorTag || collision.tag == fallFloorTag))
{
isGroundEnter = true;
}
}
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundStay = true;
}
else if (checkPlatformGroud && (collision.tag == platformTag || collision.tag == moveFloorTag || collision.tag == fallFloorTag))
{
isGroundStay = true;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.tag == groundTag)
{
isGroundExit = true;
}
else if (checkPlatformGroud && (collision.tag == platformTag || collision.tag == moveFloorTag || collision.tag == fallFloorTag))
{
isGroundExit = true;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
#region//インスペクターで設定する
[Header("移動速度")] public float speed;
[Header("重力")] public float gravity;
[Header("ジャンプ速度")] public float jumpSpeed;
[Header("ジャンプする高さ")] public float jumpHeight;
[Header("ジャンプする長さ")] public float jumpLimitTime;
[Header("接地判定")] public GroundCheck ground;
[Header("天井判定")] public GroundCheck head;
[Header("ダッシュの速さ表現")] public AnimationCurve dashCurve;
[Header("ジャンプの速さ表現")] public AnimationCurve jumpCurve;
[Header("踏みつけ判定の高さの割合(%)")] public float stepOnRate;
[Header("ジャンプする時に鳴らすSE")] public AudioClip jumpSE;
[Header("やられた鳴らすSE")] public AudioClip downSE;
[Header("コンティニュー時に鳴らすSE")] public AudioClip continueSE;
#endregion
#region//プライベート変数
private Animator anim = null;
private Rigidbody2D rb = null;
private CapsuleCollider2D capcol = null;
private SpriteRenderer sr = null;
private MoveObject moveObj = null;
private bool isGround = false;
private bool isJump = false;
private bool isHead = false;
private bool isRun = false;
private bool isDown = false;
private bool isOtherJump = false;
private bool isContinue = false;
private bool nonDownAnim = false;
private float jumpPos = 0.0f;
private float otherJumpHeight = 0.0f;
private float dashTime = 0.0f;
private float jumpTime = 0.0f;
private float beforeKey = 0.0f;
private float continueTime = 0.0f;
private float blinkTime = 0.0f;
private string enemyTag = "Enemy";
private string deadAreaTag = "DeadArea";
private string hitAreaTag = "HitArea";
private string moveFloorTag = "MoveFloor";
private string fallFloorTag = "FallFloor";
#endregion
void Start()
{
//コンポーネントのインスタンスを捕まえる
anim = GetComponent<Animator>();
rb = GetComponent<Rigidbody2D>();
capcol = GetComponent<CapsuleCollider2D>();
sr = GetComponent<SpriteRenderer>();
}
private void Update()
{
if (isContinue)
{
//明滅 ついている時に戻る
if (blinkTime > 0.2f)
{
sr.enabled = true;
blinkTime = 0.0f;
}
//明滅 消えているとき
else if (blinkTime > 0.1f)
{
sr.enabled = false;
}
//明滅 ついているとき
else
{
sr.enabled = true;
}
//1秒たったら明滅終わり
if (continueTime > 1.0f)
{
isContinue = false;
blinkTime = 0.0f;
continueTime = 0.0f;
sr.enabled = true;
}
else
{
blinkTime += Time.deltaTime;
continueTime += Time.deltaTime;
}
}
}
void FixedUpdate()
{
if (!isDown && !GManager.instance.isGameOver)
{
//接地判定を得る
isGround = ground.IsGround();
isHead = head.IsGround();
//各種座標軸の速度を求める
float xSpeed = GetXSpeed();
float ySpeed = GetYSpeed();
//アニメーションを適用
SetAnimation();
//移動速度を設定
Vector2 addVelocity = Vector2.zero;
if (moveObj != null)
{
addVelocity = moveObj.GetVelocity();
}
rb.velocity = new Vector2(xSpeed, ySpeed) + addVelocity;
}
else
{
rb.velocity = new Vector2(0, -gravity);
}
}
/// <summary>
/// Y成分で必要な計算をし、速度を返す。
/// </summary>
/// <returns>Y軸の速さ</returns>
private float GetYSpeed()
{
float verticalKey = Input.GetAxis("Vertical");
float ySpeed = -gravity;
//何かを踏んだ際のジャンプ
if (isOtherJump)
{
//現在の高さが飛べる高さより下か
bool canHeight = jumpPos + otherJumpHeight > transform.position.y;
//ジャンプ時間が長くなりすぎてないか
bool canTime = jumpLimitTime > jumpTime;
if (canHeight && canTime && !isHead)
{
ySpeed = jumpSpeed;
jumpTime += Time.deltaTime;
}
else
{
isOtherJump = false;
jumpTime = 0.0f;
}
}
//地面にいるとき
else if (isGround)
{
if (verticalKey > 0)
{
if (!isJump)
{
GManager.instance.PlaySE(jumpSE);
}
ySpeed = jumpSpeed;
jumpPos = transform.position.y; //ジャンプした位置を記録する
isJump = true;
jumpTime = 0.0f;
}
else
{
isJump = false;
}
}
//ジャンプ中
else if (isJump)
{
//上方向キーを押しているか
bool pushUpKey = verticalKey > 0;
//現在の高さが飛べる高さより下か
bool canHeight = jumpPos + jumpHeight > transform.position.y;
//ジャンプ時間が長くなりすぎてないか
bool canTime = jumpLimitTime > jumpTime;
if (pushUpKey && canHeight && canTime && !isHead)
{
ySpeed = jumpSpeed;
jumpTime += Time.deltaTime;
}
else
{
isJump = false;
jumpTime = 0.0f;
}
}
if (isJump || isOtherJump)
{
ySpeed *= jumpCurve.Evaluate(jumpTime);
}
return ySpeed;
}
/// <summary>
/// X成分で必要な計算をし、速度を返す。
/// </summary>
/// <returns>X軸の速さ</returns>
private float GetXSpeed()
{
float horizontalKey = Input.GetAxis("Horizontal");
float xSpeed = 0.0f;
if (horizontalKey > 0)
{
transform.localScale = new Vector3(1, 1, 1);
isRun = true;
dashTime += Time.deltaTime;
xSpeed = speed;
}
else if (horizontalKey < 0)
{
transform.localScale = new Vector3(-1, 1, 1);
isRun = true;
dashTime += Time.deltaTime;
xSpeed = -speed;
}
else
{
isRun = false;
xSpeed = 0.0f;
dashTime = 0.0f;
}
//前回の入力からダッシュの反転を判断して速度を変える
if (horizontalKey > 0 && beforeKey < 0)
{
dashTime = 0.0f;
}
else if (horizontalKey < 0 && beforeKey > 0)
{
dashTime = 0.0f;
}
xSpeed *= dashCurve.Evaluate(dashTime);
beforeKey = horizontalKey;
return xSpeed;
}
/// <summary>
/// アニメーションを設定する
/// </summary>
private void SetAnimation()
{
anim.SetBool("jump", isJump || isOtherJump);
anim.SetBool("ground", isGround);
anim.SetBool("run", isRun);
}
/// <summary>
/// コンティニュー待機状態か
/// </summary>
/// <returns></returns>
public bool IsContinueWaiting()
{
if (GManager.instance.isGameOver)
{
return false;
}
else
{
return IsDownAnimEnd() || nonDownAnim;
}
}
//ダウンアニメーションが完了しているかどうか
private bool IsDownAnimEnd()
{
if (isDown && anim != null)
{
AnimatorStateInfo currentState = anim.GetCurrentAnimatorStateInfo(0);
if (currentState.IsName("player_down"))
{
if (currentState.normalizedTime >= 1)
{
return true;
}
}
}
return false;
}
/// <summary>
/// コンティニューする
/// </summary>
public void ContinuePlayer()
{
GManager.instance.PlaySE(continueSE);
isDown = false;
anim.Play("player_stand");
isJump = false;
isOtherJump = false;
isRun = false;
isContinue = true;
nonDownAnim = false;
}
//やられた時の処理
private void ReceiveDamage(bool downAnim)
{
if (isDown)
{
return;
}
else
{
if (downAnim)
{
anim.Play("player_down");
}
else
{
nonDownAnim = true;
}
isDown = true;
GManager.instance.PlaySE(downSE);
GManager.instance.SubHeartNum();
}
}
#region//接触判定
private void OnCollisionEnter2D(Collision2D collision)
{
bool enemy = (collision.collider.tag == enemyTag);
bool moveFloor = (collision.collider.tag == moveFloorTag);
bool fallFloor = (collision.collider.tag == fallFloorTag);
if (enemy || moveFloor || fallFloor)
{
//踏みつけ判定になる高さ
float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
//踏みつけ判定のワールド座標
float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
foreach (ContactPoint2D p in collision.contacts)
{
if (p.point.y < judgePos)
{
if (enemy || fallFloor)
{
ObjectCollision o = collision.gameObject.GetComponent<ObjectCollision>();
if (o != null)
{
if (enemy)
{
otherJumpHeight = o.boundHeight; //踏んづけたものから跳ねる高さを取得する
o.playerStepOn = true; //踏んづけたものに対して踏んづけた事を通知する
jumpPos = transform.position.y; //ジャンプした位置を記録する
isOtherJump = true;
isJump = false;
jumpTime = 0.0f;
}
else if(fallFloor)
{
o.playerStepOn = true;
}
}
else
{
Debug.Log("ObjectCollisionが付いてないよ!");
}
}
else if(moveFloor)
{
moveObj = collision.gameObject.GetComponent<MoveObject>();
}
}
else
{
if (enemy)
{
ReceiveDamage(true);
break;
}
}
}
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if (collision.collider.tag == moveFloorTag)
{
//動く床から離れた
moveObj = null;
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == deadAreaTag)
{
ReceiveDamage(false);
}
else if (collision.tag == hitAreaTag)
{
ReceiveDamage(true);
}
}
#endregion
}
動く床の時と同じように踏んづけた判定を追加していくのですが、ちょっと多くなってきたのでまとめています。
private void OnCollisionEnter2D(Collision2D collision)
{
bool enemy = (collision.collider.tag == enemyTag);
bool moveFloor = (collision.collider.tag == moveFloorTag);
bool fallFloor = (collision.collider.tag == fallFloorTag);
if (enemy || moveFloor || fallFloor)
{
//踏みつけ判定になる高さ
float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
//踏みつけ判定のワールド座標
float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
foreach (ContactPoint2D p in collision.contacts)
{
if (p.point.y < judgePos)
{
if (enemy || fallFloor)
{
ObjectCollision o = collision.gameObject.GetComponent<ObjectCollision>();
if (o != null)
{
if (enemy)
{
otherJumpHeight = o.boundHeight; //踏んづけたものから跳ねる高さを取得する
o.playerStepOn = true; //踏んづけたものに対して踏んづけた事を通知する
jumpPos = transform.position.y; //ジャンプした位置を記録する
isOtherJump = true;
isJump = false;
jumpTime = 0.0f;
}
else if(fallFloor)
{
o.playerStepOn = true;
}
}
else
{
Debug.Log("ObjectCollisionが付いてないよ!");
}
}
else if(moveFloor)
{
moveObj = collision.gameObject.GetComponent<MoveObject>();
}
}
else
{
if (enemy)
{
ReceiveDamage(true);
break;
}
}
}
}
}
あとはインスペクターの値を設定すればOKです。
はい、これで落ちる床を実装する事ができました。
何かうまくいかない事があった場合は↓の記事を参考にしてみてください
最低限↓の動画の要件を満たしていない質問は受けかねるので、ご理解ください。
また、筆者も間違えることはありますので、何か間違っている点などありましたら、動画コメント欄にでも書いていただけるとありがたいです。