ゲームエンジンⅡ⑤(2025/10/30)

現状の弾は「RigidbodyにAddForce」で飛ばしている。
もっと複雑な挙動をさせる場合は、軌道をコーディングしていく。
省力化のために「継承」を利用しつつ作っていく。

※初心者のうちは、スクリプトを書いていって共通の変数や関数ができたら、まとめて継承する、というやり方も効果的。

まずは「BaseBullet.cs」をつくる。

※VisualStudio等のエディタで“///”を書くと<summary>というフィールドが作られる。
ここにクラスや関数の仕様などを書き込んでおくと、意味が伝わりやすい。
(嫌うプログラマーもいる)

「弾の管理者」を定義しておくと、これを使って、「自分の発射した弾には当たらない」等の管理が可能になるので、当たり判定を取ったりする場合にスクリプトを分ける必要がない。

[property]
C#の機能。ゲッター・セッターをまとめられる。

    public int statePattern{ get; protected set; }

この場合
getはpublicなのでどこからでも読める。
setはprotectedなので書き込みは子クラスからしか書き込み不可。
なお、自クラス内部だけにしかアクセスを認めたくない場合はprivateに設定する。


C#の「プロパティ(Property)」は、変数(フィールド)へのアクセスをコントロールする仕組みです。簡単に言えば、「変数に値を入れたり取り出したりするための特別な方法」です。

🧠 まずはイメージから:プロパティは「窓口」

たとえば、ゲームのキャラクターに「HP(体力)」という情報があるとします。
でも、HPを直接いじられると困ることもありますよね?(マイナスになったり、上限を超えたり…)

そこで「HPの窓口」を作って、正しく値を設定・取得できるようにするのがプロパティです。

🧪 実際のコード例

class Player
{
    private int hp; // 本体の変数(外から直接触れない)

    public int HP // プロパティ(外から使える窓口)
    {
        get { return hp; } // 値を取り出す
        set
        {
            if (value < 0) hp = 0;       // マイナスは禁止
            else if (value > 100) hp = 100; // 上限は100
            else hp = value;
        }
    }
}

🔍 使い方

Player p = new Player();
p.HP = 120;         // → hpは100になる(上限チェック)
Console.WriteLine(p.HP); // → 100と表示される

✨ 自動プロパティ(簡略版)

もし特別なチェックが不要なら、もっと簡単に書けます:

public string Name { get; set; }

これは、裏で自動的に変数を作ってくれる便利な書き方です。

💡まとめ

用語意味
get値を取り出す
set値を設定する
private外から触れられない
public外から使える

プロパティは「安全でスマートな変数の使い方」を助けてくれる仕組みです。


とりあえずの“BaseBullet.cs”。

using UnityEngine;
/// <summary>
/// 弾丸の基本クラス
/// </summary>
public class BaseBullet : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    [Header(">>Base Settings")]
    public GameObject owner;    //弾丸の管理者
    public float shotPower;     //弾丸の発射威力
    public float attack;        //弾丸の攻撃力
    public float lifeTime;      //弾丸の寿命
    public Vector3 direction;   //進行方向のベクトル
    public Transform target;    //弾丸のターゲット

    protected Vector2 velocity; //弾丸の速度ベクトル
    protected float lifeTimer;  //弾丸の寿命タイマー

    //弾丸の状態遷移番号
    public int statePattern { get; protected set; }//ゲッター・セッターをまとめられる。

    /// <summary>
    /// 
    /// </summary>
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

BaseBullet.csを継承させてHommingBullet.csを作成する。

using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class HommingBllet : BaseBullet
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        Movement();
    }
    
    void Movement()
    {
        if (target == null)
        {
            Debug.LogWarning($"{transform.name}:target not found.");
            return;
        }
        // 弾丸の方向ベクトルを計算
        direction = target.position - transform.position;

        //ベクトルを正規化して速度ベクトルを計算
        velocity = direction.normalized * shotPower;

        //移動させる
        transform.Translate(velocity * Time.deltaTime, Space.World);

    }
}

[Normalize(正規化)]


C#の「Normalize」は、文字列をUnicodeの正規化形式に変換するためのメソッドです。特に、文字の見た目が同じでも内部表現が異なる場合に、比較や保存を安定させるために使います。

🔤 そもそも「正規化」って?

Unicodeでは、同じ見た目の文字でも複数の表現方法があります。
たとえば「é」は以下の2通りで表せます:

  • 合成済み文字:\u00E9(1文字)
  • 分解文字:e + ´\u0065\u0301(2文字)

この違いがあると、見た目は同じでも文字列比較で一致しないことがあります。

🧪 String.Normalize()の使い方

string s1 = "\u00E9";           // 合成済み
string s2 = "\u0065\u0301";     // 分解

