【Unity2d】グリッド移動の制御と引っ張り処理の仕上げ

Unity2D

Unity2D ARPG

こんにちは。なおキーヌです。

ブログ毎日更新は342日目になります。

前回はグリッド移動の制御ができてなかったり、移動処理をアクションスクリプトに書いてたりしたので
制御したりちゃんと移動スクリプト移動処理を任せてキレイにしていきましょう。

グリッド移動

プレイヤーは基本ドット移動なので、グリッド移動させてしまうと割り切れない場合があって
延々と動き続けるバグが生じる可能性があります。

なのでグリッド移動の起点となるのはオブジェクトの方にしたいと思います。

オブジェクトは基本的にグリッドに沿って配置されており、
動くのもグリッド移動のみに縛っておきます(一部は除く)

移動処理を移動スクリプトに移す

まずはAction_Playerに書いてしまった移動処理をちゃんと移動スクリプトに移しましょう。

このままだと何のためにスクリプトに名前を付けているのかわからないですからね。

ちゃんと役割分担しましょう。

グリッド移動関数は対象オブジェクト・ステートを渡すようにしましょう。

グリッド移動のスピードもステート側で速さを設定しておけば渡されたステートから参照できます。

とりあえず前回作った移動処理をMove_PlayerのgridMoveにそのまま持っていきます。

    protected override void gridMove() {
        if(is_lcastFront.collider != null) {
            float vx=0,vy=0;
            int speed = 30;
            switch(_State.Direction) {
                case 0:
                    vy = -(speed/4);
                    break;
                case 1:
                    vy = speed/4;
                    break;
                case 2:
                    vx   = speed/4;
                    break;
                case 3:
                    vx = -(speed/4);
                    break;
            }
            is_lcastFront.transform.Translate(vx/50, vy/50, 0);
            _Player.transform.Translate(vx/50, vy/50, 0);
        }
        // xもyも16で割り切れたら
        if(Math.Floor(is_lcastFront.transform.position.x)%16 == 0 && Math.Floor(is_lcastFront.transform.position.y)%16 == 0) {
            Debug.Log("割り切れた");
        }
    }

内部で使っているものは

  • speed(移動速度)
  • is_lcastFront(掴んでるオブジェクト情報)
  • _State(プレイヤーのステート)
  • _Player(プレイヤーオブジェクト)

ですね。

speedはさっきも言った通り、押す引く時のスピードをステートとして定義します。

is_lcastFrontはAction側で検出して保持しているのでそれを参照できるようにしましょう。

_Stateと_Playerは既に参照しているのでそのまま使います。

まずはState_Playerにspeedを定義します。

プレイヤーの移動速度をspeedを参照するようにして、
グリッド移動時はそのスピードで速度を調整するような感じにします。

  // 移動用スピード
  protected static int _moveSpeed = 30;
  // _moveSpeed用Getter
  public int MoveSpeed {
    get {
      return State_Player._moveSpeed;
    }
  }

これでスピードの設定と参照が可能になりました。

次にAction_Managerで定義している掴んだオブジェクトの情報を
移動スクリプト側で参照できるようにしましょう。

前回のコピペしてきたので変数の手前にアンダースコアが付いてないので付けて
参照しているところも変えておきましょう。

public class Action_Manager : MonoBehaviour
{
    // 衝突判定が取れたかどうかを格納する変数
    protected static RaycastHit2D _isLcastFront;
    // _isLcastFront用Getter
    public int IsLcastFront {
        get {
        return Action_Manager._isLcastFront;
        }
    }
    // 省略
}

Action_Playerで使っている「is_lcastFront」を「_isLcastFront」に置換しておいてください。

