徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】ドミノ倒しを作ってみよう!

 今回はUnityを使ってドミノ倒しを作ってみようと思います。
 ドミノ倒しこそ、きっと今年のトレンドになるに違いありません。
 さっそく作っていきましょう。


この記事にはUnity2017.1.1f1を使用しています。

動くドミノ牌

 さて、どのようにドミノ倒しを実装していきましょうか。
 まずはドミノ牌を指定した場所に配置していく必要があります。
 配置システムを作るのも面倒くさいので、フィールド上を動き回れるドミノ牌を作って、そのドミノ牌のある場所にドミノ牌をコピーしていく感じで作っていきましょう。

 というわけで、まずは移動制御のスクリプトでも作りましょうか。

DominoController.cs

using UnityEngine;

[RequireComponent( typeof( Rigidbody ) )]
public class DominoController : MonoBehaviour
{
    [SerializeField]
    private float   m_moveSpeed     = 0.0f;
    [SerializeField]
    private float   m_turnSpeed     = 0.0f;

    private Rigidbody   m_rigidbody     = null;


    private void Awake()
    {
        m_rigidbody = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        ControlObject();
    }

    private void ControlObject()
    {
        Vector3 moveDir = Vector3.zero;

        if( Input.GetKey( KeyCode.UpArrow ) )
        {
            moveDir += Vector3.forward;
        }
        if( Input.GetKey( KeyCode.DownArrow ) )
        {
            moveDir += Vector3.back;
        }
        if( Input.GetKey( KeyCode.RightArrow ) )
        {
            moveDir += Vector3.right;
        }
        if( Input.GetKey( KeyCode.LeftArrow ) )
        {
            moveDir += Vector3.left;
        }

        if( moveDir.sqrMagnitude > Mathf.Epsilon )
        {
            moveDir = moveDir.normalized;
            Turn( moveDir );
            Move( moveDir );
        }
    }

    private void Move( Vector3 i_forward )
    {
        Vector3 delta       = i_forward * m_moveSpeed * Time.deltaTime;
        Vector3 targetPos   = transform.position + delta;

        m_rigidbody.MovePosition( targetPos );
    }

    private void Turn( Vector3 i_forward )
    {
        Quaternion  toRot   = Quaternion.LookRotation( i_forward );
        Quaternion  fromRot = transform.rotation;

        float delta = m_turnSpeed * Time.deltaTime;
        Quaternion targetRot    = Quaternion.RotateTowards( fromRot, toRot, delta );

        m_rigidbody.MoveRotation( targetRot );
    }

} // class DominoController

f:id:urahimono:20170924214205p:plain
f:id:urahimono:20170924214217g:plain

 はーい、出来ました。
 そこには元気に走り回るドミノ牌の姿が!

 キーボードの左右上下キーに合わせて、ドミノ牌が移動できるようになりました。

増えるドミノ牌

 さてお次は今あるドミノ牌の場所に、同じ形のドミノ牌をコピーして配置する処理を作成しましょう。

DominoController.cs

private void ControlObject()
{
    if( Input.GetKeyDown( KeyCode.A ) )
    {
        Duplication();
    }
}

private void Duplication()
{
    Instantiate( this, transform.position, transform.rotation );
}

f:id:urahimono:20170924214255g:plain

 おや、ドミノ牌をコピーして配置した瞬間、生成したドミノ牌と元のドミノ牌が仲良く倒れてしまいましたね。
 ああ、なるほど。
 まったく同じ場所にドミノ牌を生成したから、生成と同時にコリジョン判定が起きて倒れしまったということか。
 うーむ、操作しているドミノ牌の位置から前後に生成してあげれば良さそうですが、出来ることなら同じ場所に生成したいものです。

 とすれば、コリジョン判定が起きないようにする必要がありますね。
 レイヤーを分けちゃいましょうか。
 二つのレイヤーを追加し、そのレイヤー同士はコリジョン判定を行わないように設定しましょう。

f:id:urahimono:20170924214317p:plain

 操作するドミノ牌にはControledDominoレイヤーを使い、生成するドミノ牌にはDominoレイヤーを指定するようにしましょう。
 ただレイヤーはint型で指定するため、エディタ上ではどのレイヤーなのかわかりにくいです。
 折角なのでレイヤー指定用のアトリビュートも作ってしまいましょう。

