【Unity2D】円状に放出する弾をつくる(2D)

弾幕STGでよくある円状に放出する弾をつくります


円状に弾を配置する

円状に弾を配置するには、円状に弾をおくことが必要になります。
以下の状態を目標にします。


上の状態を図にすると以下のようになります。
f:id:aoaoaoaoaoaoaoi:20181014224628p:plain
ここに弾を載せている円を書きこむと以下のようになります。
f:id:aoaoaoaoaoaoaoi:20181014230502p:plain
この円周上の点の座標が求められれば、円状に弾を配置することができます。

円周上の点の求め方

必要なもの
・円の中心(赤い点)の座標
・半径の長さ(円の中心(赤い点)と弾の距離)
・角度(中心(赤い点)と弾の位置をつないだときにできる角度)
f:id:aoaoaoaoaoaoaoi:20181014230134p:plain
円周上の点は
円の中心(赤い点)の座標を(a1,a2)、
半径の長さをr、
角度をθとし、
求めたい円周上の弾の座標をB(b1,b2)とおくと
以下のように求めることができます。

b1=a1+r*cosθ
b2=a2+r*sinθ
よって、B(a1+r*cosθ,a2+r*sinθ)

※cosθ、sinθの表に当てはめてみると何となくわかると思います。
下の図の半径は1、中心は原点(0,0)です。
f:id:aoaoaoaoaoaoaoi:20181014232658p:plain

これで円周上の点の求め方は分かりました。

実際に円周上に点を置いてみる

まず、ここで決めなくてはいけないことが二つあります それは 1.弾を配置する円の半径 2.配置する弾と原点を結んだときにできる角度(円周上のどこに弾を配置するか) 上の二つは任意で決めることができます。 ここでは、上の動画のようにきれいな円を作りたいので 1.弾を配置する円の半径を「2」 2.弾の配置を、0°~360°までを30°区切り(0°,30°,60°,90°,120°…330°) とします。 sinθ,cosθを求めるには以下を使います。

sinθ=Mathf.Sin(rad);
cosθ=Mathf.Cos(rad);

Sin()とCos()に渡しているradはラジアンという単位です。 このラジアンを求める(角度からラジアンへの変換)には以下を使います。

//角度
float deg=30.0f; // θ=30.0f
//ラジアン
float rad = deg * Mathf.Deg2Rad;

以上のものを使う順番に並び替えると以下のようになります。

//角度(円周上で弾を置きたい場所)を用意
float deg=30.0f;
//角度をラジアンに変換
float rad = deg * Mathf.Deg2Rad;
//cos30°、sin30°を求める
cos30°=Mathf.Cos(rad);
sin30°=sinθ=Mathf.Sin(rad);
//弾の座標を求める(中心を原点(0,0)、半径rを1とする)
弾の座標(a1+r*cosθ,a2+r*sinθ)
よって(0+1*cos30°,0+1*sin30°)

これを実際に条件を
1.弾を配置する円の半径を「2」
2.弾の配置を、0°~360°までを30°区切り(0°,30°,60°,90°,120°…330°)
として、コードを書きました。

スクリプトは円の中心にいるキャラクターにつけました。
SetTama.cs

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

public class SetTama : MonoBehaviour {

    //弾のプレハブ
    public GameObject tama;
    //一秒ごとに弾を生成するための時間管理
    private float targetTime = 1.0f;
    private float currentTime = 0;
    //角度(0°から始まるので0で初期化)
    private float deg = 0;
    //弾を作り終えたかどうか
    //330°より大きくなったら既に円は完成しているので弾は作成しない
    bool hasMakeTama = false;
    