これで外部スクリプトからReadOnlyで参照することができるようになったので
Move_Player側でスクリプトを取得するようにしてみましょう。

    // 掴んだオブジェクト情報の取得
    private Action_Player _Action;

    // 更新処理
    protected override void Start() {
        // プレイヤーオブジェクトの取得
        _Player = GameObject.FindGameObjectsWithTag("Player")[0];
        _State = GameObject.FindGameObjectsWithTag("State_Manager")[0].GetComponent<State_Player>();
        _Action = GameObject.FindGameObjectsWithTag("Action_Manager")[0].GetComponent<Action_Player>();
    }

新たなタグ「Action_Manager」を作ってActionManagerオブジェクトに適用しましょう。

これでAction_Playerスクリプトを取得したので用意したゲッターで、
Action_Player側で掴んだオブジェクトを参照できますね。

グリッド移動の修正

やっとグリッド移動ができるようになりました。

早速Move_Player側のgridMoveを修正していきます。

ココで少し気になったのですが、掴んでなくてもグリッド移動する可能性がありそうなので
関数名を「grabGridMove」に変更します。

Move_Managerにvirtualで定義してMove_Playerでoverride出来るようにしましょう。

    // 掴みグリッド関数
    protected virtual void grabGridMove() {
    }
    // 掴みグリッド移動関数
    protected override void grabGridMove() {
        if(_Action.IsLcastFront.collider != null) {
            float vx=0,vy=0;
            int speed = _State.MoveSpeed;
            switch(_State.Direction) {
                case 0:
                    vy = -(speed/4);
                    break;
                case 1:
                    vy = speed/4;
                    break;
                case 2:
                    vx   = speed/4;
                    break;
                case 3:
                    vx = -(speed/4);
                    break;
            }
            _Action.IsLcastFront.transform.Translate(vx/50, vy/50, 0);
            _Player.transform.Translate(vx/50, vy/50, 0);
        }
        // xもyも16で割り切れたら
        if(Math.Floor(_Action.IsLcastFront.transform.position.x)%16 == 0 && Math.Floor(_Action.IsLcastFront.transform.position.y)%16 == 0) {
            Debug.Log("割り切れた");
        }
    }

内容は殆ど変わらずです。

しかしこのままだと前回と結果が変わらないですね。

割り切れたら割り切れたと出てくれているのでここでグリッド移動を止める何かをしなければいけません。

_Stateを取得してるのでステートを書き替えたいのですが、
Moveスクリプトで書き替えるというのはちょっと違うと思います。

自由に値を入れ替えられてしまっては役割の意味がありません。

なのでState側でグリッド移動が終わったことを告げる関数を作り、
その関数が呼ばれたらState側がステートをを変更するようにすればきれいでしょう。

State_Managerに以下のpublic関数を作ります。

  // グリッド移動終了関数
  public void endGridMove(bool endFlag = false) {
    if (endFlag) {
      _actState = 0;
    }
  }

つまるところこの関数に割り切れたかどうかの判定を渡して、
割り切れたらステートが強制で0にするようにすればとまってくれます。

これによりAction_PlayerのgrabGridMove関数はこうなります。

    // 引っ張り処理
    protected override void grabGridMove() {
        if(_Action.IsLcastFront.collider != null) {
            float vx=0,vy=0;
            int speed = 30;
            switch(_State.Direction) {
                case 0:
                    vy = -(speed/4);
                    break;
                case 1:
                    vy = speed/4;
                    break;
                case 2:
                    vx   = speed/4;
                    break;
                case 3:
                    vx = -(speed/4);
                    break;
            }
            _Action.IsLcastFront.transform.Translate(vx/50, vy/50, 0);
            _Player.transform.Translate(vx/50, vy/50, 0);
        }
        // xもyも16で割り切れたら強制ステート0
        _State.endGridMove(Math.Floor(_Action.IsLcastFront.transform.position.x)%16 == 0 && Math.Floor(_Action.IsLcastFront.transform.position.y)%16 == 0);
    }

次にAction_PlayerでPullが呼ばれた時、Move_PlayerでgridGrabMove呼ばれ続ける必要がありますね。

現状MoveTypeでドット移動かグリッド移動をわけています。

