【Unity】スマブラの振動するヒットストップを再現する

<br></br>
[Aizu Advent Calendar 2024](https://adventar.org/calendars/10858)の2日目の記事になります！
昨日はユオレイ先輩がすばらしい記事を書いてくださったので、まずはそちらを見てください→[Python3 と SlackBotで請求書作成してみた](https://blog.yuorei.com/entry/2024-12-01-python-pdf-invoice)
また、今後もさまざまな記事が出るため、他の方の記事も見ていただけると幸いです:bow_tone2:
# 自己紹介
![yukidaruma.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/770433e1-e726-5430-23ce-453130aa3366.jpeg)

会津大学1年の **アト** と申します！
普段はUnityを使ってゲーム開発をしたり、Unityに飽きた時はフロントエンドを触ってみたりFailったりなど...
今回、初めて出す記事のため、温かい目で見てくださると幸い...。
また、感想等もしてくださるとすごくハッピーな気持ちになれるので是非...!
# はじめに
皆さんは、[桜井政博のゲーム作るには](https://www.youtube.com/@sora_sakurai_jp)をご覧になったことはありますか？
ゲーム制作者であれば必見のYouTubeチャンネルですが、動画が更新されていた当時、私は受験期だったが故、そもそも視聴する余裕がありませんでした...。(泣)
チャンネルの最終回を迎えた頃になって、このチャンネルの存在に気づき、めっちゃ面白い、これは見ねば...！と心を動かされました。

現在は、毎日正座しながら30分ほどのまとめ動画を一本ずつ視聴する日々...。

そんな毎日をすごしていると、特にこの動画すげぇ...!というものがありました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/CiQzmwz_Olg?si=wV8E0IrjPByPogFW" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

動画をまとめると、この動画のヒットストップはただ時を止めるだけでなく、振動や様々な工夫により、攻撃が当たった時の快感をより増幅させているという内容でした。すばらしい動画...！

自分自身でもゲームを作ってはいるものの、どのゲームも演出が良くないと言われることが多く、今回の記事を通じてヒットストップを学び、次回のゲームに活かしていきたい...!

そう思い実装することとした。
# 実装
そんなわけで実装していく、
今回このようなSceneを用意し
<img width=100% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/5b7195f2-942e-92e9-722e-1b1c48953135.png">
左の球が敵に当たる→時が止まる（ヒットストップ）→球微振動、敵大振動→敵がどっかいく
ここまでの処理を実装したい。
### 0.ヒットストップがnullの場合
まず、適当なコードを書き、ヒットストップがない状態の処理を実装して動かす。
~~~unity:BulletScript
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletScript : MonoBehaviour
{
    private Rigidbody2D myrb2d;
    // Start is called before the first frame update
    void Start()
    {
        myrb2d = this.GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        myrb2d.AddForce(Vector2.right * 50);//適当な力の値を50と定めた
    }
}

~~~
~~~unity:BulletScript
using System;
using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using UnityEngine;
using Random = UnityEngine.Random;

public class ShakeScript : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D bullet)
    {
        if(bullet.gameObject.CompareTag("test"))//球にtestタグが入っている
        {
            transform.DOMove(new Vector3(10f,10f,0f),1f).SetEase(Ease.OutCirc);//やられたら画面外に出る
        }
    }

~~~

![NOShake.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/53ee905c-5071-556c-ac8c-492ba095fe04.gif)


うーん。倒したんだなぁということはわかるものの、なんか物足りない感...
何が起こっているのかも一目では分かり難い...
これはあまりよろしくない。
これらの処理をヒットストップを用いることでより良くしていきたい。
### 1.とりあえず止めてみる
ひとまず、当たってもすぐには飛んで行かないようにストップの処理を敵側に追加する。
今回はストップの処理にUniTaskを用いた。（コルーチンで処理するよりはUniTaskを使った方が早い）
~~~unity:BulletScript
    private void OnCollisionEnter2D(Collision2D bullet)
    {
        if(bullet.gameObject.CompareTag("test"))
        {
            shaking();//止める処理
        }
    }

    private async UniTask shaking()
    {
        int stopflame = 15;//止めるフレーム数
        for (int i = 0;i<stopflame;i++)
        {
            await UniTask.DelayFrame(1); //1fごとに処理
        }
        transform.DOMove(new Vector3(10f,10f,0f),1f).SetEase(Ease.OutCirc);//やられたら画面外に出る
        
    }
~~~
![littleStop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/5cdb7fde-67f8-a5fe-d5a6-e36cc4d71949.gif)

悪くない感じ！
しっかり止めることによって、何が起こっているのかが一目でわかるようになりました。
今回の場合、敵が球に当たったことで敵が吹っ飛んだということが一目でわかるようになりました。
### 2.敵側に振動を与えてみる
現状、ただ止まっているだけでも当たったということはわかるものの、それ以上にわかることがないため、敵がどうなっているのかまではわかりにくい...
そのため、敵に振動を与えてみることにした。
~~~unity:ShakeScript
    private void OnCollisionEnter2D(Collision2D bullet)
    {
        if(bullet.gameObject.CompareTag("test"))
        {
            float prex = transform.position.x;
            float prey = transform.position.y;
            shaking(prex, prey);
        }
    }

    private async UniTask shaking(float x, float y)
    {
        int stopflame = 15;//止めるフレーム数
        for (int i = 0;i<stopflame;i++)
        {
            float randomx = Random.Range(x-0.5f,x+0.5f);
            float randomy = Random.Range(y-0.5f,y+0.5f);
            transform.position = new Vector3(randomx,randomy,0);
            await UniTask.DelayFrame(1); //1fごとに処理
        }
        transform.DOMove(new Vector3(10f,10f,0f),1f).SetEase(Ease.OutCirc);//やられたら画面外に出る
        
    }
~~~
![EnemyStop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/fab0b5ba-676d-d861-3f4a-4d0fc2656676.gif)

かなりいい感じ！1.2.比べると見違えるほどには、敵の”喰らってる感”を演出することができたのではないでしょうか。
### 3.完璧にしたい
ここに、わずかに球の方も振動するように動かしてみると、
~~~unity:ShakeScript
public class ShakeScript : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D bullet)
    {
        if(bullet.gameObject.CompareTag("test"))
        {
            var transform1 = transform;
            float prex = transform1.position.x;
            float prey = transform1.position.y;
            var position = bullet.gameObject.transform.position;
            float prebx = position.x;
            float preby = position.y;
            shaking(prex, prey,prebx,preby,bullet);
        }
    }

    private async UniTask shaking(float x, float y,float bx,float by,Collision2D bullet)
    {
        int stopflame = 15;//止めるフレーム数
        for (int i = 0;i<stopflame;i++)
        {
            Vector2 randomxy = new Vector2(Random.Range(x - 0.5f, x + 0.5f), Random.Range(y - 0.5f, y + 0.5f));
            transform.position = randomxy;
            if (i<stopflame/3)//球を揺らすのは敵よりも短くありたい
            {
                Vector2 randombxy = new Vector2(Random.Range(bx - 0.05f, bx + 0.05f), Random.Range(by - 0.05f, by + 0.05f));
                bullet.transform.position = randombxy;
            }

            await UniTask.DelayFrame(1); //1fごとに処理
        }
        transform.DOMove(new Vector3(10f,10f,0f),1f).SetEase(Ease.OutCirc);//やられたら画面外に出る
        bullet.transform.position = new Vector2(bx, by);

    }
}