    // Update is called once per frame
    void Update () {
        //一秒ごとに弾をつくる
        currentTime += Time.deltaTime;
        if (targetTime<currentTime) {
            currentTime = 0;
            //角度が330°以下なら弾をつくる
            //まだ円周上に弾を作り終えていなければ弾を作成する
            if (!hasMakeTama)
            {
                //角度degからラジアンを作成
                var rad = deg * Mathf.Deg2Rad;
                //ラジアンを用いて、sinとcosを求める
                var sin = Mathf.Sin(rad);
                var cos = Mathf.Cos(rad);
                //円周上の点を求める
                //円の中心座標に半径をかけたcosとsinを足す
                var pos = this.gameObject.transform.position+new Vector3(cos*2.0f, sin*2.0f, 0);
                //弾の作成
                var t = Instantiate(tama) as GameObject;
                //弾を先ほど求めた円周上の座標に置く
                t.transform.position = pos;
                
                //角度を30°足す
                deg += 30;
                //330°よりも大きくなったら弾を作らないのでフラグをtrueにしておく
                if (deg > 330) hasMakeTama = true;
        }
    }
}

弾を放射線状に発射する

こちらに関しての考え方は
【Unity】自機に向かってくる弾をつくる(2D)を参考にしてください。
記事では敵からプレイヤーに向かうベクトルを求めていますが、
今回は弾を放射線状に飛ばしたいので
中心から弾の座標に向かうベクトルを求めればできます。
すなわち、
「原点から円の中心へのベクトルー原点から円周上の弾へのベクトル」を求めて
弾オブジェクトの「RigidBody2Dコンポネントのvelocity」に代入すれば、
放射線状に飛んでいく弾を実現できます。

コードは以下のように書いてみました。
ただし、今回は弾が出そろってから一気に飛ばしているので
飛ばす動作を
中心のオブジェクトにつけているスクリプトから行うのではなく、
弾オブジェクトにつけているスクリプトから行っています。
そのため
中心のオブジェクトのスクリプトでは
弾オブジェクトにつけているスクリプトのコンポネントを保存しておいて
飛ばす段階になったら
弾オブジェクトにつけているスクリプトのメソッドに
アクセスして弾を飛ばすということをしています。

中心にあるオブジェクトにつけているスクリプト
(上のスクリプトに追記)
SetTama.cs

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

public class SetTama : MonoBehaviour {

    public GameObject tama;
    private float targetTime = 1.0f;
    private float currentTime = 0;
/*追記*/
    //飛ばすタイミングをはかるための変数
    //num=20で飛ばしています
    private int num = 0;
/*追記終わり*/
    private float deg = 0;
    bool hasMakeTama = false;
/*追記*/
    //弾オブジェクトについているスクリプト(TamaTobasu.cs)コンポネントを保存するためのリスト
    private List<TamaTobasu> list=new List<TamaTobasu>();
/*追記終わり*/
    
    // Update is called once per frame
    void Update () {
        currentTime += Time.deltaTime;
        if (targetTime<currentTime) {
            currentTime = 0;
/*追記*/
            //弾を飛ばすまでのカウントを1進める
            num++;
/*追記終わり*/
            if (!hasMakeTama)
            {
                var rad = deg * Mathf.Deg2Rad;
                var sin = Mathf.Sin(rad);
                var cos = Mathf.Cos(rad);
                var pos = this.gameObject.transform.position+new Vector3(cos*2.0f, sin*2.0f, 0);
                var t = Instantiate(tama) as GameObject;
                t.transform.position = pos;
/*追記*/
                //弾オブジェクトtからTamaTobasuコンポネントを取得               
                var a=t.GetComponent<TamaTobasu>();
                //取得したTamaTobasuコンポネントをlistに加える
                list.Add(a);
                //TamaTobasuスクリプト内のCharaPosにアクセスして中心座標をセットする
                a.CharaPos = this.gameObject.transform.position;  
            //numカウントが20になったら弾を放射線状に飛ばす              
            }else if (num == 20)
            {
                //リストから一つずつ各弾オブジェクトのTamaTobasuコンポネントを取り出す
                foreach (var t in list)
                {
                    //TamaTobasuコンポネント内のTobu()メソッドを実行
                    t.Tobu();
                }
            }
/*追記終わり*/
            deg += 30;
            if (deg == 330) hasMakeTama = true;
        }
    }
}

弾オブジェクトにつけるスクリプト
TamaTobasu.cs

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

public class TamaTobasu : MonoBehaviour {

    //中心座標
    private Vector2 charaPos;
    //charaPosに中心座標をセット(SetTama.csで呼び出してセットしています)
    public Vector2 CharaPos { set { charaPos = value; } }
    //弾自身体の座標をセットする変数
    private Vector2 pos;

    // Use this for initialization
    void Start () {
        //弾の始点をセット(SetTama.csで始点に置いているので自分自身の位置を取得するだけ)
        pos = this.gameObject.transform.position;
    }

    /**
     * 弾を飛ばす
     */  
    public void Tobu()
    {
        this.gameObject.GetComponent<Rigidbody2D>().velocity = new Vector2(pos.x - charaPos.x, pos.y - charaPos.y);
    }
}

参考

開発メモ:円周上の点
数学系の処理を扱うMathfの全変数と全関数【Unity】
ラジアン
座標と三角比の関係