iT邦幫忙

2022 iThome 鐵人賽

DAY 20
0

我們平常可能會希望可以回顧該使用者的動作或是一些姿態的轉動方向,這時候就需要不斷的在一段時間內紀錄受試者的動作。所以今天我希望能夠跟大家說明並且以簡單的方式記錄在一段時間中該一個物件的Position移動的位置, 並且能夠讓我們進行回顧一下其在這段時間中移動的方向。今天的程式碼部分需求量比較大。

建置場景

  1. 首先我設計一個 Car 物件,這個 Car 物件包含了 Trail Renderer,因為我希望之後能讓觀眾可以看到該位移的變化。場景如下:

  2. 以下這邊我也新增一個 CarMovement.cs 的腳本,目的是要讓Car 能夠根據我鍵盤的 w,a,s,d 縱向與橫向的位移。程式碼非常的簡單,如以下結果:

  3. 我們撰寫程式碼。該就是取得 Mouse 移動的位置,並且更改 Car 的位置。

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

public class CarMovement : MonoBehaviour
{
    
    public float sideForce = 50f;
    public float forwardForce = 80f;

    
    void Update()
    {
        float dx = Input.GetAxis("Horizontal");
        float dz = Input.GetAxis("Vertical");
        transform.position += new Vector3(dx * sideForce * Time.deltaTime, 0f, dz * forwardForce * Time.deltaTime);
    }
}
  1. 回到 Unity 後執行看看。關於Trail Renderer 就希望讀者自行設計就好,這邊就給大家參考。

紀錄 Movement的 Class 與 List 紀錄類別物件的方式

MovementData.cs

  1. 首先要能記錄物件的移動,就會需要紀錄物件的 Position , Position 主要就是物件的 Vector3 所產生的,因此需要能記錄 Vector3 的 List。
  2. 要注意到這邊的 Class 並沒有繼承 MonoBehavior 這邊僅作為儲存 Vector3 資料與調用資料的地方。所以先寫好架構。
[Serializable]
public class MovementData
{
	[Serializable]
	public class MovePosition
	{

	}
	
}


  • MovementData 是主要的架構,設定 MovePosition List 儲存該物件。
  • MovePosition 是負責儲存Position 資料的地方。
    **目的就是要在主要的架構(MovementData) 下面宣告 List,去儲存該類別(MovePosition)的物件,這樣子每個物件可以獨立調用 MovePosition 的內部參數。 **
  1. 接下來就是宣告物件的地方。先看到 MovePosition 內部。
[NonSerializable]
public class MovePosition
{
	[NonSerialized]
        public List<Vector3> Movements = new List<Vector3>();
        public List<string> MovementsStr = new List<string>();
        public List<float> DirectionForward = new List<float>();

        public void positionToString() 
        {
            if(Movements.Count > 0)
            {
                MovementsStr.Clear();
            }

            foreach(Vector3 movement in Movements)
            {
                string str = movement.x.ToString() + ", " + movement.y.ToString() + ", "  + movement.z.ToString();
                MovementsStr.Add(str);
            }
        }

        public void StringToPosition() 
        {
            if(MovementsStr.Count > 0)
                Movements.Clear();
            
            foreach(string str in MovementsStr)
            {
                string[] arr = str.Split(',');
                Vector3 vec = new Vector3(float.Parse(arr[0]), float.Parse(arr[1]), float.Parse(arr[2]));
                Movements.Add(vec);
            }
        }
} 

每個 MovePosition 物件都會需要存入傳進來的 Vector3 List的資料。

public List<Vector3> Movements = new List<Vector3>()

接下來是我們可能會希望顯示List 中的 Vector3 的 Position 這邊新增兩種方法

  • void PositionToString( )
  • void StringToPosition( )
public List<string> MovementsStr = new List<string>();
public void PositionToString()
{
	if(Movements.Count > 0)
		MovementsStr.Clear();

	foreach(Vector3 movement in Movements)
	{
		string str = movement.x.ToString() + "," + movement.y.ToString() + "," + movement.z.ToString();
		MovementsStr.Add(str);
	}
}