LayerTypeFieldAttribute.cs

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif // UNITY_EDITOR

public class LayerTypeFieldAttribute : PropertyAttribute
{

} // class TagAttribute

#if UNITY_EDITOR

[CustomPropertyDrawer( typeof( LayerTypeFieldAttribute ) )]
public class LayerTypeFieldDrawer : PropertyDrawer
{
    public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label )
    {
        i_property.intValue = EditorGUI.LayerField( i_position, i_label, i_property.intValue );
    }

    public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label )
    {
        return EditorGUI.GetPropertyHeight( i_property );
    }

} // class TagAttribute

#endif // UNITY_EDITOR

 このアトリビュートを使って、ドミノのスクリプトを改良しましょうか。

DominoController.cs

[SerializeField, LayerTypeField]
private int     m_dominoLayer   = 0;

private void Duplication()
{
    var copiedDomino    = Instantiate( this, transform.position, transform.rotation );
    SetDominoParameter( copiedDomino );
}

private void SetDominoParameter( DominoController i_domino )
{
    i_domino.gameObject.layer   = m_dominoLayer;
}

f:id:urahimono:20170924214332p:plain
f:id:urahimono:20170924214342g:plain

 操作しているドミノ牌とコリジョン判定が行われないようにはなりましが、生成したドミノ牌まで操作できてしまっています。
 そのため、ドミノ牌を増やそうとすると倍々と増えていってしまっています。
 バイバインかいな。

 コピーして生成したドミノ牌は操作しないようにスクリプトを修正しましょう。

DominoController.cs

[SerializeField]
private bool    m_controlled    = true;

private void Update()
{
    if( m_controlled )
    {
        ControlObject();
    }
}

private void SetDominoParameter( DominoController i_domino )
{
    i_domino.m_controlled       = false;
    i_domino.gameObject.layer   = m_dominoLayer;
}

f:id:urahimono:20170924214427g:plain

 OKです。
 これでドミノ牌を生成して配置することが出来ましたね。

倒れるドミノ牌

 配置まで行きましたので、あとはドミノ牌が倒れてしまえばドミノ倒し完成です。
 指定したキーを押したら、操作しているドミノ牌が倒れるようにスクリプトに追記しましょう。

DominoController.cs

[SerializeField]
private float   m_torqueForce   = 0.0f;

private void ControlObject()
{
    if( Input.GetKeyDown( KeyCode.S ) )
    {
        SetDominoParameter( this );
        Topple( transform.forward );
    }
}

private void Topple( Vector3 i_forward )
{
    // 進行方向に対してX軸で回転させたいのでrightベクトルを使用して回転ベクトルを作る。
    Vector3 rightVec    = transform.right;
    Vector3 torque      = rightVec * m_torqueForce;

    m_rigidbody.AddTorque( torque, ForceMode.Force );
}

f:id:urahimono:20170924214459p:plain
f:id:urahimono:20170924214509g:plain

 ちょいスローリーのような気もしますが、バタッと倒れました。
 ではこの機能を使って、他のドミノ牌に向かって倒れてみましょう。

f:id:urahimono:20170924214527g:plain
f:id:urahimono:20170924214600p:plain

 思ってたのと違う!
 全然倒れやしねぇ。
 パワーが足りないのかもしれません。
 AddTorque()に渡すパワーをもっと大きくしましょう。

f:id:urahimono:20170924214617p:plain
f:id:urahimono:20170924214628g:plain
f:id:urahimono:20170924214655p:plain

 うん、こりゃ駄目だ。
 これ以上パワーを増やしてもあんまり変わんない気がする。

連鎖するドミノ牌

 考え方を変えてみましょう。
 ドミノ牌の倒れるパワーを大きくするのではなく、倒れてきたドミノ牌に当たったドミノ牌も倒れる処理を呼ぶようにしましょうか。
 スクリプトを改良しましょう。

DominoController.cs

private bool        m_isTopple      = false;
private Vector3     m_toppleFoward  = Vector3.forward;
    