グリッド移動にも何種類かでてきそうなので、gridMove関数の中でgridGrabMoveを呼ぶようにして
状況に応じて呼ぶグリッド移動関数を切り替えるようにしましょう。

一旦はこんな感じにします。

    protected override void Update() {
        if (_State.MoveType == 1) {
            gridMove();
        } else {
            dotMove();
        }
    }
    // グリッド移動
    protected override void gridMove() {
        grabGridMove();
    }

よくみたら関数としては呼び出しているsetMoveType関数の中身が空だったので作っていきます。

仕組みとしてはグリッド移動中のステートになったらMoveTypeを1にして以外は0ですね。

  // 移動タイプ変更
  protected void setMoveType() {
    if (_actState == 106 || _actState == 107) {
      _moveType = 1;
    } else {
      _moveType = 0;
    }
  }

今のところグリッド移動中はステート106と107ですね。

もし増えて来たらちょっと書き方を変えようと思います。

これによりグリッド移動が終わったらmovetypeが0に戻るのでグリッド移動に戻るというわけです。

setMoveTypeは既にState_Player側で読んでいるので常に呼ばれ続けています。

コレでグリッド移動が完了したら止まってくれます。

……微妙にずれてますが?

というのも、判定をしてから動かしていないので判定後に補正してやらないと微妙にずれた状態になっているわけです。

endGridMoveの引数にtrueを渡したらTrueが返ってくるしくみにしてもいいですし、
endGridMoveにtrueを渡したら次の行ではStateが0になっているのでそれで判断してしまうというのも手です。

後者は恐らくないとは思うのですがなんらかの状況で割り込みとかが入ってしまった場合
条件にみたされなくなるので出来ればendGridMoveの返り値をboolにした方が安全そうですね。

  // グリッド移動終了関数
  public bool endGridMove(bool endFlag = false) {
    if (endFlag) {
      _actState = 0;
      return true;
    }
    return false;
  }

このようにtrueかfalseを返すようにしてみます。

最終的にgridGravMoveはこうなります。


    // 掴みグリッド移動関数
    protected override void grabGridMove() {
        if(_Action.IsLcastFront.collider != null) {
            float vx=0,vy=0;
            int speed = _State.MoveSpeed;
            switch(_State.Direction) {
                case 0:
                    vy = -(speed/4);
                    break;
                case 1:
                    vy = speed/4;
                    break;
                case 2:
                    vx   = speed/4;
                    break;
                case 3:
                    vx = -(speed/4);
                    break;
            }
            _Action.IsLcastFront.transform.Translate(vx/50, vy/50, 0);
            _Player.transform.Translate(vx/50, vy/50, 0);
        }
        // xもyも16で割り切れたら強制ステート0
        if(_State.endGridMove(Math.Floor(_Action.IsLcastFront.transform.position.x)%16 == 0 && Math.Floor(_Action.IsLcastFront.transform.position.y)%16 == 0)) {
            // 位置補正
            _Action.IsLcastFront.transform.position = new Vector3((float)Math.Floor(_Action.IsLcastFront.transform.position.x),(float)Math.Floor(_Action.IsLcastFront.transform.position.y),0);
            _Player.transform.position = new Vector3((float)Math.Floor(_Player.transform.position.x),(float)Math.Floor(_Player.transform.position.y),0);

        }
    }

後半長ったらしくて見辛いですね……
ここは関数化してしまうのが良いかと思います。

以下のプライベート関数を作りましょう。

    // 割り切れたら位置補正
    private void fixedGridPosition(State_Player state, RaycastHit2D castFront, GameObject player) {
        // 見やすいようにローカル変数を使用
        Vector3 castPos = castFront.transform.position;
        Vector3 playerPos = player.transform.position;
        // 割り切れたらTrue
        bool cxF = Math.Floor(castPos.x)%16 == 0 ? true : false;
        bool cyF = Math.Floor(castPos.y)%16 == 0 ? true : false;

        // xもyも16で割り切れたら強制ステート0
        if(state.endGridMove(cxF && cyF)) {
            // 位置補正
            castPos   = new Vector3((float)Math.Floor(castPos.x),(float)Math.Floor(castPos.y),0);
            playerPps = new Vector3((float)Math.Floor(playerPos.x),(float)Math.Floor(playerPos.y),0);
        }
    }