public void StringToPosition()
{
	if(MovementsStr.Count > 0)
		Movements.Clear();
	
	foreach(string s in MovementsStr)
	{
		string[] str = s.Split(',');
		Vector3 vec = new Vector3(float.Parse(str[0]), float.Parse(str[1]), float.Parse(str[2]));
		Movements.Add(vec);	
	}
}

這邊使用了兩種新的方法:

  • stringName.Split(根據你要區別的字元作輸入)
  • float.Parse(強制string 轉型成 浮點數)
  1. 接下來是透過 MovePosition 的物件List 來新增 Vector3 Movements 到各自的物件中。
public List<MovePosition> MovesPositionObjList = new List<MovePosition>();

這邊就是透過 List 儲存 MovesPosition 的物件,這個物件可以使用關於 MovePosition物件中的 List 儲存 vector3 。所以說要一個能引入List 以 Vector3型態,並且透過MovePosition 物件來轉換該物件下面的 Movements List設為 傳入的 _movesList。

public void InsertMoves(List<Vector3> _movesList)
{
	MovePosition moves = new MovePosition();
	moves.Movements = _movesList;
	this.MovesPositionObjList.Add(moves);
	
}
  1. 這邊有其他的方法,這個方法是紀錄該物件下的 DirectionForward 數值與 _moveList 但目前不會用到。
public void InsertMoves(List<Vector3> _movesList, List<float> _directionForward)
    {
        MovePosition moves = new MovePosition();
        moves.Movements = _movesList;
        moves.DirectionForward = _directionForward;
        this.MovesPositionObjList.Add(moves);
    }
  1. 這邊有為了要將資料從檔案移出使用時須要使用到的方法,之前會將資料儲存 .dat 檔案後,後面需要時拿來使用,所以說要先把資料傳換成 string 的方式。所以說這邊有 store 與 load 的方法。
public void SaveMoves() 
{
	foreach(MovePosition mo in MovesPositionObjList)
	{
		mo.PositionToString();
	}
}


public void loadMoves()
{
	foreach(MovePosition mo in MovesPositionObjList)
	{
		mo.stringToPosition();
	}
}
  1. 接下來是新增該物件時可能所需要的 name 顯示該目前 MovesPositionObjList List 中元素的數量。 並且新增讓該 MovePosition List 中元素刪除的方法。
public void CleanMovements() 
{
	MovesPositionObjList.Clear();
}
  1. 總程式碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]
public class MovementData
{
    public string name;
    public List<MovePosition> MovesPositionObjList = new List<MovePosition>();

    [Serializable]
    public class MovePosition
    {
        [NonSerialized]
        public List<Vector3> Movements = new List<Vector3>();
        public List<string> MovementsStr = new List<string>();
        public List<float> DirectionForward = new List<float>();

        public void PositionToString() 
        {
            if(Movements.Count > 0)
            {
                MovementsStr.Clear();
            }

            foreach(Vector3 movement in Movements)
            {
                string str = movement.x.ToString() + ", " + movement.y.ToString() + ", "  + movement.z.ToString();
                MovementsStr.Add(str);
            }
        }

        public void StringToPosition() 
        {
            if(MovementsStr.Count > 0)
                Movements.Clear();
            
            foreach(string str in MovementsStr)
            {
                string[] arr = str.Split(',');
                Vector3 vec = new Vector3(float.Parse(arr[0]), float.Parse(arr[1]), float.Parse(arr[2]));
                Movements.Add(vec);
            }
        }
    }

    public void InsertMoves(List<Vector3> _movesList)
    {
        MovePosition moves = new MovePosition();
        moves.Movements = _movesList;                       // store the list of the Vector3 _moves
        this.MovesPositionObjList.Add(moves);                          // Moves is the list of the object, then stroe the objects
    }

    public void InsertMoves(List<Vector3> _movesList, List<float> _directionForward)
    {
        MovePosition moves = new MovePosition();
        moves.Movements = _movesList;
        moves.DirectionForward = _directionForward;
        this.MovesPositionObjList.Add(moves);
    }

