弾
現状の弾は「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.position、Transform.Translate、Rigidbody を使う方法で、それぞれに座標系の指定可否や利点があります。
🚀 主な移動方法と座標系の対応
transform.positiontransform.localPosition | ✅ World / Local | 絶対位置の指定。瞬間移動に向いている。 |
transform.Translate(Vector3, Space) | ✅ World / Local( | 相対移動。方向に応じた移動が可能。 |
Rigidbody.MovePosition() | ✅ Worldのみ | 物理演算と両立できる移動。滑らかな動き。 |
transform.rotationtransform.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);
}
}

コメント