bool isEqual = s1 == s2;        // falseになることも

// 正規化して比較
bool isNormalizedEqual = s1.Normalize() == s2.Normalize(); // true

🔧 正規化形式の種類(NormalizationForm)

形式説明
FromC合成済み形式(よく使われる)
FormD分解形式(すべての文字を分解)
FormKC合成済み+互換文字も統一
FromKD分解+互換文字も統一
string normalized = s.Normalize(NormalizationForm.FormC);

🧠 どんなときに使う?

  • 文字列比較:ユーザー名やファイル名の一致判定
  • 保存処理:データベースやファイルに保存する前に統一
  • 国際化対応:多言語入力で混入する特殊文字の整理

🧩 Unityでの「ベクトルのNormalize」とは別物!

ちなみに、Unityなどで使う Vector3.Normalize()ベクトルの長さを1にする処理で、文字列のNormalizeとは関係ありません。(←今回使ったのはこれ)

UnityのNormalizeについてはこちら⇒https://nekojara.city/unity-normalize-vector

(by Copilot)


Unityで「ものを動かす」方法は複数あり、World座標とLocal座標の使い分けが重要です。代表的な方法は Transform.positionTransform.TranslateRigidbody を使う方法で、それぞれに座標系の指定可否や利点があります。

🚀 主な移動方法と座標系の対応

transform.position
transform.localPosition
✅ World / Local絶対位置の指定。瞬間移動に向いている。
transform.Translate(Vector3, Space)✅ World / Local(Space.World / Space.Self相対移動。方向に応じた移動が可能。
Rigidbody.MovePosition()✅ Worldのみ物理演算と両立できる移動。滑らかな動き。
transform.rotation
transform.localRotation
✅ World / Local回転の制御。親子関係で挙動が変わる。
transform.TransformPoint()
InverseTransformPoint()
✅ 座標変換Local↔Worldの座標変換に便利。

Sources:

🧭 World座標 vs Local座標:どちらを使うべき?

🌍 World座標(グローバル座標)

  • シーンの原点 (0,0,0) を基準にした絶対位置。
  • 他のオブジェクトとの位置関係を把握しやすい。
  • 例:敵がプレイヤーの位置に向かって移動する。

🏠 Local座標(ローカル座標)

  • 親オブジェクトから見た相対位置。
  • 親の動きに追従する子オブジェクトの制御に便利。
  • 例:プレイヤーの手に持った武器の位置調整。

🧪 実例:Translateでの座標系指定

transform.Translate(Vector3.forward * Time.deltaTime, Space.Self); // 自分の向きに進む transform.Translate(Vector3.forward * Time.deltaTime, Space.World); // ワールドの前方向に進む 

🧠 ゆきさん向け補足:構造診断的な視点

  • Transform.position は「構造の絶対座標」、localPosition は「親構造からの相対座標」。
  • Translate は「構造の向きに沿った移動」、つまり「構造の文脈に従った動き」。
  • TransformPoint は「構造変換」、まさに座標系のリフレーミング。
using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class HommingBllet : BaseBullet
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        target = SerchTarget();
    }

    // Update is called once per frame
    void Update()
    {
        Movement();
    }

    void Movement()
    {
        if (target == null)
        {
            Debug.LogWarning($"{transform.name}:target not found.");
            return;
        }
        // 弾丸の方向ベクトルを計算
        direction = target.position - transform.position;

        //ベクトルを正規化して速度ベクトルを計算
        velocity = direction.normalized * shotPower;

        //移動させる
        transform.Translate(velocity * Time.deltaTime, Space.World);

    }
    
    ///<summary>
    /// ターゲットを探すメソッド
    ///</summary>
    ///<returns>ターゲットのTransform</returns>
    private Transform SerchTarget()
    {
        return GameObject.FindWithTag("Player").transform;
    } 
}

Spriteを貼るときは、空オブジェクトを作り、その「子」オブジェクトにSpriteを持たせる。
「親」オブジェクト側にスクリプトなどのコンポーネントを持たせるのがおすすめ。
「アニメーション処理」と「当たり判定」などを分けるため。
アニメーションをさせると座標が動かない、などの不具合が出るためそれを避けるため。
作成したSpriteに後から親をつけることも当然可能。方法の一つが、Spriteを右クリック⇒「CreateEmptyParent」。これで、指定したSpriteを空オブジェクトに含めることができる。

HommingBulltオブジェクト配下にSpriteを置き、HommingBullt側にCollider、Rigidbody、HommingBulltコンポーネントを追加。Prefab化したら準備完了。


弾丸の設定をできるようにする。

BaseBulletに初期化メソッドを追加。