だいぶ見やすくなったと思います。

割り切れたかどうかのフラグを三項演算子というものを使って1行でif分的なものを書きました。

最後の切り捨ててからfloatにキャストするところはなんかいい方法が浮かばなかったのでそのままです(笑)

もっとスマートな書き方できたかもしれませんが、私の勉強不足ということで。

ここで1つ問題が発生!

このままfixedGridPositionを呼び出したらグリッド補正が効かないまま移動が終わってしまいます。

というのも、関数内でローカル変数としてVector3を定義しているのに注目してください。

ここにcastとplayerのポジションを入れていますが、
これは値コピーになっているため本体のオブジェクトからは切り離されてしまっています。

いわゆる参照コピーじゃなくて値コピーなわけですね。

なので最後のif分は

        // xもyも16で割り切れたら強制ステート0
        if(state.endGridMove(cxF && cyF)) {
            // 位置補正
            castFront.transform.position = new Vector3((float)Math.Floor(castPos.x),(float)Math.Floor(castPos.y),0);
            player.transform.position = new Vector3((float)Math.Floor(playerPos.x),(float)Math.Floor(playerPos.y),0);
        }

このように参照の方に値を適用してやらないといけませんね。

おそらくpositionではなくtransformのローカル変数だと参照だと思います。

これはかなりメモリの無駄遣いかもしれません。

ですが今は見やすさ重視です。

最終的にMove_Playerスクリプトはこうなっています。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move_Player : Move_Manager
{
    // プレイヤーオブジェクト
    private GameObject _Player;
    // プレイヤーステートスクリプトの取得
    private State_Player _State;

    // 掴んだオブジェクト情報の取得
    private Action_Player _Action;

    // 更新処理
    protected override void Start() {
        // プレイヤーオブジェクトの取得
        _Player = GameObject.FindGameObjectsWithTag("Player")[0];
        _State = GameObject.FindGameObjectsWithTag("State_Manager")[0].GetComponent<State_Player>();
        _Action = GameObject.FindGameObjectsWithTag("Action_Manager")[0].GetComponent<Action_Player>();
    }
    protected override void Update() {
        if (_State.MoveType == 1) {
            gridMove();
        } else {
            dotMove();
        }
    }

    // ドット移動関数
    protected override void dotMove() {
        float vx = 0;
        float vy = 0;

        if (_State.ActState == 1) {
            int speed = _State.MoveSpeed;
            switch(_State.Direction%10) {
                case 0:
                    vy = -speed;
                    break;
                case 1:
                    vy = speed;
                    break;
                case 2:
                    vx = -speed;
                    break;
                case 3:
                    vx = speed;
                    break;
                default:
                    break;
            }
            switch(_State.Direction) {
                case 10:
                case 11:
                    vx = -speed;
                    break;
                case 20:
                case 21:
                    vx = speed;
                    break;
                case 12:
                case 13:
                    vy = -speed;
                    break;
                case 22:
                case 23:
                    vy = speed;
                    break;
            }
        }
        // 実際のドット移動
        _Player.transform.GetComponent<Rigidbody2D>().velocity = new Vector2(vx, vy);
    }

    // グリッド移動
    protected override void gridMove() {
        grabGridMove();
    }

    // 掴みグリッド移動関数
    protected override void grabGridMove() {
        if(_Action.IsLcastFront.collider != null) {
            float vx=0,vy=0;
            int speed = _State.MoveSpeed;
            switch(_State.Direction) {
                case 0:
                    vy = -(speed/4);
                    break;
                case 1:
                    vy = speed/4;
                    break;
                case 2:
                    vx   = speed/4;
                    break;
                case 3:
                    vx = -(speed/4);
                    break;
            }
            _Action.IsLcastFront.transform.Translate(vx/50, vy/50, 0);
            _Player.transform.Translate(vx/50, vy/50, 0);

            // 割り切れたら位置補正
            fixedGridPosition(_State, _Action.IsLcastFront, _Player);
        }
    }

    // 割り切れたら位置補正
    private void fixedGridPosition(State_Player state, RaycastHit2D castFront, GameObject player) {
        // 見やすいようにローカル変数を使用
        Vector3 castPos = castFront.transform.position;
        Vector3 playerPos = player.transform.position;
        // 割り切れたらTrue
        switch(state.)
        bool cxF = Math.Floor(castPos.x)%16 == 0 ? true : false;
        bool cyF = Math.Floor(castPos.y)%16 == 0 ? true : false;

        // xもyも16で割り切れたら強制ステート0
        if(state.endGridMove(cxF && cyF)) {
            // 位置補正
            castFront.transform.position = new Vector3((float)Math.Floor(castPos.x),(float)Math.Floor(castPos.y),0);
            player.transform.position = new Vector3((float)Math.Floor(playerPos.x),(float)Math.Floor(playerPos.y),0);
        }
    }
}

