プレイヤーの動きをマネさせるには

# プレイヤーの動きをマネさせるには


### 目次

- こんなのを作りました！
- 全体の処理の流れ
- 具体的なコード


## こんなのを作りました！
### 作成した機能の概要

- プレイヤーの動きをマネするオブジェクト（以下「カゲ」と略称）を作成しました。
この機能は大きく2つの機能に分けられます。

  - プレイヤーの動きを記録・保存する「録画」の機能
  - その動きをカゲにマネさせる「再生」の機能


### 実装した機能のGIF画像
- 下記に実装した「録画」の機能と「再生」の機能のGIF画像を掲載します。
赤い色のやつがユーザーが操作する「Player」で赤い奴と同じ形状をした黒いやつがPlayerの動きをマネする「カゲ」です。

プレイヤーの動きを「録画」する機能のGIF画像↓
![画面録画-2025-01-13-105445.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758326/656ed1d4-a66f-dd68-99b7-14695d6151cf.gif)


プレイヤーと同じ動きをカゲに「再生」させる機能のGIF画像↓
![画面録画-2025-01-13-105526.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758326/87cb1c5b-db2c-31f3-c793-bafd85f8583e.gif)


## 全体の処理の流れ
- 全体の処理の流れは下記のようです。
　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758326/03bed261-eae7-c822-dbfc-826f90ae0cf0.png)


## 具体的なコード（「録画」の機能編）
### Step1：フレームデータの構造を定義
- プレイヤーの状態を保存するためのデータ構造を作成します。
```c#:PlayerRecoder.cs
/// <summary>
/// プレイヤーの状態を1フレーム単位で記録する構造体
/// </summary>
[System.Serializable]
public class RecordedFrame
{
    public float time; // 記録時の時間
    public Vector3 position; // プレイヤーの位置
    public Vector3 localScale; // スケール
    public float animSpeed; // アニメーション速度
    public bool animIsJumping; // ジャンプ中かどうか
}

```

### Step2：録画の開始と停止
- **録画を開始する関数 (StartRecording)**
後から現れますがUpdate関数でプレイヤーの状態を記録するので「今」録画しているかどうかを「IsRecording」というフラッグで管理します。
```c#:PlayerRecoder.cs
/// <summary>
/// 録画開始
/// </summary>
public void StartRecording()
{
    if (IsRecording) return;

    IsRecording = true;
}
```

- **録画を停止する関数 (StopRecording)**

```c#:PlayerRecoder.cs
/// <summary>
/// 録画停止
/// </summary>
public void StopRecording()
{
    if (!IsRecording) return;

    IsRecording = false;
    Debug.Log("録画終了: " + frames.Count + " フレームが記録されました");
}

```

上記の２つの関数を録画ボタンや録画停止ボタンが押された際に呼び出します。

### Step3：フレームデータの記録
- Update関数内でフレーム単位でプレイヤーの状態を保存するためのRecordFrame関数を呼び出します。
この時に先ほど管理した「IsRecording」フラッグが使用されています。
```c#:PlayerRecoder.cs
    void Update()
    {
        if (IsRecording)
        {
            RecordFrame();
        }
    }

```

- 録画機能の中核となるのが「RecordFrame」関数です。この関数は毎フレーム呼び出され、プレイヤーの状態をRecordedFrameクラスのインスタンスに記録してリストに追加します。以下に、関数の主な役割と処理内容を説明します。

### 関数の役割
1. **プレイヤーの状態を保存する**
プレイヤーの現在の**位置**、**スケール**、**アニメーションの速度**や**ジャンプ状態**など、ゲームプレイに必要な情報をフレームごとに記録します。

2. **リストにフレームデータを蓄積する**
記録した各フレームデータをリストに追加し、後でリプレイ機能で活用できるようにします。
```c#:PlayerRecoder.cs
/// <summary>
/// プレイヤーの状態をフレーム単位で記録
/// </summary>
private void RecordFrame()
{
    // フレームデータの作成
    RecordedFrame frame = new RecordedFrame
    {
        time = Time.time,                     // 現在の時間
        position = transform.position,        // プレイヤーの位置
        localScale = transform.localScale     // プレイヤーのスケール
    };

    // プレイヤーのアニメーション情報を追加
    if (animator != null)
    {
        frame.animSpeed = animator.GetFloat("Speed");        // アニメーション速度
        frame.animIsJumping = animator.GetBool("isJumping"); // ジャンプ中かどうか
    }

    // フレームデータをリストに追加
    frames.Add(frame);
}
```
### 全体の流れの解説
1. **RecordedFrameの作成**
この部分では、現在のフレームの情報をRecordedFrameオブジェクトとしてまとめています。位置やスケールなどの基本的な情報が含まれます。

2. **アニメーション情報の追加**
アニメーションの進行状況（例: 歩く速度やジャンプ中かどうか）を保存することで、再生時にプレイヤーの動作をよりリアルに再現します。

3. **リストへの追加**
最後に、記録したフレームデータをframesリストに追加します。このリストが全録画データを保持し、再生時に利用されます。
## 具体的なコード（「再生」の機能編）
### Step1：録画データの再生開始を行うTryCreateShadow()
```c#:PlayerRecoder.cs
public void TryCreateShadow()
{
    if (frames.Count == 0)
    {
        Debug.Log("No recording data!");
        return;
    }

    // 既存の影オブジェクトがある場合は削除
    if (currentShadowObj != null)
    {
        Destroy(currentShadowObj);
        currentShadowObj = null;
    }

    // 新しい影オブジェクトを生成
    currentShadowObj = Instantiate(shadowPrefab, frames[0].position, Quaternion.identity);
    currentShadowObj.SetActive(false);

    // 影に録画データを渡す
    ShadowReplayer replayer = currentShadowObj.GetComponent<ShadowReplayer>();
    if (replayer != null)
    {
        replayer.InitReplay(frames.ToArray());
        currentShadowObj.SetActive(true);
    }
}
```