using Unity.VisualScripting;
using UnityEngine;
/// <summary>
/// 弾丸の基本クラス
/// </summary>
public class BaseBullet : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    [Header(">>Base Settings")]
    public GameObject owner;    //弾丸の管理者
    public float shotPower;     //弾丸の発射威力
    public float attack;        //弾丸の攻撃力
    public float lifeTime;      //弾丸の寿命
    public Vector3 direction;   //進行方向のベクトル
    public Transform target;    //弾丸のターゲット

    protected Vector2 velocity; //弾丸の速度ベクトル
    protected float lifeTimer;  //弾丸の寿命タイマー

    //弾丸の状態遷移番号
    public int statePattern { get; protected set; }//ゲッター・セッターをまとめられる。

    /// <summary>
    /// 初期化メソッド
    /// </summary>
    /// <param name="owner">管理者</param>
    /// <param name="shotPower">発射スピード</param>
    /// <param name="attack">攻撃力</param>
    /// <param name="lifeTime">生存時間</param>
    /// <param name="direction">発射方向</param>
    /// <param name="target">ターゲット</param>
    public void Initialize(GameObject owner, float shotPower, float attack, float lifeTime, Vector3 direction, Transform target)
    {
        this.owner = owner;
        this.shotPower = shotPower;
        this.attack = attack;
        this.lifeTime = lifeTime;
        this.direction = direction;
        this.target = target;


    }
    
}

Shotメソッドに弾丸の初期化を組み込み。

using Unity.VisualScripting;
using UnityEngine;

public class BossEnemy : BaseEnemy
{
    public enum BossState
    {
        Appear, //出現
        Idle,   //待機
        Attack_01,  //攻撃1:ばらまき射撃
        Attack_02,  //攻撃2:集中攻撃
        Dead,       //死亡
    }
    public BossState currentState;
    public Vector3 defaultPosition = new Vector3(0, 2.0f, 0); //BossEnemyの出現座標
    public float attackInterval = 2.0f;
    public float attackTimer = 0.0f;
    public float shotInterval = 0.2f;
    public float shotTimer = 0.0f;

    public GameObject bulletPrefab;
    public float shotPower = 10f;

    void Start()    
    {
        currentState = BossState.Appear;
    }

    // Update is called once per frame
    void Update()
    {
        switch(currentState)
        {
            case BossState.Appear:
                Appear();
                break;

            case BossState.Idle:
                Idle();
                break;

            case BossState.Attack_01:
                Attack_01();
                break;

            case BossState.Attack_02:
                Attack_02();
                break;
        }
    }

    public void Appear()
    {
        transform.position = Vector3.Lerp(
            transform.position, //この変数を
            defaultPosition,    //ここまで動かす
            Time.deltaTime);

        var distance = Vector3.Distance(//座標AとBの距離を求めてくれる
            transform.position, // 座標A
            defaultPosition);   // 座標B

            if(distance <= 0.01f)
            {
            currentState = BossState.Idle;//目的地に到達したら待機状態へ移行
            }
    }

    public void Idle()
    {
        attackTimer += Time.deltaTime;
        if(attackTimer >= attackInterval)
        {
            attackTimer = 0.0f;

            int random = Random.Range(0, 100);//ランダムで1~100の範囲の値を返す
            if(random > 50)
                currentState = BossState.Attack_01;
            else
                currentState = BossState.Attack_02;
        }
    }

    public void Attack_01()
    {
        Shot(new Vector2(Random.Range(-1f, 1f), -1));

        shotTimer += Time.deltaTime;
        if(shotTimer >= shotInterval)
        {
            shotTimer = 0.0f;
            currentState = BossState.Idle;
        }
    }

    public void Attack_02()
    {
        shotTimer += Time.deltaTime;

        if (shotTimer >= shotInterval)
        {
            Shot(new Vector2(0,-1));

            shotTimer = 0.0f;
            currentState = BossState.Idle;
        }

    }
    
    public void Shot(Vector2 dir)
    {
        GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation);

        //弾丸の初期化
        BaseBullet baseBullet = bullet.GetComponent<BaseBullet>();
        if(baseBullet != null)
        {
            baseBullet.Initialize(
                owner: gameObject,
                shotPower: shotPower,
                attack: 10.0f,
                lifeTime: 3.0f,
                direction: dir,
                target: null);
        }

        Vector2 direction = dir.normalized;//射撃方向を決める

        //Rigidbody2Dを取得して力を加える
        Rigidbody2D rb2d = bullet.GetComponent<Rigidbody2D>();
        rb2d.AddForce(direction * shotPower * Random.Range(0.6f, 1f), ForceMode2D.Impulse);
    }
    public void Death()
    {
        
    }
    public override void OnDamage(int damage)
    {
        int adjusted = Mathf.CeilToInt(damage * 0.5f);//ダメージ半減
        base.OnDamage(adjusted);
    }
}

コメント

タイトルとURLをコピーしました