ドット移動の方もspeed変数をStateのものからとるようにしました。

これで引く処理は完成ですね!

……実際に動かしてみたら下からと左側から引っ張ったら普通に動いたのですが、
右からと上から引っ張るとうまく動きませんでした。

というのも、割り切れるかどうかの計算のところで小数点切り落としをしているので
上向きと左向きのときは計算式を少し変更しなければいけません。

最後にココだけ修正しましょう。

    // 割り切れたら位置補正
    private void fixedGridPosition(State_Player state, RaycastHit2D castFront, GameObject player) {
        // 見やすいようにローカル変数を使用
        Vector3 castPos = castFront.transform.position;
        Vector3 playerPos = player.transform.position;
        // 割り切れたらTrue
        bool cxF = false;
        bool cyF = false;
        switch(state.Direction) {
            case 0:
            case 2:
                cxF = Math.Ceiling(castPos.x)%16 == 0 ? true : false;
                cyF = Math.Ceiling(castPos.y)%16 == 0 ? true : false;
                break;
            case 1:
            case 3:
                cxF = Math.Floor(castPos.x)%16 == 0 ? true : false;
                cyF = Math.Floor(castPos.y)%16 == 0 ? true : false;
                break;
            default:
                break;
        }


        // xもyも16で割り切れたら強制ステート0
        if(state.endGridMove(cxF && cyF)) {
            // 位置補正
            switch(state.Direction) {
                case 0:
                case 2:
                    castFront.transform.position = new Vector3((float)Math.Ceiling(castPos.x),(float)Math.Ceiling(castPos.y),0);
                    player.transform.position = new Vector3((float)Math.Ceiling(playerPos.x),(float)Math.Ceiling(playerPos.y),0);
                    break;
                case 1:
                case 3:
                    castFront.transform.position = new Vector3((float)Math.Floor(castPos.x),(float)Math.Floor(castPos.y),0);
                    player.transform.position = new Vector3((float)Math.Floor(playerPos.x),(float)Math.Floor(playerPos.y),0);
                    break;
                default:
                    break;
            }
        }
    }

もうちょっと綺麗な書き方が出来たかもしれませんが、一旦これで行きましょう。

マイナス方向に行く時は切り捨てをしていましたが、プラス方向に行く時は切り上げを行います。

Math.Ceilingが切り上げになりますね。

しかしやってみるとわかるのですが、最後の補正でカクッってなってしまいます。

この辺は後に最適化して滑らかに移動完了させたいと思います。

とりあえずこれでOKとしましょう。

それでは。