~~~
![FullsStop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/3d979900-e925-7025-1a99-61fa42c64461.gif)

*Excellent...*
0.と比べると一目瞭然。かなり爽快感が上がりました。

### ∞.応用編?
せっかくなので、敵を3体用意した。![スクリーンショット 2024-12-02 1.41.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/e1e1c022-130a-d9a5-acc6-b497c980d232.png)
これらも3体まとめて倒すと、
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949450/bfebf41e-575a-396c-eeff-a3aa0bd09e24.gif)

...球がやや荒ぶりすぎているのが気になるが、連続して倒している時の爽快感は凄まじい。しかし、ヒットストップを多用しすぎたことで、量が多く、遅延しているようにも感じられてしまった。
そのため、ヒットストップの時間はゲーム性によって変えていった方が良いだろう。
（たくさん倒して爽快感を得るために、ヒットストップを短くする、少ない敵を頑張って倒してゆっくりヒットストップを掛けるのか...etc）
# まとめ
* ヒットストップを有効的に使うことにより、倒した時の爽快感を引き上げることができた
* ヒットストップの実装もそこまで重くないため、負担をかけずに演出を強化することができるとわかりました
* 演出時間はゲーム性に合わせて調整すると良いかも
ぜひ、ヒットストップを活用してよりよいゲーム制作ライフを...!
<br></br><br></br><br></br><br></br><br></br><br></br>
   ~文章はここで途切れている~