【Unity】衝突時に角度を固定する

ハンマーを的にぶつけて刺すゲームを想定(物理エンジンあり)。

何もしない場合は、こんな刺さり方もします。


ハンマーの柄が的に刺さるのがOKなら問題ないです。

的は板とかコンクリートブロックみたいな硬いものを想定しているので、ハンマーの柄が的に刺さることを想定していない場合はNGになります。

NGの場合はそれっぽく見せる必要があります。

それっぽく見せる方法

そもそも論として、
的に当たったときに、当たる角度で違和感が出てしまうものを投げることに問題があります。
的当てゲームを素早く作りたいなら、どの角度でも刺さる手裏剣や、鋭利なブーメランなどにするか、ナイフや弾丸などを回転させずに投げるべきです。

素材提供「いらすとや

また、ミニゲームやスーパーカジュアルでこういう細かい事にこだわるのはお勧めしません。
ゲーム全体の完成度を上げる方がはるかに重要です。
1~2時間悩んでダメなら諦めた方が良いです。

それっぽく見せる方法
  1. 当たったときにそれっぽい角度に変更
  2. それっぽい角度でしか当たらない
  3. コライダーの位置とサイズを調節する
1. 当たったときにそれっぽい角度に変更
問題点


無理やり角度を変えるので、こんな風に、一瞬元の角度が見えてしまうことがあります。

解決方法
  1. 端っこで当たらないようにする
  2. 変更する角度を元の角度に近づける
  3. 投げるときの角度を変更する
1. 端っこで当たらないようにする

的の真ん中にめりこんだときは、ハンマーがあまり見えないので気づきにくく、的の端に当たったときに目立ちます。
であれば、こういうギリギリの当たり方をした場合は当たっていないと判定させることで回避できるかも知れません(的とハンマーの距離を計算すれば判定できます)。
ただし、当たるはずの状況でも当たらなくなるので、当てる難易度が上がります。

2. 変更する角度を元の角度に近づける

当たったときの角度と、変更する角度との差を減らすことで違和感は減るかも知れません。
変更する角度を、許容できる範囲で元の角度に近づけるということです。
違和感が減るだけで、ごまかしている点は変わりません。

3. 投げるときの角度を変更する

的に当たったときに違和感が出ない角度で投げるようにします。
ただ、投げるときに一瞬ハンマーの角度が変わるので、的に当たったときの違和感はなくなりますが、投げるときの違和感が出ます。

それから、当てる的が1つだけなら良いのですが、複数ある場合は簡単に解決できません。

どの的に当たりそうか?当たるならどの角度で当たるか? を事前に計算する必要が出てきます。
物理エンジンが内部でやっている計算をシミュレーションしないといけないので、かなりハードルが高いです。
衝突判定以外は物理エンジンを使わないようにすれば、計算しやすくなります。
私だったら、ミニゲームやスーパーカジュアルでは、そこまでやらないです。

2. それっぽい角度でしか当たらない

それっぽい角度でないときは弾かれるようにしますが、難易度が跳ね上がります。

3. コライダーの位置とサイズを調節する

的の方のコライダーを調節します。

こんな風に、コライダーを2つに分けます。

  1. 的に当たったことを判定するコライダー
  2. ハンマーをはじくためのコライダー
1. 的に当たったことを判定するコライダー

当たったかどうかを判定するためのコライダー(トリガー用)は、的の前面に配置します。
ハンマーが飛ぶスピードに応じて、範囲を広げます。
そうしないと、ハンマーが的の後ろに行ったときにトリガーすることがあります。

2. ハンマーをはじくためのコライダー

ハンマーをはじかない場合は必要ないので無効にしておきます。
ある程度めりこませたい場合は、めり込みを許容する分だけコライダーのサイズを小さくします。

Rigidbody について

的に当たったときにハンマーをはじく場合、両方に Rigidbody が必要です。
めり込みを気にしない場合は、どちらか片方に Rigidbody があればトリガーします。

ハンマーの Rigidbody 設定

的の Rigidbody 設定

キモは、「衝突判定」と「Constraints」の設定です。

衝突判定を Continuous Speculative にしているのは、ハンマーが高速で回転しながら飛ぶからです。
とはいえ、的は回転しないので、「連続的かつ動的(Continuous Dynamic)」でも問題なさそうです。
衝突判定については、以下の記事の説明が完璧すぎて、とめどなく溢れ出す感謝の念が絶えません。

Constraints は Rigidbody 同士が衝突したときに、お互いに押し合って位置が変わるか?角度が変わるか?を設定します。

的の場合、位置が変わるのはX座標だけなので、YとZにチェックして、Y座標とZ座標の位置が変わらないようにしています。
全ての座標軸で回転させる必要がないので、XYZ全ての回転を固定しています。

ハンマーの場合、投げたときに変化するのはZ座標だけですが、的にはじかれる場合はX座標も変化するので、Y座標だけ固定しています。
ハンマーはY軸回転だけするので、回転はY以外を固定しています。

GameObject の構造とソースコード

角度が悪いとはじかれるバージョンのソースコードです。

ハンマーの構造

ハンマーは以下の構造になってます。

親 { Rigidbody, ↓のスクリプト }
 └ 子 { MeshFilter, MeshRenderer, MeshCollider, BoxCollider }