private void Topple( Vector3 i_forward )
{
    // 進行方向に対してX軸で回転させたいのでrightベクトルを使用して回転ベクトルを作る。
    Vector3 rightVec    = transform.right;
    Vector3 torque      = rightVec * m_torqueForce;

    m_rigidbody.AddTorque( torque, ForceMode.Force );

    m_isTopple      = true;
    m_toppleFoward  = transform.forward;
}

private void OnCollisionEnter( Collision i_collision )
{
    if( m_isTopple )
    {
        return;
    }

    var domino  = i_collision.gameObject.GetComponent<DominoController>();
    if( domino != null && domino.m_isTopple )
    {
        Topple( domino.m_toppleFoward );
    }
}

f:id:urahimono:20170924214712g:plain
f:id:urahimono:20170924214748p:plain

 おお、いいじゃないですか。
 いい感じにドミノ倒しになっていますよ。
 目的達成です!

 んー……、ただスクリプトを見返してみると、この倒れる処理には問題がありますね。
 Topple()の関数は、ドミノ牌の進行方向に対して倒れる処理になっています。
 そのため、反対方向から倒された場合はうまく倒れる処理が働かなくなってしまいます。

f:id:urahimono:20170924214803g:plain
f:id:urahimono:20170924214841p:plain

 こんな感じになってしまいます。

 というわけでTopple()の関数を修正する必要があります。
 ドミノ牌が倒れ方は、前に倒れる後ろに倒れるかの二パターンだけです。
 そのため倒したい方向に対して、前か後ろかを判定してあげればよさそうです。

 現時点でドミノ牌を倒したい方向の情報は引数で関数に渡しています。
 この情報と、ドミノ牌の進行方向の情報を使って何とかしてみましょう。

 内積でも使ってみますか。

 ドミノ牌の向きベクトルと倒したい向きのベクトルの内積を求めてみましょう。
 この二つのベクトルの向きが同じ方向ならば、二つのベクトルの角度は鋭角になります。
 ということは、内積の値は0以上になるはずです。

 逆にこの二つのベクトルの向きが反対方向ならば、二つのベクトルの角度は鈍角になります。
 ということは、内積の値は0未満になるはずです。

 この情報から、倒れるドミノ牌は、前に倒れるべきか後ろに倒れるべきかが判定できます。
 直角の場合は……、いいや、前に倒してしまえ。
 というわけで内積が0以上なら前に倒れる、0未満なら後ろに倒れる、こんな感じにしましょう。

 ちなみに内積について詳しくはゲーム制作バイブル「○×つくろーどっとコム」の記事を見ていただいたほうがいいかと。
基礎の基礎編その1 内積と外積の使い方

 ではスクリプトを修正しましょう。

DominoController.cs

private void Topple( Vector3 i_forward )
{
    // オブジェクトの進行方向と倒す方向を内積計算する。
    // 進行方向と倒す方向のベクトルの角度が同じ方向(鋭角)なら0以上になる。
    // 進行方向と倒す方向のベクトルの角度が反対方向(鈍角)なら0未満になる。
    // 上記を利用して、前に倒れるべきか後ろに倒れるかを求める。
    bool isForward  = Vector3.Dot( transform.forward, i_forward ) >= 0.0f;

    // 進行方向に対してX軸で回転させたいのでrightベクトルを使用して回転ベクトルを作る。
    // 後ろに倒れる場合はleftベクトルを使いたいので、rightベクトルのマイナス値を使っている。
    Vector3 rightVec    = isForward ? transform.right : -transform.right;
    Vector3 torque      = rightVec * m_torqueForce;

    m_rigidbody.AddTorque( torque, ForceMode.Force );

    m_isTopple      = true;
    m_toppleFoward  = isForward ? transform.forward : -transform.forward;
}

f:id:urahimono:20170924214903g:plain
f:id:urahimono:20170924214934p:plain

 はい、ドミノ倒しの出来上がりー。

出来上がってドミノ牌

 というわけで出来上がりました、ドミノ倒し。
 スクリプトの完成形は以下に記載しておきますね。
 
 これで今年のメディアアワードはいただきですね。

f:id:urahimono:20170924215613g:plain
f:id:urahimono:20170924215658p:plain

【Unity】ドミノ倒しを作ってみよう!