頭おかしいと言われているティアキンのモドレコをUnityで再現してみる

ティアキン

まず初めに

モドレコとはなにか??

引用元:https://gamewith.jp/zelda-totk/article/show/397322

モドレコとはゼルダの伝説ティアーズオブキングダムに出てくる特殊能力で、「オブジェクトの移動を逆再生」することが出来ます。

ちなみに巷では

「モドレコ 頭おかしい」

と言われています。???一体どういう事なのでしょうか????

クリエイターの父が「ゼルダの伝説ティアーズオブキングダム」をプレイして頭おかしい頭おかしいってずっと言ってるんですがクリエ... - Yahoo!知恵袋
クリエイターの父が「ゼルダの伝説ティアーズオブキングダム」をプレイして頭おかしい頭おかしいってずっと言ってるんですがクリエイターや開発目線に立つとこれそんなにやばいんですか? やばいです。トオレループは俗にいう壁抜けバグと同じですからそのバグがどこで使っても正常に働くようにしていることからデバッグのすごさを感じます。...

知恵袋ユーザー曰く、
「動いたコードを暗記して巻き戻しているのがヤバい」らしいです。

また質問者さんのお父様はクリエイターらしく、その目線で見てもヤバいとされているモドレコを
この私めがUnityで再現してみたいと思います!!!!!

どうすればいいのかを考えてみました。

・一定時間ごとにオブジェクトの位置をVector3(座標を入れられる変数)のリスト(もしくは配列)に入れていく
・モドレコもどきを発動したらそのリストに入っている座標に沿ってオブジェクトを移動させる
・その座標をつないだ線を表示する(LineRendererというものを使います)

ざっくりとした考えですが、
これでいけるんじゃないだろうか?と思います。

まず最初に、Vector3のリストに入れた座標通りにオブジェクトを動かしてみます。

思いついたことが実現できるかという第一歩目です。

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

public class move : MonoBehaviour
{
    List<Vector3> pos= new List<Vector3>();
    float speed =5f;
    [SerializeField] private LineRenderer _rend;
    int posCount;
    // Start is called before the first frame update
    void Start()
    {
    //座標をリストに入れていく
        pos.Add(new Vector3 (-3.0f, 0f, 0.0f));//0番目
        pos.Add(new Vector3 (-1.0f, 1.5f, .0f));//1番目
        pos.Add(new Vector3 (-0.2f, 2.5f, 0.0f));//2番目
        pos.Add(this.transform.position);//3番目
        lineRenderer();
    }

    // Update is called once per frame
    void Update()
    {
    //リストに入っている座標に沿ってオブジェクトを動かしていく
        this.transform.position=Vector3.Lerp(this.transform.position,pos[pos.Count-1],speed*Time.deltaTime);

        if(Vector3.Distance(this.transform.position,pos[pos.Count-1])<0.2f){
            if(pos.Count-1>=1){
                pos.RemoveAt(pos.Count-1);
                Debug.Log(pos.Count-1);
            }
        }
        
    }

    void lineRenderer(){
        //軌道を描く
        Debug.Log("lineRenderer");
        //posCount=0;

        Vector3[] posArray =pos.ToArray();
        _rend.positionCount =posArray.Length;
        _rend.SetPositions(posArray);
        
    }
}

テストなので保存する座標は少なめにしています。

