[Unity] ブロック崩しをあのアプリ風にする #5 パズルボブルのような入力

つまりこんな感じ。


どんなスクリプトになるんだ?

  • タップすると発射地点から決められた長さの予測ラインが出る
  • スライドすると予測ラインも傾く
  • 離すと消える
  • 予測ラインが画面の端に当たると進行方向を変える
  • 平行線より下にスライドするとキャンセル

こんな感じでしょうか。シンプルに見える操作も結構複雑だったりします。
何か参考になるものはないか..と探していたらありました。
Unity勇者の冒険の書さん。アングリーバード風の弾道予測スクリプトですね。
ありがたく使わせていただきます。

しかし、アングリーバード風に開発されたソレなので若干求めてる仕様と違います。
  • 壁を超えた時の進行方向が反転しない
  • 入力した位置に向かって伸縮する→伸縮せず入力した方向に傾く
  • 入力あり、なしで表示のOn,Off

この辺りをこちょこちょして自分色に染めなおさせてもらいます。

GameManagerを作る

その前にゲーム全体を司るGameManagerを作ります。
CreateEmptyから2つ作ります。1つがGameManager、2つ目がBallBornPointとしておきます。同じくスクリプトもGameManagerという名前で作ります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {

//ボールの初期位置
    public GameObject ballBornPoint;

 void Start () {
        Init ();
 }
    void Init(){
        GameObject Bwall = GameObject.Find ("Bwall");
//下の壁に当たる位置 + ボールの半径分だけ初期位置を上に設定
        float _z = Bwall.transform.position.z + Bwall.transform.lossyScale.z/2 + 0.4f;
        ballBornPoint.transform.position = transform.position= new Vector3 (0, 0, _z);;
    }

Initを呼び出してボールと弾道予測に使われるダミースフィアの初期位置を設定しています。
下の壁の座標を元にしているのでヒエラルキーの下の壁に当たる部分の名前をBwallに変えておきます。


今はこれだけだけど、この初期位置はこの後書くスクリプト2つで利用するので、このスクリプトがもっとも早く実行されないといけません。なのでその設定をします。
Edit > Project Settings > Script Execution Orderで出てくる画面の+を押してGameManager を選択したらドラッグして上に持っていきます。
名前の右に表示されている数字が小さいほど早く実行されます。



Applyを押して完了です。
GameManagerにスクリプトをつけたらインスペクターのballBornPointにヒエラルキーのBallBornPointを紐付けます。

ちなみにGameManagerという名前でスクリプトを作ったらこんなアイコンに変化しました。


入力管理のスクリプト


まずInputControllerオブジェクトからTapGestureを消します。前回インストールしたTouchScriptのFullscreenLayer 、PressGesture 、TransformGesture 、ReleaseGestureをアタッチします。
新規スクリプトをInputControllerEpitaphという名前で2つ作りこれもアタッチしておきます。


入力を管理するInputControllerはこうなりました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TouchScript.Gestures;
using TouchScript.Gestures.TransformGestures;

public class InputController : MonoBehaviour {

//弾道予測スクリプト
    Epitaph epitaph;

    void Start(){
        epitaph = GetComponent<Epitaph> ();
    }

// オブジェクトが有効化されたときにeventにメソッドを登録する
    private void OnEnable(){
        GetComponent<PressGesture>().Pressed += onPress;
        GetComponent<TransformGesture>().Transformed += onTransformed;
        GetComponent<ReleaseGesture>().Released += OnRelease;
    }

// オブジェクトが無効化されたときにeventからメソッドを削除する
    private void OnDisable(){
        GetComponent<PressGesture>().Pressed  -= onPress;
        GetComponent<TransformGesture>().Transformed -= onTransformed;
        GetComponent<ReleaseGesture>().Released -= OnRelease;
    }

// タッチすると呼ばれる
    private void OnPress(object sender, System.EventArgs e) {
//点を可視化
        epitaph.Visualaization();
    }

// 離した時に呼ばれる
    private void OnRelease(object sender, System.EventArgs e) {
//点を不可視化
        epitaph.Misunderstanding ();
    }

// スワイプすると呼ばれる
    private void OnTransformed(object sender, System.EventArgs e) {
// TransformGesture型にキャスト
        var gesture = sender as TransformGesture;
//スクリーン座標を得ワールド座標に変換
        Vector3 point = gesture.ScreenPosition;
        point.z = Camera.main.transform.position.y ;
        Vector3 p = Camera.main.ScreenToWorldPoint (point);
//エピタフ更新
        epitaph.SetPoint(p);
    }
}

Start

まずStart取得しているのは弾道予測スクリプトです。Epitaph(エピタフ)とはもちろんキングクリムゾンの額についてるアレです。未来を視ることができるのでこう名付けました。完璧なセンスだけど本来はこういった個人的なネーミングは事故の元です


OnEnable、OnDisable

ジェスチャーを3つ登録してます。タップスライド離した時です。
前回はTapGestureを使ってたけどPressGestureに変えました。TapGestureは タッチ→スライドせずに離す で呼ばれているようなので今回の仕様とは相性が悪いからです。PressGestureはタッチした瞬間に呼ばれます。
OnDisableではそれらを解除してます。


OnPress、OnRelease

タッチされた時に弾道予測の点を可視化、離した時に不可視化のメソッドを呼び出してるだけです。このあと実装します。
タッチ状態のハンドラはOnTransformedを登録してるんだからそもそもOnPressは必要なさそうに思えますが、OnTransformedが呼び出されるのスライドされた時のみです。タッチされた瞬間は拾ってくれないのでOnPressも登録してます。


OnTransformed

ここではタッチされた座標を得るため、まずTransformGesture型にキャストしてます。もし他のジェスチャーから変換する場合はそのジェスチャー名でキャストします(TapGestureならTapGesture型にキャスト)。
ScreenPositionで返ってくるのはスクリーン座標なのでワールド座標に変換する必要があります。

この辺ほんとややこしい。

スクリーン座標はx軸とy軸の2次元で表されます。ワールド座標はz軸を加えた3次元です。 ScreenToWorldPointはスクリーン座標をワールド座標に変換できるけど、z軸を指定しないとおかしな事になります。その辺はテラシュールブログさんなど詳しいです。
要はカメラからみてどれくらいの奥行きの位置にあるか、が指定するzの値です。今回は点の位置がちょうどカメラのyなので、そのままepitaphに渡しています。


弾道予測のスクリプト

そのEpitaphはこうなりました。(オリジナルを見ておくといいかも)


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Epitaph : MonoBehaviour {

    [SerializeField]
    private GameObject dummyObjPref;

//玉が生み出される位置情報
    private Transform  ballBornPoint ;

    [SerializeField]
    private int dummyCount;

    [SerializeField]
    private float secInterval;

    [SerializeField]
    private float offsetSpeed = 0.5f;

//壁の位置情報
    float LOverLine,ROverLine;

//タッチされているか
    private bool isTouch; 

//タッチされているスクリーン座標
    private Vector3 touchPos;
    private float offset;
    private List<GameObject> dummySphereList = new List<GameObject>();
    private Rigidbody rigid;

    void Start()
    {
        rigid = GetComponent<Rigidbody>();
        if (!rigid)
            rigid = gameObject.AddComponent<Rigidbody>();
        rigid.isKinematic = true;
        for (int i = 0; i < dummyCount; i++)
        {
            var obj = (GameObject)Instantiate(dummyObjPref, dummyObjParent);
            dummySphereList.Add(obj);
        }
//壁の位置を取得して反転座標を設定
        GameObject Rwall = GameObject.Find ("Rwall");
        GameObject Lwall = GameObject.Find ("Lwall");
        ROverLine = Rwall.transform.position.x - Rwall.transform.lossyScale.x/2;
        LOverLine = Lwall.transform.position.x + Lwall.transform.lossyScale.x/2;
        isTouch = false;
    }

    void Update(){
//タッチされているなら
        if(isTouch){
            offset = Mathf.Repeat(Time.time * offsetSpeed, secInterval);
//弾道予測の更新
            for (int i = 0; i < dummyCount; i++) {
                float t = (i * secInterval) + offset;
                float x = 0;
                float z = t * 1.5f;
                float y = 0;
//角度の更新
                Vector3 anglePoint = touchPos - ballBornPoint.position;
                ballBornPoint.rotation = Quaternion.LookRotation (anglePoint);
//一旦更新する
                dummySphereList [i].transform.localPosition = new Vector3 (x, y, z);
//壁を超えていたら
                float curentX = dummySphereList [i].transform.position.x;
//右にオーバーの場合
                if (curentX > ROverLine) {
                    float pos = Mathf.Abs (curentX - ROverLine) * -2;
//補正用Vector3作成
                    Vector3 offsetPos = new Vector3 (pos, 0, 0);
//座標を補正
                    dummySphereList [i].transform.position += offsetPos;
//左にオーバーの場合
                } else if (curentX < LOverLine) {
                    float pos = Mathf.Abs (curentX - LOverLine) * 2;
                    Vector3 offsetPos = new Vector3 (pos, 0, 0);
                    dummySphereList [i].transform.position += offsetPos;
                }
            }
        }
    }
//タッチされている座標が入ってくる
    public void SetPoint(Vector3 p){
        touchPos = p;
    }

//OFF 点を非アクティブ化
    public void Misunderstanding(){
        foreach(var obj in dummySphereList) {
            obj.SetActive (false);
        }
        isTouch = false;
    }
//ON 点をアクティブ化
    public void Visualaization(){
        foreach(var obj in dummySphereList) {
            obj.SetActive (true);
        }
        isTouch = true;
    }
}

追加したところだけ説明します。

Start

この中で新たに折り返し地点となる左右のボーダーラインを設定しています。名前から取得しているのでヒエラルキーの右の壁を"Rwall"、左を"Lwall"に変えておきます。
最後にタッチされているかどうかのフラグをfalseにしておきます。

Update

タッチされていれば処理に入ります。重力の判定は必要ないのでyは0固定です。
InputControllerから渡ってくるtouchPosとballBornPointを元に玉の位置が決まったら角度を変えて一旦更新します。
更新するのは左右にオーバーしているかどうかを見るためです。本当は計算で一発で出した方が処理的にもいいんだろうけどやめました。
というか、自前で計算しようとしていてQuaternion.LookRotationという便利メソッドを見つけました。もっと早く出会いたかった。
その後ステージ内に収まっているかを見て、オーバーしていた場合座標を修正しています。
オーバーした距離の2倍、戻しています。

SetPoint

スワイプされるたびにtouchPosを更新。

Misunderstanding、Visualaization

Misunderstandingは指を離した時、Visualaizationはタッチした時にInputControllerから呼ばれます。


最後にインスペクターのEpitaph > Ball Born PointにヒエラルキーのBallBornPointを紐付けておきます。
ほぼ完成したけどもう一つやることがあります。


平行線より下にスライドされたらキャンセル

画面の下に向けて撃てないようにします。なくてもいい機能だけどあった方が親切です。
ちなみに平行に玉を打たれてしまうと永遠に止まらくなるので実際は平行線よりちょっと上でキャンセルにした方がいいと思います。

それと、スライド終了 → 再タッチとすると前回の弾道ラインが表示されてしまうので、そこらへんも修正します。
inputControllerを少し書き換えます。

//〜中略〜    

// タッチすると呼ばれる
    private void OnPress(object sender, System.EventArgs e) {
    epitaph.Visualaization();
// PressGesture型にキャスト
    var gesture = sender as PressGesture;
    Vector3 point = gesture.ScreenPosition;
    LineUpdate (point);
}

// スワイプすると呼ばれる
    private void OnTransformed(object sender, System.EventArgs e) {
// TransformGesture型にキャスト
    var gesture = sender as TransformGesture;
//タッチされているスクリーン座標を得る
    Vector3 point = gesture.ScreenPosition;
    LineUpdate (point);
}

// タッチされている座標が有効ならEpitaphへ渡す
    void LineUpdate(Vector3 point){
    point.z = Camera.main.transform.position.y ;
    Vector3 p = Camera.main.ScreenToWorldPoint (point);
    if (p.z - 0.1f >= gameObject.transform.position.z) {
//エピタフ更新
        epitaph.SetPoint (p);
    } else {
//点を不可視化
        epitaph.Misunderstanding ();
    }
}
//〜中略〜

OnTransformedの中身を一部切り出して新しくLineUpdateを作りました。その中でz座標の位置が下がりすぎなら点を消しています。
それをOnPressからも呼び出しているのでタッチした瞬間にラインが更新されます。


何が変わったのか分かりづらいですね..。
ちょっと長くなったけど次は玉を発射できるようにします。

コメント