    public void SaveMoves() 
    {
        foreach(MovePosition mo in MovesPositionObjList)
        {
            mo.PositionToString();                              // mo is the List of Moves, and mo is an object from MovePosition so here use object to do that the position from float to string
        }
    }

    public void loadMoves() 
    {
        foreach(MovePosition mo in MovesPositionObjList)
        {
            mo.StringToPosition();
        }
    }

    public override string ToString() 
    {
        string str = this.name + ", Len(Moves): " + MovesPositionObjList.Count.ToString();
        return str; 
    }

    public void CleanMovements() 
    {
        MovesPositionObjList.Clear();
    }
  
}

主程式撰寫

RecordManager.cs

  1. 首先我們宣告一下UI上會需要使用到的兩個 Text。目的是要去顯示倒數的秒數與紀錄動作的秒數。
// UI Text
    public Text recordTimeText;
    public Text startTimeText;

回到 Unity 設計一下我們的 Text

  1. 之後我們要有兩個 GameObject 一個是主要物件 MainObj,另外一個是回顧剛剛動作的物件 RecordObj。
// record Object, main Object
    public GameObject MainObj;
    public GameObject RecordObj;

回到 Unity 新增兩個Car 物件,紅色是我主要移動的物件,綠色為該紀錄物件的動作並回顧剛剛主物件的動作。當然用方塊取代也可以。

  1. 宣告一下在我們環境時需要使用到的時間或是判斷變數,每個變數我都撰寫註解可以參考。
// control start time
    int startCount = 3;             // ready to start time 
    int recordPlayObjCount = 0;         // show the recored time
    const int RecordCount = 5;      
    int recordCount = RecordCount;  // record the object move time

    bool recoredStart = false;      // check ready to start
    bool isRecordPlay = false;       // check ready to record
  1. 首先當我們撰寫紀錄的方法,以及開始主程式的進行。
  • 首先我們要宣告 MovementData( ) 的物件。
moveData = new MovementData();
  • 重新設定我們倒數計時開始的時間,以及記錄過程的時間。
recordCount = RecordCount;
startCount = 3;
  • 接下來就是顯示我們倒數計時的時間在我們的 Text 上。
startTimeText.text = startCount.ToString();
  • 這邊要注意到 InvokeRepeating 是負責喚醒一個 void Function,後面的兩個變數就是設定該Function 每次執行的秒數與重複執行的時間區隔。

  • OnRecord( ) 完整的程式:

void OnRecord() 
    {
        moveData = new MovementData();
        recordCount = RecordCount;
        startTimeText.text = startCount.ToString();
        startCount = 3;                                 // need to restart the time 

        // start to minus time and ready to record
        InvokeRepeating("StartRecord", 1, 1);
    }
  1. 進入到開始倒數與透過判斷來觸發紀錄物件的 Function,這邊當 startCount 計數到 0 後,我們就開始記錄該物件的位置,並且開始倒數紀錄的時間 recordCount。透過 recordStart 的判斷來切換該狀態,並且透過 recordStart 同時觸發紀錄物件的方法。
// here is to control time and set the bool to record movement
    void StartRecord()
    {
        if(!recoredStart)
        {
            startCount--;
            startTimeText.text = startCount.ToString();
            if(startCount == 0)
            {
                recoredStart = true;                     // start to record mainObj movement
                startTimeText.text = "===Start to Record!===";
            }
        }else{
            recordCount--;
            if(recordCount >= 0)
            {
                recordTimeText.text = recordCount.ToString();
            }
            
            if(recordCount == -1){  
                recoredStart = false;                    // stop record!
                CancelInvoke("StartRecord");             // stop to Invoke the void function
                Debug.Log("Success to record movement!");
            }
        }

    }
  1. 以下就是透過 recordedStart 來判斷是否開始記錄 MainObj 的位置。當 recordedStart 為真就會記錄該原物件的位置。我們先撰寫一個 List 來儲存該 Main Object 的Vector3 的位置,接著透過 moveData 物件去調用該 InsertPosition 的方法,存入我們 positionList。
