ハンマーを的にぶつけて刺すゲームを想定(物理エンジンあり)。
何もしない場合は、こんな刺さり方もします。
的は板とかコンクリートブロックみたいな硬いものを想定しているので、ハンマーの柄が的に刺さることを想定していない場合はNGになります。
NGの場合はそれっぽく見せる必要があります。
そもそも論として、
的に当たったときに、当たる角度で違和感が出てしまうものを投げることに問題があります。
的当てゲームを素早く作りたいなら、どの角度でも刺さる手裏剣や、鋭利なブーメランなどにするか、ナイフや弾丸などを回転させずに投げるべきです。
また、ミニゲームやスーパーカジュアルでこういう細かい事にこだわるのはお勧めしません。
ゲーム全体の完成度を上げる方がはるかに重要です。
1~2時間悩んでダメなら諦めた方が良いです。
- 当たったときにそれっぽい角度に変更
- それっぽい角度でしか当たらない
- コライダーの位置とサイズを調節する
無理やり角度を変えるので、こんな風に、一瞬元の角度が見えてしまうことがあります。
- 端っこで当たらないようにする
- 変更する角度を元の角度に近づける
- 投げるときの角度を変更する
的の真ん中にめりこんだときは、ハンマーがあまり見えないので気づきにくく、的の端に当たったときに目立ちます。
であれば、こういうギリギリの当たり方をした場合は当たっていないと判定させることで回避できるかも知れません(的とハンマーの距離を計算すれば判定できます)。
ただし、当たるはずの状況でも当たらなくなるので、当てる難易度が上がります。
当たったときの角度と、変更する角度との差を減らすことで違和感は減るかも知れません。
変更する角度を、許容できる範囲で元の角度に近づけるということです。
違和感が減るだけで、ごまかしている点は変わりません。
的に当たったときに違和感が出ない角度で投げるようにします。
ただ、投げるときに一瞬ハンマーの角度が変わるので、的に当たったときの違和感はなくなりますが、投げるときの違和感が出ます。
それから、当てる的が1つだけなら良いのですが、複数ある場合は簡単に解決できません。
どの的に当たりそうか?当たるならどの角度で当たるか? を事前に計算する必要が出てきます。
物理エンジンが内部でやっている計算をシミュレーションしないといけないので、かなりハードルが高いです。
衝突判定以外は物理エンジンを使わないようにすれば、計算しやすくなります。
私だったら、ミニゲームやスーパーカジュアルでは、そこまでやらないです。
それっぽい角度でないときは弾かれるようにしますが、難易度が跳ね上がります。
的の方のコライダーを調節します。
こんな風に、コライダーを2つに分けます。
- 的に当たったことを判定するコライダー
- ハンマーをはじくためのコライダー
当たったかどうかを判定するためのコライダー(トリガー用)は、的の前面に配置します。
ハンマーが飛ぶスピードに応じて、範囲を広げます。
そうしないと、ハンマーが的の後ろに行ったときにトリガーすることがあります。
ハンマーをはじかない場合は必要ないので無効にしておきます。
ある程度めりこませたい場合は、めり込みを許容する分だけコライダーのサイズを小さくします。
的に当たったときにハンマーをはじく場合、両方に Rigidbody が必要です。
めり込みを気にしない場合は、どちらか片方に Rigidbody があればトリガーします。
キモは、「衝突判定」と「Constraints」の設定です。
衝突判定を Continuous Speculative にしているのは、ハンマーが高速で回転しながら飛ぶからです。
とはいえ、的は回転しないので、「連続的かつ動的(Continuous Dynamic)」でも問題なさそうです。
衝突判定については、以下の記事の説明が完璧すぎて、とめどなく溢れ出す感謝の念が絶えません。
Constraints は Rigidbody 同士が衝突したときに、お互いに押し合って位置が変わるか?角度が変わるか?を設定します。
的の場合、位置が変わるのはX座標だけなので、YとZにチェックして、Y座標とZ座標の位置が変わらないようにしています。
全ての座標軸で回転させる必要がないので、XYZ全ての回転を固定しています。
ハンマーの場合、投げたときに変化するのはZ座標だけですが、的にはじかれる場合はX座標も変化するので、Y座標だけ固定しています。
ハンマーはY軸回転だけするので、回転はY以外を固定しています。
角度が悪いとはじかれるバージョンのソースコードです。
ハンマーは以下の構造になってます。
親 { 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; } }
uGui のボタンに、クリック時のコールバック先を設定しているだけです。
ハンマーはこちらの有料アセットを使用しています。