ハンマーに AddComponent するスクリプト

using UnityEngine;
using UnityEngine.UI;

public class Projectile : MonoBehaviour {
  public float rotationSpeed = -500.0f;
  public float deadLine = 12;
  public float power = 12;
  public float waitSecondsWhenFailed = 1.0f;
  public Button button = null;

  Rigidbody rigid;
  Vector3 initialPosition;
  float count_seconds = 0;
  bool IsThrown { get; set; }
  bool IsHit { get; set; }
  bool IsRepelled { get; set; }

  void Start() {
    rigid = GetComponent<Rigidbody>();
    IsThrown = IsHit = IsRepelled = false;
    initialPosition = transform.position;
    rigid.maxAngularVelocity = 20; 
  }

  void FixedUpdate() {
    if (IsRepelled) {
      //  はじかれたら少し待ってから最初の状態に戻す
      count_seconds += Time.deltaTime;
      if (count_seconds >= waitSecondsWhenFailed) {
        count_seconds = 0;
        Restart();
      }
    }
    else if (IsHit) {
      return;
    }
    else {
      rigid.AddTorque(new Vector3(0, rotationSpeed, 0), ForceMode.Acceleration);

      //  的に当たらなかったので初期位置に戻して再開させる
      if (transform.position.z > deadLine) { Restart(); }
    }
  }

  void Restart() {
    Stop(0);
    OnButtonPushed();
    IsThrown = IsHit = IsRepelled = false;
  }

  public void OnButtonPushed() {
    if(IsThrown) { return; }
    if (IsHit) {
      transform.position = initialPosition;
      transform.GetChild(0).GetComponent<MeshCollider>().enabled = true;  //コライダーを無効にしていたので有効に戻す
      IsHit = false;
    }
    else {
      rigid.AddForce(0, 0, power, ForceMode.Impulse);
      button.interactable = false;
      IsThrown = true;
    }
    transform.GetChild(0).GetComponent<BoxCollider>().enabled = true;  //コライダーを無効にしていたので有効に戻す
  }

  public void Repel() {
    rigid.AddExplosionForce(10000, transform.position, 0 * Mathf.Deg2Rad);
    transform.GetChild(0).GetComponent<BoxCollider>().enabled = false;  //  再度的に当たらないようにBoxColliderを切る
    IsRepelled = true;
  }

  public void Stop(float z) {
    IsHit = true;

    {
      var p = transform.position;
      p.z = z;
      transform.position = p;
    }

    transform.rotation = Quaternion.identity;
    rigid.velocity = Vector3.zero;
    rigid.angularVelocity = Vector3.zero;

    //  的のコライダーに弾かれるので、こっちのコライダーを無効にする
    transform.GetChild(0).GetComponent<MeshCollider>().enabled = false;

    button.interactable = true;
    IsThrown = false;
  }
}
的の構造

的は以下の構造になってます。

親 { Cube, Rigidbody, BoxCollider, BoxCollider, ↓のスクリプト }
 └ 子 = ハンマーを刺す位置を取得するための GameObject


子は、ハンマーが的に当たったとき、ハンマーの位置をそれっぽい位置に固定させるために使います。
子のワールド座標を取得し、ハンマーのZ位置のみ、取得したZ位置で上書きします。
そうすることで、ハンマーが的の前面に固定されるので、それっぽい刺さり方をしているように見えるようになります。

的の厚みを変更するなど、親のスケールを変更すると子の位置がずれるので、親のスケールをいじる度に子の位置を調節し直す必要があります。

的に AddComponent するスクリプト

using UnityEngine;

public class Target : MonoBehaviour {
  public float moveSpeed = 0.03f;
  public float turnDistance = 10;

  Vector3 initialPosition;
  int direction = -1;
  float distance = 0;
  bool IsWaiting { get; set; }

  void Start() {
    transform.Translate(turnDistance * 0.5f, 0, 0);
    initialPosition = transform.position;
    IsWaiting = false;
  }

  void FixedUpdate() {
    if (direction == 0) { return; }

    transform.Translate(direction * moveSpeed, 0, 0);

    distance += moveSpeed;
    if (distance >= turnDistance) {
      direction *= -1;
      distance = 0;
    }
  }

  void OnTriggerEnter(Collider other) {
    if (IsWaiting) { return; }

    var projectile = other.transform.parent;

    //  見た目上、無理のない範囲で、的に刺さる角度を制限する
    var angle_y = projectile.rotation.eulerAngles.y;
    if (angle_y < -10 || angle_y > 80) {
      Debug.Log("pass through angle y=" + angle_y);
      projectile.GetComponent<Projectile>().Repel();
    }
    else {
      direction = 0;
      var fix_pos = transform.GetChild(0);
      var z = fix_pos.position.z;
      projectile.GetComponent<Projectile>().Stop(z);
      Debug.Log("hit!! angle y=" + angle_y);
    }
    IsWaiting = true;
  }

  public void OnButtonPushed() {
    if (direction == 0) {
      direction = -1;
      distance = 0;
      transform.position = initialPosition;
    }
    IsWaiting = false;
  }
}
Push ボタンの構造

uGui のボタンに、クリック時のコールバック先を設定しているだけです。

使用したアセット

ハンマーはこちらの有料アセットを使用しています。

コメントを残す

メールアドレスが公開されることはありません。