void RecoredObjMovement() 
    {
        if(recoredStart)
        {
            //print("Record Object Position....");
            List<Vector3> positionList = new List<Vector3>();
            positionList.Add(MainObj.transform.position);
            moveData.InsertPosition(positionList);
        }
    }
  1. 執行回顧動作的方法,如果 isRecordPlay 為真的話就會開始回顧動作。這邊我們利用 recordPlayObjCount 來當作我們List 的索引。並且透過這個索引來獲取在 MovementData.MovePosition 的物件。利用這個物件來獲取該 Vector3 的 List,並且使其帶入到我們回顧的物件 RecordObj.transform.position。最後就reset 我們的 isRecordPlay 與 recordPlayObjCount。
void RecordedObjPlay() 
    {
        if(isRecordPlay)
        {
            //print("Play Record...");
            if(recordPlayObjCount < moveData.movePositionsObjList.Count)
            {
                MovementData.MovePosition movePosObj = moveData.movePositionsObjList[recordPlayObjCount++];
                foreach(Vector3 vec in movePosObj.MoveVector3List)
                {
                    RecordObj.transform.position = vec;
                }
                
            }else{
                // reset the variables
                isRecordPlay = false;
                recordPlayObjCount = 0;
            }
        }
    }

以下就是去控制是否要開始回顧剛剛的動作。加上控制去執行。

void PlayRecord()
    {
        if(moveData != null) 
        {
            isRecordPlay = true;
        }
    }
  1. 撰寫控制,要知道雖然 RecordObjMovement( ) 與 RecordedObjPlay( ) 都在Update 內部,但不代表會去執行,因為若內部的判斷沒有成立就不會執行。
void Update() 
    {
        if(Input.GetKeyDown("p"))
        {
            Debug.Log("Start to record movement!");
            OnRecord();
        }


        if(Input.GetKeyDown("l"))
        {
            Debug.Log("Start to play movement!");
            print("Start to play movement!");
            PlayRecord();
        }


        // check record and play record
        RecoredObjMovement();
        RecordedObjPlay();

    }

完整的程式如下

RecordManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RecordManager : MonoBehaviour
{
    // UI Text
    public Text recordTimeText;
    public Text startTimeText;

    // record Object, main Object
    public GameObject MainObj;
    public GameObject RecordObj;

    private MovementData moveData;

    // control start time
    int startCount = 3;             // ready to start time 
    int recordPlayObjCount = 0;         // show the recored time
    const int RecordCount = 5;      
    int recordCount = RecordCount;  // record the object move time

    bool recoredStart = false;      // check ready to start
    bool isRecordPlay = false;       // check ready to record

    void Update() 
    {
        if(Input.GetKeyDown("p"))
        {
            Debug.Log("Start to record movement!");
            OnRecord();
        }


        if(Input.GetKeyDown("l"))
        {
            Debug.Log("Start to play movement!");
            print("Start to play movement!");
            PlayRecord();
        }


        // check record and play record
        RecoredObjMovement();
        RecordedObjPlay();

    }

    // step 1, record the object
    void OnRecord() 
    {
        moveData = new MovementData();
        recordCount = RecordCount;
        startTimeText.text = startCount.ToString();
        startCount = 3;                                 // need to restart the time 

        // start to minus time and ready to record
        InvokeRepeating("StartRecord", 1, 1);
    }

    // here is to control time and set the bool to record movement
    void StartRecord()
    {
        if(!recoredStart)
        {
            startCount--;
            startTimeText.text = startCount.ToString();
            if(startCount == 0)
            {
                recoredStart = true;
                startTimeText.text = "===Start to Record!===";
            }
        }else{
            recordCount--;
            if(recordCount >= 0)
            {
                recordTimeText.text = recordCount.ToString();
            }
            
            if(recordCount == -1){  
                recoredStart = false;
                CancelInvoke("StartRecord");             // stop to Invoke the void function
                Debug.Log("Success to record movement!");
            }
        }

    }

    void PlayRecord()
    {
        if(moveData != null) 
        {
            isRecordPlay = true;
        }
    }


    void RecoredObjMovement() 
    {
        if(recoredStart)
        {
            //print("Record Object Position....");
            List<Vector3> positionList = new List<Vector3>();
            positionList.Add(MainObj.transform.position);
            moveData.InsertPosition(positionList);
        }
    }

    void RecordedObjPlay() 
    {
        if(isRecordPlay)
        {
            //print("Play Record...");
            if(recordPlayObjCount < moveData.movePositionsObjList.Count)
            {
                MovementData.MovePosition movePosObj = moveData.movePositionsObjList[recordPlayObjCount++];
                foreach(Vector3 vec in movePosObj.MoveVector3List)
                {
                    RecordObj.transform.position = vec;
                }
                
            }else{
                // reset the variables
                isRecordPlay = false;
                recordPlayObjCount = 0;
            }
        }
    }



}