### 全体の流れの解説
- ここでは主にカゲをインスタンス生成してそれまでに記録されたデータをカゲにアタッチされているスクリプトの[ShadowReplayer]で定義されているInitReplay関数に渡しています。
- 注意すべき点は以下の点でインスタンス生成をした後に非アクティブ化を行っている点です。

```c#:PlayerRecoder.cs
// 新しい影オブジェクトを生成
    currentShadowObj = Instantiate(shadowPrefab, frames[0].position, Quaternion.identity);
    currentShadowObj.SetActive(false);
```
このような処理を行っている理由はカゲにアタッチされている**ShadowReplayer.csでOnEnable関数を使用してカゲがアクティブ化された時に渡されたデータを再生しようとする**のですが一度[PlayerRecoder.cs]側で非アクティブ化を挟まないと**ShadowReplayer.csにプレイヤーの動きを記録したデータを送信する前に[ShadowReplayer.cs]がデータを再生しよう**としてしまって「記録されたデータがないよ！」というエラーが出てしまうためです。

なのでShadowReplayer.csでOnEnable関数が作動する前にカゲの非アクティブ化を実行しています。

### ----------ここから先はShadowReplayer.csでの処理になります----------
### Step2：再生データの初期化を行うInitReplay()

カゲに録画データを渡して再生の準備を整えます。
```c#:ShadowReplayer.cs
public void InitReplay(RecordedFrame[] frames)
{
    replayFrames = frames;
    startReplayTime = Time.time; // 再生開始時刻を記録
    currentIndex = 0; // 再生フレームの初期化
}
```
### 全体の流れの解説
- replayFrames: 録画データを配列形式で保存。
- startReplayTime: 再生の基準時刻を記録。
- currentIndex: 現在再生中のフレームを追跡します。

先ほどのPlayerRecoder.cs内のRecordFrame関数で出てきたInitReplayです。**ここでプレイヤーの状態が記録されたデータを受け取っています。**

### Step3：OnEnable関数でReplayCourutineを呼び出す
先ほども軽く触れましたが**OnEnable関数を使用してプレイヤーの状態を保存してあるデータを再生するためのReplayCoroutine()を呼び出します。**

```c#:ShadowReplayer.cs
    void OnEnable()
    {
        // リプレイ開始
        StartCoroutine(ReplayCoroutine());
    }

```

### Step4：再生処理の流れを管理するReplayCoroutine()
影の動きをフレームごとに再現するためのコルーチン処理です。
```c#:ShadowReplayer.cs
IEnumerator ReplayCoroutine()
{
    if (replayFrames == null || replayFrames.Length == 0)
    {
        Debug.Log("No replay frames to play.");
        yield break;
    }

    float baseTime = replayFrames[0].time;

    while (currentIndex < replayFrames.Length)
    {
        // 経過時間に基づいてフレームを再現
        float elapsed = Time.time - startReplayTime;
        float targetTime = baseTime + elapsed;

        while (replayFrames[currentIndex].time <= targetTime)
        {
            ApplyFrame(replayFrames[currentIndex]);
            currentIndex++;
        }

        yield return null;
    }

    // 再生終了後に影を削除
    Destroy(gameObject);
}

```
### 全体の流れの解説
- **録画データの有無をチェック**: 再生データが空の場合は再生を中断。
- **経過時間に基づく再生**: elapsed で経過時間を計算し、各フレームのタイミングに合わせて ApplyFrame() を呼び出します。
- **再生後に自動削除**: 再生が終了した影は削除されます。

ここで記述されているApplyFrame関数がカゲに対して記録されたデータを適応させている関数になります。

### Step5:各フレームの再生処理する関数であるApplyFrame()
録画データの1フレーム分を再現します。
```c#:ShadowReplayer.cs
private void ApplyFrame(RecordedFrame frame)
{
    // 位置・スケール・アニメーション適用
    transform.position = frame.position;
    transform.localScale = frame.localScale;

    if (animator != null)
    {
        animator.SetFloat("Speed", frame.animSpeed);
        animator.SetBool("isJumping", frame.animIsJumping);
    }
}

```
### 全体の流れ
- **位置とスケールの適用**: 録画データに基づいて影の位置とスケールを更新。
- **アニメーションの再現**: 録画時のアニメーションパラメータ（animSpeed, isJumping）を設定。

### ここまでできれば実装完了！
- おそらくこれで上手く処理できるはずです・・・！懸念点があるとしたらanimationが多分皆さんとは違うのでそこだけはうまく動作するのだろうか・・・という感じです。

## まとめ
- 今回実装した機能は拡張性や改良の余地がふんだんにあります。今回は長くなる過ぎるのでそこまで書きませんでしたが例えば

    - プレイヤーの位置やアニメーションだけでなく、プレイヤーのInputの状態も管理したい！（攻撃ボタンとか話しかけるボタンとか）
    - 本当に1フレームずつ保存する必要あるの？それは逆に処理に対して不必要な負荷がかかりすぎているんじゃない？

ここらへんが主な改善点にはなります。
ここまで読んでくださってありがとうございました！この記事があれかの役に立てれば幸いです！