3DCG覚書とかとか

参考書のすみっこ

~普段メモとかしても忘れちゃうのでブログにしました~

目指せUnityマスター【Unity1週間ゲームジャム①】

Q.何この記事?
A.新卒Webエンジニアが、Unityマスターになるまでの過程を描いていく記事。
Unity1週間ゲームジャム参加レポート?(゚∀゚)

今回の目玉は...オブジェクトへの追尾(AddForceでの実現)とかかな?あとロックオンの時の凡ミスとか、残段UI実現のアイデアとか。

今週やったこと

Unity1週間ゲームジャムに参加してた。
1日ごとの進捗を半分ぐらいに分けて記事にしておこうかな。

▼URL

unityroom.com

目次

1日目

企画

まず8/10(月) 0:00 にお題が公開されて、「増える」がテーマだった。
なのでガンダムのフルバーストみたいなのでミサイルとかレーザーとかがどんどん増えていくのをやろうと思った。

ペース配分が良くわからなかったので、すぐに企画して↓みたいなイメージを作っておいた。

f:id:kuraudo0309:20200810112456j:plainf:id:kuraudo0309:20200810112501j:plain
ゲームイメージ

イメージには「ボスに挑む」があったけど、正直そこまで手が回らなかった....。
あとステータス強化画面で強化時にエフェクト付けるの忘れてた。今気づいた( ゚д゚ )

んで、その後すぐ寝て実装に備えた。

実装

まずこのゲームでこだわりたいと思ったのは「ミサイルの挙動」だったから、まずミサイルを完成させねばと思った。

んで、↓みたいなのをその日までに作った。

f:id:kuraudo0309:20200819215754g:plainf:id:kuraudo0309:20200819215827g:plain
ミサイル挙動&発射

この時は「止まっている物体」に対して追尾するだけだが、物体に対して力を加えるだけだと”すでに自分に加わっている別方向の力”が邪魔をして永遠にグルグルし続ける。
そのためすでに自分に加わっている力から相手の方向へと力を加えることで何とかしようとした。

f:id:kuraudo0309:20200819222529j:plain
追尾方向の力