MovementData.cs

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

[System.Serializable]
public class MovementData
{
    public List<MovePosition> movePositionsObjList = new List<MovePosition>();

    [System.Serializable]
    public class MovePosition
    {
        [NonSerialized]
        public List<Vector3> MoveVector3List = new List<Vector3>();
        public List<string> MovePosStringList = new List<string>();
        public List<float> MovePosFloatList = new List<float>();

        // let the position float change to string
        public void positionToString()
        {
            if(MoveVector3List.Count > 0)
            {
                MovePosStringList.Clear();      // clean the list
            }
            foreach(Vector3 movement in MoveVector3List)
            {
                string str = movement.x.ToString() + ", " + movement.y.ToString() + ", " + movement.z.ToString();
                MovePosStringList.Add(str);
            }
        }
        public void StringToPosition()
        {
            if(MovePosStringList.Count > 0)
            {
                MovePosStringList.Clear();
            }

            foreach(string str in MovePosStringList)
            {
                string[] splitStr = str.Split(',');
                float getstrX = float.Parse(splitStr[0]);
                float getstrY = float.Parse(splitStr[1]);
                float getstrZ = float.Parse(splitStr[2]);
                Vector3 newVec = new Vector3(getstrX, getstrY, getstrZ);
                MoveVector3List.Add(newVec);


            }
        }

    }


    // Insert the Object Position List 
    public void InsertPosition(List<Vector3> _moveList)
    {
        MovePosition moves = new MovePosition();
        moves.MoveVector3List = _moveList;
        movePositionsObjList.Add(moves);
    }

    // let the position  string value change to float value
    public void StringToPos()
    {
       foreach(MovePosition movesObj in movePositionsObjList)
       {
            movesObj.StringToPosition();
       }
    }

    // let the position float value change to string value
    public void PosToString() 
    {
        foreach(MovePosition movesObj in movePositionsObjList)
        {
            movesObj.positionToString();
        }
    }

    public void ClearObj() 
    {
        movePositionsObjList.Clear();
    }

}

CarMovement.cs

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

public class CarMovement : MonoBehaviour
{
    
    public float sideForce = 50f;
    public float forwardForce = 80f;

    
    void Update()
    {
        float dx = Input.GetAxis("Horizontal");
        float dz = Input.GetAxis("Vertical");
        transform.position += new Vector3(dx * sideForce * Time.deltaTime, 0f, dz * forwardForce * Time.deltaTime);
    }
}

回到 Unity 執行

  1. 首先確認我們的 UI Text 是否都對應好了

  2. 執行開始的時候不會有任何變化

  3. 點選 P 會開始倒數3秒讓使用者準備好。

  4. 接下來是給受試者紀錄物件移動的時間,這時候可以開始移動物件。

  5. 錄製完物件的動作後,點選 L 就會回顧剛剛物件移動的方式,很有趣完全一樣的路徑。

結論:

  1. 今天了解到如何去有效的回顧我們物件的移動,未來或許也可以應用在物件回顧旋轉的方式。
  2. 這邊介紹 InvokeRepeating 來處理 Function 的使用時間與重複使用時間上的區間,並且結束以 CancelInvoke 來停止當前的 Function 執行。所以這邊才可以看到說為何 time -- 時可以實現每一秒減少一次。
  3. 透過判斷來切換各種不同的狀態,或是幫助我們開始回顧或紀錄我們物件的移動。

上一篇
Day19: Health Bar
下一篇
Day21: Arduino with Unity
系列文
Unity 基本功能實作與日常紀錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言