this.transform.position=Vector3.Lerp(this.transform.position,pos[pos.Count-1],speed*Time.deltaTime);

        if(Vector3.Distance(this.transform.position,pos[pos.Count-1])<0.2f){
            if(pos.Count-1>=1){
                pos.RemoveAt(pos.Count-1);
                Debug.Log(pos.Count-1);
            }

指定されたポジションと現在のオブジェクトの距離が0.2fを下回った時(めちゃくちゃ近づいてる)に、
最新の座標をリストから削除して、次のポジションを目指すことになっています。

リスト名.Countはリストの要素数を数えてくれるメソッドです。
Count-1でリストの最新の値を出すことが出来ます。
リストや配列は0番目から数え始めるため
全体の要素数-1が最新の値となります。

(0,1,4)//1000番目
(0,0.5f,4)//999番目

と並んでいる場合

pos.RemoveAt(pos.Count-1);

で1000番目を削除すると、それ以降はpos.Count-1は999番目を指すことになります。
リストに入っている最新のVector3の値に近づいたらその値を削除して
次の座標まで移動するということを繰り返していくことで動きを逆再生する事が出来ます。

lineRenderer

ヒエラルキー上で空のオブジェクトを作成し、LineRendererというコンポーネントを追加します。

この画像のように設定しました。

lineRendererを使ってリストに保存されている座標を線でつなぐエフェクトを表示します。

Vector3[] posArray =pos.ToArray();
_rend.positionCount =posArray.Length;
_rend.SetPositions(posArray);

SetPositionsによって、LineRendererに表示させたい座標をまるごと渡します。
その前にリスト名.ToArrayによってリストから配列に変換します。
(配列じゃないとSetPositionsで受け渡しできないみたいです)
LineRenderer名.positionCountで点の数を設定します。配列の要素数だけ点を用意しておきます。

後は適当なオブジェクトにこのスクリプトをつけて、インスペクターのところにLineRendererをアサインしてください。

これで試してみましょう。

配列に入れた座標に沿ってボールが移動し、その通り道にLineRendererで線を描くということはできました。

次は一定時間ごとに自分のポジションを記録させてみます。

モドレコもどきでやりたい事をまとめてみると

・0.1秒ごとに現在の座標をリストに入れる(最新の座標と現在の位置を比べて、全く動いていなかった場合、リストに入れない)
・上限を決めておく(とりあえず1000個)
・上限に達した場合、古いものから順に消していく

こんな感じになります。

Returnスクリプトを書いていきます。

①動かす用の処理

前後左右に動かすだけのスクリプト(これは本当になんでもいい)

ややこしくなることを防ぐためモドレコ発動中は操作を受け付けないようにします。

②座標記録用の関数

0.1秒ごとに、最新の記録と現在の位置を比べます。

動いていれば現在の位置を記録し、動いていなければ記録しないようにします。

これもややこしくならないために、モドレコもどき発動中は記録しないようにします。

モドレコもどきの関数

スペースキーを押した時にモドレコもどきを発動します。

座標のリストの新しい順から古い順へと遡る一番古い座標にたどり着いたらモドレコもどきを解除します。

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

public class Return : MonoBehaviour
{
    List<Vector3> pos= new List<Vector3>();
    float speed =3f;
    [SerializeField] private LineRenderer _rend;
    [SerializeField] private GameObject line;
    GameObject obj;
    bool activate=false;//モドレコ発動中かどうかの判定
    float interval=0.2f;
    float time;
    // Start is called before the first frame update
    void Start()
    {
        pos.Add(this.transform.position);
       
    }

    // Update is called once per frame
    void Update()
    {
       
        if(activate==false){
             time+=Time.deltaTime;


            // (前方移動)
        if (Input.GetKey(KeyCode.W))
        {
            transform.position += speed * transform.forward * Time.deltaTime;
        }
 
        // (後方移動)
        if (Input.GetKey(KeyCode.S))
        {
            transform.position -= speed * transform.forward * Time.deltaTime;
        }
 
        // (右移動)
        if (Input.GetKey(KeyCode.D))
        {
            transform.position += speed * transform.right * Time.deltaTime;
        }
 
        // (左移動)
        if (Input.GetKey(KeyCode.A))
        {
            transform.position -= speed * transform.right * Time.deltaTime;
        }

            if(time>=interval){//0.1秒毎
            if(Vector3.Distance(this.transform.position,pos[pos.Count-1])>0.2f){
                pos.Add(this.transform.position);
                Debug.Log(this.transform.position+":座標を追加");
               
                if(pos.Count>1000){
                    Debug.Log(pos[0]+"を削除します");
                    pos.RemoveAt(0);
                }
            }
             }
            if (Input.GetKey(KeyCode.Space))//スペースキーで発動する
            {
                modoreko();
            }
        }
        if(activate==true){

            this.transform.position=Vector3.Lerp(this.transform.position,pos[pos.Count-1],speed*2*Time.deltaTime);
            if(Vector3.Distance(this.transform.position,pos[pos.Count-1])<0.2f){
                if(pos.Count-1>=1){
                    pos.RemoveAt(pos.Count-1);
                    Debug.Log(pos.Count-1);
                }
                if(Vector3.Distance(this.transform.position,pos[0])<0.01f){
                    Debug.Log("戻ったよ");
                    activate=false;
                    Destroy(obj);
                }
            }
        }
    }

    void modoreko(){
        activate=true;
        lineRenderer();
    }


    void lineRenderer(){
        obj = Instantiate(line, Vector3.zero, Quaternion.identity);
        _rend=obj.GetComponent<LineRenderer>();
        Vector3[] posArray =pos.ToArray();
        _rend.positionCount =posArray.Length;
        _rend.SetPositions(posArray);
        
    }

}

0.1秒ごとに現在の座標を保存し続けて、リストの要素数が1000を超えた場合に古いものから削除していくようにしています。
リスト名.RemoveAt(0);で簡単に削除してしまえるから便利ですね。
一番古い値を削除した場合、自動的に前に詰めてくれるので扱いやすいです。


(0,1,0)//0番目
(0,2,0)//1番目
(0,3,0)//2番目

↓0番目を削除

(0,2,0)//0番目
(0,3,0)//1番目

というようなイメージです。

あとは
LineRendererをprefab化しておいて、インスペクターのlineに入れておきます。

lineRendererの関数の中身でさっきまでと違う点は

obj = Instantiate(line, Vector3.zero, Quaternion.identity);
_rend=obj.GetComponent();


の部分だけです。

動かす前に、Rigidbodyというコンポーネントを忘れずに追加しておいてください。

ティアキンのモドレコをUnityで再現してみるという挑戦の結果

見た目がちゃちいのとぎこちない部分はあるにしても動きを逆再生することには成功しました

ここまでお付き合いいただきありがとうございました。

しかしながら、適当にボールを動かしていたところ

一つ大きな欠点が発覚してしまいました

それは

モドレコもどきでは「重力に逆らえない」という事!!!

上から落とした後にモドレコもどきを発動した場合、その逆再生ができず陸に打ち上げられたコイキングのようにビチビチ跳ねるだけ……

この場合どうすればいいか?

モドレコ中だけrigidbodyのuse gravityを無効にすればいいのです。

↑このチェックを入れたり外したりします。

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

public class Return : MonoBehaviour
{
    List<Vector3> pos= new List<Vector3>();
    float speed =3f;
    [SerializeField] private LineRenderer _rend;
    [SerializeField] private GameObject line;
    GameObject obj;
    bool push=false;
    float interval=0.2f;
    float time;
    Rigidbody rb
    // Start is called before the first frame update
    void Start()
    {
        pos.Add(this.transform.position);
        rb= this.gameObject.GetComponent<Rigidbody>();//自身のRigidbodyを取得する
    }

    // Update is called once per frame
    void Update()
    {
       
        if(push==false){
             time+=Time.deltaTime;


            // Wキー(前方移動)
        if (Input.GetKey(KeyCode.W))
        {
            transform.position += speed * transform.forward * Time.deltaTime;
        }
 
        // Sキー(後方移動)
        if (Input.GetKey(KeyCode.S))
        {
            transform.position -= speed * transform.forward * Time.deltaTime;
        }
 
        // Dキー(右移動)
        if (Input.GetKey(KeyCode.D))
        {
            transform.position += speed * transform.right * Time.deltaTime;
        }
 
        // Aキー(左移動)
        if (Input.GetKey(KeyCode.A))
        {
            transform.position -= speed * transform.right * Time.deltaTime;
        }

            if(time>=interval){//0.1秒毎
            if(Vector3.Distance(this.transform.position,pos[pos.Count-1])>0.01f){
                pos.Add(this.transform.position);
                
               
                if(pos.Count>1000){
                    pos.RemoveAt(0);
                }
            }
             }
            if (Input.GetKey(KeyCode.Space))
            {
                modoreko();
            }
        }
        if(push==true){

            this.transform.position=Vector3.Lerp(this.transform.position,pos[pos.Count-1],speed*2*Time.deltaTime);
            if(Vector3.Distance(this.transform.position,pos[pos.Count-1])<0.2f){
                if(pos.Count-1>=1){
                    pos.RemoveAt(pos.Count-1);
                    Debug.Log(pos.Count-1);
                }
                if(Vector3.Distance(this.transform.position,pos[0])<0.01f){
                    Debug.Log("戻ったよ");
                    push=false;
                    rb.useGravity = true;
                    Destroy(obj);
                }
            }
        }
    }

    void modoreko(){
        push=true;
        rb.useGravity = false;
        lineRenderer();
    }


    void lineRenderer(){
        obj = Instantiate(line, Vector3.zero, Quaternion.identity);
        _rend=obj.GetComponent<LineRenderer>();
        Debug.Log("lineRenderer");
        Vector3[] posArray =pos.ToArray();
        _rend.positionCount =posArray.Length;
        _rend.SetPositions(posArray);
        
    }

}

rb.useGravity = true;

rb.useGravity = false;

で重力の有無を切り替えることが出来ます。

最後の座標に近づいた時、モドレコを解除するとともにuse gravityを有効にします。
モドレコ中だけ重力を無視することが出来るようになりました。

問題を解決できたのでちょっと遊んでみます。

うーん まだまだ 問題点がありますね。

空中でモドレコもどきを発動するとさっきのようにコイキング現象が起きてしまいます。

……というより、モドレコもどきを使って空中に戻ったあと、すぐにモドレコもどきを使うとそうなるのかな?

というわけでインターバルなしで使えてしまうと問題があるみたいなのでモドレコもどきにクールダウンタイムを設けることにします。

本家にもクールダウンタイムはありますからね。

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

public class Return : MonoBehaviour
{
    List<Vector3> pos= new List<Vector3>();
    float speed =3f;
    [SerializeField] private LineRenderer _rend;
    [SerializeField] private GameObject line;
    GameObject obj;
    bool push=false;
    float interval=0.2f;
    float CDtime;
    bool cooldown=false;
    float time;
    Rigidbody rb;
    // Start is called before the first frame update
    void Start()
    {
        pos.Add(this.transform.position);
        rb= this.gameObject.GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {

        if(cooldown==true){
            CDtime+=Time.deltaTime;
            if(CDtime>=5.0f){
                cooldown=false;
                CDtime=0;
             }
         }
       
        if(push==false){
             time+=Time.deltaTime;


            // Wキー(前方移動)
        if (Input.GetKey(KeyCode.W))
        {
            transform.position += speed * transform.forward * Time.deltaTime;
        }
 
        // Sキー(後方移動)
        if (Input.GetKey(KeyCode.S))
        {
            transform.position -= speed * transform.forward * Time.deltaTime;
        }
 
        // Dキー(右移動)
        if (Input.GetKey(KeyCode.D))
        {
            transform.position += speed * transform.right * Time.deltaTime;
        }
 
        // Aキー(左移動)
        if (Input.GetKey(KeyCode.A))
        {
            transform.position -= speed * transform.right * Time.deltaTime;
        }

            if(time>=interval){//0.1秒毎
            if(Vector3.Distance(this.transform.position,pos[pos.Count-1])>0.2f){
                pos.Add(this.transform.position);
                
                if(pos.Count>1000){
                    Debug.Log(pos[0]+"を削除します");
                    pos.RemoveAt(0);
                }
            }
             }
            if (Input.GetKey(KeyCode.Space))
            {

                Debug.Log("cooldown:"+cooldown);
                if(cooldown==false){
                    modoreko();
                    
                }
            }
        }
        if(push==true){

            this.transform.position=Vector3.Lerp(this.transform.position,pos[pos.Count-1],speed*Time.deltaTime);
            if(Vector3.Distance(this.transform.position,pos[pos.Count-1])<0.2f){
                Debug.Log(pos.Count-1+"番目の座標"+pos[pos.Count-1]+":座標に到達");
                if(pos.Count-1>=1){
                    
                    pos.RemoveAt(pos.Count-1);
                    Debug.Log(pos.Count-1+"番目の座標"+pos[pos.Count-1]);
                }
                if(Vector3.Distance(this.transform.position,pos[0])<0.2f){
                    Debug.Log("戻ったよ");
                    push=false;
                    rb.useGravity = true;
                    cooldown=true;
                    Destroy(obj);
                }
            }
        }
    }

    void modoreko(){
        push=true;
        rb.useGravity = false;
        lineRenderer();
    }


    void lineRenderer(){
        obj = Instantiate(line, Vector3.zero, Quaternion.identity);
        _rend=obj.GetComponent<LineRenderer>();
        Debug.Log("lineRenderer");
        Vector3[] posArray =pos.ToArray();
        _rend.positionCount =posArray.Length;
        _rend.SetPositions(posArray);
        
    }

}

これで大丈夫になったと思います。(多分。。。)

というわけで「Unityでモドレコを再現してみる」でした。

最後まで読んでいただきありがとうございました。

え・????

座標を元にもどすくらいでモドレコを再現とか吹いてんじゃねーよって???

それはホントにすみません。。。。

気が向けば今度は加速度や角度も考慮して動かしてみたいと思います。

コメント

タイトルとURLをコピーしました