ただこれには問題があって、既に加わっている力は物体への方向ベクトルと合成されてしまっていて、どうしても物体の少し奥に収束してしまう。
物体の向きに対して垂直方向のベクトルを求めようともしたが、脳みそが足りなかった(´・ω・`)

なので反対方向の力をその力の大きさによって(物体への方向ベクトルとの角度によって)変えるために、↓のような式になった。

    //Enemyオブジェクトへ向かうための方向ベクトル補正値を計算する
    Vector3 CalcRevisionForceToEnemyDirection()
    {
        Vector3 revisionDirection = ((1f - (this.vectorToEnemy - this.moveDirection).magnitude/MAGNITUDE_OF_VECTOR3) * this.vectorToEnemy) - moveDirection;

        return revisionDirection*3.5f;
    }

要するに自分が動いている力が小さいほど反発させる力が小さくなるように設定した。
微妙な値はなんとなくこの値が合っただけであり、MAGNITUDE_OF_VECTOR3は三次元ベクトルそれぞれの単位ベクトルを合成したときのベクトル。

2日目

1・2日目が一番疲れたかもしれない。相手が動くとミサイルが全然追尾しないことに気づいて、あの手この手で試行錯誤した。その結果↓みたいなイメージになった。

f:id:kuraudo0309:20200819224425j:plain
移動予測

力を加えるのでなく、対象の移動地点を予測してそこに対して追尾させるようにした。
ただ対象は一定速度で動かしてるけどミサイルは加速度を与えているので、もちろんそう簡単には行かない。 なので速度を上げても対応できるように、近似曲線で補正をかけることにした。

この時、ある速度に対して逆方向に減速をかける割合を、補正値を変えながら3つぐらいやったら、
(加える力,補正値)としたとき、
(1000,1.5)
(1500,1.2)
(3000,0,8)
という感じになったので、これを近似すると反比例な感じになる。しかしそれでは急峻すぎるので全体の平方根をとりながら考えた時、
0.8=√(x/3000) であり、1.5 = √(x/1000)であるxを求めると、

f:id:kuraudo0309:20200819230139p:plain
近似
こんな感じのグラフになった。結果としてx=1920となって、

//Enemyオブジェクトの移動速度ベクトルを取得
    Vector3 CalcPredictDirection()
    {
        Vector3 enemySpeedVector = this.target.GetComponent<MoveForward>().GetSpeedVector();
        return enemySpeedVector*Mathf.Sqrt(BASE_FORCE_MAGNITUDE/this.ForceMagnitude);
    }

という感じに割り出した。BASE_FORCE_MAGNITUDEに1920が入っている。
そして最終的な軌道は、

   void AddResultForce()
    {
        Vector3 forceToEnemy = this.vectorToEnemy;
        Vector3 revisionForce = this.CalcRevisionForceToEnemyDirection();
        Vector3 resultForce = forceToEnemy + revisionForce;
        rb.AddForce(resultForce * ForceMagnitude * Time.fixedDeltaTime, ForceMode.Force);
    }

こんな感じになっている。ここまでコンパクトになるまでかなり長かったが、おおむね期待通りの動きになって満足した。

3日目

たしかここでミサイルのロックオンとUI反映機構を作った気がする。

ミサイルロックオン

まず流れとして、
1.右クリックホールドでロックオン
2.左クリックで発射
という感じにしたかった。

なので右クリックを推している最中は、”Enemyタグが付いている&ロックオンされていない”オブジェクトを一定間隔で取得するコルーチンを作成して、右クリックを押したらそれが呼ばれるようにした。

▼ロックオン処理

    IEnumerator LockOnMissileCoroutine()
    {
        //ターゲットフィールドを初期化
        this.targets = new List<GameObject>();
        GameObject[] enemies;// = GameObject.FindGameObjectsWithTag("Enemy");
        int count=0;
        while(this.stock < MAX_STOCK)
        {
            /*if (count < enemies.Length && enemies.Length>0)
            {*/
            //targets.Add(enemies[count]);
            if (canShoot == false)
            {
                break;
            }
            if (Input.GetKey(KeyCode.Mouse1))
            {
                enemies = GetCollideSphereRayGameObjects();//GameObject.FindGameObjectsWithTag("Enemy");
                //何してもフリーズする。治した。
                if (enemies.Length > 0)
                {
                    int prevCount = targets.Count;
                    AddSearchTargetToTargetList(enemies, targets);
                    if (prevCount != targets.Count)
                    {
                        //UIにターゲットアイコンを生成
                        gameManager.GetComponent<BattleUIController>().CreateTargetEnemyIcon(targets[count]);
                        count++;
                    }
                    //}
                    this.stock++;

                    //UIに残弾を反映
                    gameManager.GetComponent<BattleUIController>().SetBulletStock(this.stock, this.maxStock);

                    if (this.lockOnAudio)
                    {
                        this.lockOnAudio.GetComponent<AudioSource>().Play();
                    }

                    yield return new WaitForSeconds(this.lockOnSpan);
                }else{
                    yield return new WaitForSeconds(Time.fixedDeltaTime);
                }
            }
            else
            {
                yield return new WaitForSeconds(Time.fixedDeltaTime);
            }
        }
        //isLockOnFinished = true;
    }

//取得されていないターゲットを、targetsに順次追加する処理
    void AddSearchTargetToTargetList(GameObject[] enemies, List<GameObject> targets)
    {
        int index = 0;
        int prevCount = targets.Count;

        while (index < enemies.Length && targets.Count == prevCount)
        {
            if (targets.Contains(enemies[index]) == false)
            {
                targets.Add(enemies[index]);
            }
            index++;
        }
    }

//マウスカーソルの向きに球体の光線を放って、衝突したEnemyを取得している。
    GameObject[] GetCollideSphereRayGameObjects()
    {
        Ray mouseRay = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit[] hits = Physics.SphereCastAll(mouseRay,this.lockOnRadius);
        List<GameObject> gameObjects=new List<GameObject>();

        foreach (RaycastHit hit in hits)
        {
            if (hit.collider.tag == "Enemy")
            {
                gameObjects.Add(hit.collider.gameObject);
            }
        }

        return gameObjects.ToArray();
    }

▼発射処理

    IEnumerator ShootMissileCoroutine(List<GameObject> targets, int stock)
    {
        int count = 0;
        while (stock > 0)
        {
            if (Input.GetKey(KeyCode.Mouse1))
            {
                yield return new WaitForSeconds(0.01f);
                break;
            }
            foreach (GameObject missilePosition in missilePositions)
            {
                if (targets.Count <= count)
                {
                    count = 0;
                }

                Instantiate(this.missile, CalcShootPosition(missilePosition), CalcShootRotation(missilePosition.name)).GetComponent<MoveForEnemy>().SetTarget( targets.Count > 0 ? targets[count] : null);
                stock--;

                //UIにストックを反映
                gameManager.GetComponent<BattleUIController>().SetBulletStock(stock, this.maxStock);

                if (stock <= 0)
                {
                    break;
                }
                yield return new WaitForSeconds(this.shootSpan);
                count++;
            }
        }
        this.stock = 0;
        this.canLockOn = true;
        //StartCoroutine(LockOnDelay());
    }

    public Quaternion CalcShootRotation(string missilePositionName)
    {
        Vector3 rotationEuler = new Vector3(Random.Range(0f, maxMissileShootRotationX), Random.Range(0f, maxMissileShootRotationY), 0);
        if (missilePositionName.Contains("Left"))
        {
            rotationEuler = new Vector3(rotationEuler.x, -rotationEuler.y, rotationEuler.z);
        }

        return Quaternion.Euler(rotationEuler);
    }

    Vector3 CalcShootPosition(GameObject obj)
    {
        return new Vector3(obj.transform.position.x, obj.transform.position.y, obj.transform.position.z);
    }

コードが結構重たくなってしまったが、リファクタリングしている時間もないのでそのまま次に進んだ。

んで確かこの時に、

while(true){
    if(GetKey(1)){
        ... /* ここでフリーズ! */
    }else{
        yield return new WaitForSeconds(...);
    }
}

とかやらかして、一瞬でも無限ループすることになるとフリーズするんだなっていうのが分かった。(´・ω・`)

UI反映

これは、撃てる最大の数をベースとして、その上にロックオン済みのストックを重ねて実現した。
ちょっと細かく言うと、widthを20pxぐらいずつ増やすと1発分の画像が増えるように設定していた。
(動きは「今週やったこと」にある実物を見てね★)

感想

前半はこの辺にしとこうかなと思う。
ゆーて後半は敵を増やしたりシーン遷移させたりレーザー撃たせたり、、、結構やってた....。
ひょっとしたら3記事になるかもしれない。

でも1週間でゲーム作るなんて正気の沙汰じゃないと思ってたけど、意外と何とかなるもんなんだなと思った。まぁお盆期間中だったからってのが大きいけどね。
んでやっぱり作ってみて、最初のプランニングが一番大事な気がした。イメージが固まっていればいるほど、それに沿ったプログラムを書いたりエフェクトを作ったりすればいいだけだから、そうじゃなきゃ仕様変更の雨あられで大変なことになるなと思った。
今回は多少イメージがあったから何とかなったけど、それでも後悔してるのでプランニングは大事(`・ω・´)