ヒキニートがゲームを作るブログ

Unityでゲームを作る過程を投稿します

開発状況の整理【UnityでRTSを作る 12】

CubeKunWarsの制作も少しづつながら今までに無いぐらい着実に進んでいます。
ですが今、僕のゲーム制作における、かなり微妙な時期が来ています。


「他のゲームが作りたい。」

やばいです、このまま放置してるとこのゲームは間違いなく
僕がかつて開発していたゲーム達と同じ運命を辿ります。

なのでこのゲームの開発状況をこの記事で整理します。

このゲームの特徴

まずはこのゲームの特徴を明確にし、これに逆らわないように作っていきます。

手軽に遊べるRTS

このゲームのジャンルは「システム面が複雑でゲームにあまり触れない層には敷居が高くなりがちなRTS」ですが想定プラットフォームがAndroidなので、
現在プレイヤーの操作は移動のみというシンプルなところに収めています。

シンプルな操作性はこのゲームで最も重要な点なので、
これを見失わないように開発をしていきたいです。

ゲーム時間が短いRTS

ゲームにあまり時間を取らない層でも遊びやすいように、
1ゲームの時間を短く収めます。

「ゲーム終了時のリワード広告の表示回数を増やす」という目的もあります。

今後の予定

今後実装するシステムの予定と意気込みです。

ランダムマップ

この記事の後はおそらく「ランダムマップの実装」の記事になると思います。

ランダムマップの実装ってたぶんめちゃくちゃめんどくさいです。
だけど、今はあくまでも

最初から立派なものを作らず、モデルは少なく、UIもてきとーに作ります。 まずは最低限のゲームサイクルを作ります。

どんなゲームを作るか【UnityでRTSを作る 1】 - ヒキニートがゲームを作るブログ

の段階です。

ランダムマップも
「マップ生成時にオブジェクトが重なっても別に構わない」
ぐらいの精神でやります。

ユニットへの指示

プレイヤーがNPCに指示を出せるようにします。

シンプルな操作性で実装するのは難しいと思います。
でもそこは重要なところなので意地で何とかします。

カスタマイズ機能

ユニットが持つ武器などをカスタマイズできるようにするかどうかです。

これが実装できればゲーム性が格段に上がりますが、
シンプルな実装をするのが難しそうです。

そして、最大のネックとして「実装するのがめんどくさい」があります。
もしかしたら実装しないかもしれません。

アニメーション

今のままだとチープすぎるので、
アニメーションやエフェクトを付けていい感じに仕上げたいです。

武器

今ある武器はマシンガンなんですが現代の武器だと
アニメーションとか入れづらいので地味になっちゃいます。

剣、弓、大砲など、武器にバリエーション豊かな中世辺りまで文明退化させたいです。
これは上の「アニメーション」に通ずることです。

チュートリアル

初めての人ってすごい理由でゲームをやめちゃうらしいので
チュートリアルはすごい大事です。


チュートリアルに限らず、丁寧に教えるというのは新規開拓の面でも重要です。

あとがき

文章に書き起こしてやることが見えてきたので、頑張れそうです。

追記
いい記事があったのでこちらも参考にしてゲームを完成までこぎつけたい。
シルバーセカンド開発日誌 ゲームを完成させる作り方

HPバーの実装【UnityでRTSを作る 11】

Health.csの改良

まずHealthクラスをHPバーで使用できるように改良します。

Health.cs

using System;
using UnityEngine;
using UnityEngine.Events;

[Serializable]
public class HealthEvent : UnityEvent<Health> { }

public class Health : MonoBehaviour {
	
	[SerializeField]
	private int _Value = 100;
	[SerializeField]
	private int _Max = 100;

	[Header("Events")]
	public HealthEvent onValueChanged;
	public HealthEvent onDie;

	public int Value {
		get { return _Value; }
		set {
			_Value = Mathf.Clamp(value,0,Max);
			onValueChanged.Invoke(this);
			if (IsDead) onDie.Invoke(this);
		}
	}

	public int Max {
		get { return _Max; }
		set {
			_Max = Mathf.Clamp(value,1,_Max);
			Value = Value;
		}
	}

	public bool IsDead {
		get { return Value == 0; }
	}

	private void OnValidate () {
		Value = _Value;
		Max = _Max;
	}

}

変更箇所はこんな感じです。

  • 使用するイベントをOnValueChangedEvent(intを引数にする)からHealthEventに変更(Healthを引数にする)
  • Maxプロパティを追加。(HPの割合を求める際に必要)

HPバー実装

ここからHPバーを実装していきます。

UIを組み立てる

f:id:MackySoft:20170506164720j:plain
f:id:MackySoft:20170506164711j:plain

ターゲットを追い続けるUI

オブジェクトの上にHPバーを表示し続けたいので、
UIにターゲットを追わせるためのコンポーネントを用意します。

UIFollwTarget.cs

using UnityEngine;

public class UIFollowTarget : MonoBehaviour {
	
	public Transform target;
	public Vector2 offset;
	private RectTransform rectTransform = null;

	void Awake () {
		rectTransform = GetComponent<RectTransform>();
	}

	void Update () {
		if (!target) return;
		rectTransform.position = RectTransformUtility.WorldToScreenPoint(Camera.main,target.position) + offset;
	}
}

こちらで紹介されているものに少し手を加えたものです。
tsubakit1.hateblo.jp

HPBar.cs

HPバーを操作するためのコンポーネントを用意します。

HPBar.cs

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(UIFollowTarget))]
public class HPBar : MonoBehaviour {

	public static HPBar Prefab {
		get { return _Prefab ? _Prefab : (_Prefab = Resources.Load<HPBar>("Prefabs/UI/HPBar")); }
	}
	private static HPBar _Prefab = null;
	
	public Image hpBar;
	
	[Header("Color")]
	public bool colorChange = true;
	public Color safetyColor = Color.blue;
	public Color warningColor = Color.yellow;
	public Color dangerColor = Color.red;

	private RectTransform tr;
	private Health health;
	private UIFollowTarget uft;

	public static HPBar Instantiate (Health health) {
		var ins = Instantiate(Prefab,FindObjectOfType<Canvas>().transform);
		ins.Initialize(health);
		return ins;
	}
	
	private void Awake () {
		tr = hpBar.GetComponent<RectTransform>();
		uft = GetComponent<UIFollowTarget>();
	}

	private void Start () {
		if (health) Initialize(health);
	}

	public void Initialize (Health health) {
		uft.target = health.transform;
		health.onValueChanged.AddListener(OnValueChanged);
	}

	public void Disable () {
		health.onValueChanged.RemoveListener(OnValueChanged);
	}

	private void OnValueChanged (Health health) {
		float p = (float)health.Value / (float)health.Max;
		tr.localScale = new Vector3(p,1,1);
		if (colorChange) {
			if (p > 0.5f)
				hpBar.color = safetyColor;
			else if (p > 0.25f)
				hpBar.color = warningColor;
			else
				hpBar.color = dangerColor;
		}
	}
}

主なメンバー

名前 説明
Prefab インスタンス化する際に使うプレハブの参照。
hpBar 増減するバー。
colorChange 色を変化させるかどうか。
Instantiate HPバーのプレハブをインスタンス化する。
Initialize 指定したHealthで初期化する。
Disable プーリングシステムを実装したときに使用したい。
OnValueChanged HealthのValueが変化したときのコールバック。

HPバーを生成

ユニットの拠点の基底クラスのCKWBehaviourに、
HPバーをインスタンス化する処理を実装しました。

CKWBehaviour.cs(重要部分を抜粋)

public abstract class CKWBehaviour : MonoBehaviour {

	protected static bool isQuitting = false;
	
	protected HPBar hpBar = null;

	protected virtual void Start () {
		hpBar = HPBar.Instantiate(Health);
	}

	protected virtual void OnDestroy () {
		if (isQuitting) return;
		Destroy(hpBar.gameObject);
	}
	
	private void OnApplicationQuit () {
		isQuitting = true;
	}

}

動画

AIの改良【UnityでRTSを作る 10】

前回の記事でも言ったように、AIの改良にかなり手間取ってます。

ですがそろそろ更新しないと変更箇所がわからなくなりそうなので、
現在の進捗をここに記します。

ユニットと拠点を共通規格にする

ユニットと拠点に共通点が多いので、共通のクラスを継承させてコードの短縮を図ります。

CKWBehaviour.cs

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Health))]
public abstract class CKWBehaviour : MonoBehaviour {

	public static List<CKWBehaviour> list = new List<CKWBehaviour>();
	
	[Range(0,100)]
	public int priority = 50;
	public float distanceStrength = 0.5f;

	public Transform Tr { get; private set; }
	public Rigidbody Rigid { get; private set; }
	public Health Health { get; private set; }

	public virtual Team Team { get; set; }
	
	protected virtual void Awake () {
		Tr = transform;

		Rigid = GetComponent<Rigidbody>();
		Rigid.constraints = RigidbodyConstraints.FreezeAll;

		Health = GetComponent<Health>();
	}

	protected virtual void OnEnable () {
		list.Add(this);
	}

	protected virtual void OnDisable () {
		list.Remove(this);
	}
	
	public float CalculatePriority (CKWBehaviour behaviour) {
		return priority + Vector3.Distance(Tr.position,behaviour.Tr.position) * distanceStrength;
	}
	
	public Team GetTeamRelative (CKWBehaviour behaviour) {
		if (
			Team != Team.Independent && behaviour.Team == Team.Independent ||
			Team == Team.Independent && behaviour.Team != Team.Independent
		)
			return Team.Independent;
		else if (Team == behaviour.Team)
			return Team.Ally;
		else
			return Team.Enemy;
	}

}

このクラスには共通のコンポーネントの初期化やTeamプロパティの実装の他に、
目標決めの為の変数や関数を実装しています。

名前 説明
priority 優先度を計算する際の基礎。
distanceStrength 優先度を計算に距離が及ぼす影響の強さ。
CalculatePriority 指定したCKWBehaviourの優先度を計算する。
GetTeamRelative 指定したCKWBehaviourとの相対的な関係を返す。
(例:同じ陣営ならAlly)

CKWBehaviourというネーミングですが、

CKWは、このゲームの名前の「CubeKunWars」の略で、
Behaviourは、登場するキャラなどの基本になるクラスに付けるようにしています。

移動処理の移行

CubeKunにCKWBehaviourを継承させ、今までのAIPathは継承できなくなったので、
移動処理はCubeKunMoverというクラス作り、そちらに移行させます。

CubeKunMover.cs

using UnityEngine;

public class CubeKunMover : AIPath {
	
	public float sleepVelocity = 0.4f;
	public CubeKun parent;

	public void Move () {
		if (!canMove || !target) return;
		
		var dir = CalculateVelocity(GetFeetPosition());
		RotateTowards(targetDirection);
		
		dir.y = 0;
		if (dir.sqrMagnitude > sleepVelocity * sleepVelocity) {

		} else {
			dir = Vector3.zero;
		}
		tr.Translate(dir * parent.Timeline.deltaTime,Space.World);
	}
	
	public override Vector3 GetFeetPosition () {
		return tr.position;
	}

}


CubeKunにCubeKunMoverを参照させます。

CubeKun.cs

public CubeKunMover Mover { get; private set; }

protected override void Awake () {
	//前略
	Mover = GetComponent<CubeKunMover>();
	Mover.parent = this;
}

今までCubeKun・Player・NPCで参照していたAIPathのフィールドなどの先頭には

Mover.

を付け足します。

目標を決める(Targeting)

目標を探す

CubeKun.cs

public CKWBehaviour GetAttackTarget () {
	return
		CKWBehaviour.list.Where(b => b && b != this && GetTeamRelative(b) == Team.Enemy).
		OrderBy(b => CalculatePriority(b)).
		FirstOrDefault();
}

public BasePoint GetSuppressionTarget () {
	return
		BasePoint.list.Where(b => b && GetTeamRelative(b) != Team.Ally).
		OrderBy(b => CalculatePriority(b)).
		FirstOrDefault();
}
名前 説明
GetAttackTarget 敵対関係にある優先度が一番高いCKWBehaviourを返します。
GetSuppressionTarget 自身の陣営と違う陣営の優先度が一番高い拠点を返します。

コルーチンで定期更新

今までは一度決めた目標のオブジェクトがDestroyされるまで目標が変わりませんでしたが、コルーチンで定期的に目標を更新するようにします。

CubeKun.cs

public float retargetingRate = 0.1f;

private Coroutine targetingCoroutine = null;

protected virtual void Start () {
	TargetingStart();
}

public void TargetingStart () {
	if (targetingCoroutine == null)
		targetingCoroutine = StartCoroutine(TargetingCoroutine());
}

public void TargetingStop () {
	if (targetingCoroutine != null)
		StopCoroutine(targetingCoroutine);
}

private IEnumerator TargetingCoroutine () {
	while (true) {
		Target = GetAttackTarget();
		if (!Target)
			Target = GetSuppressionTarget();
		if (Target) {
			switch (GetTeamRelative(Target)) {
				case Team.Independent:
					ShootStopAll();
					break;
				case Team.Ally:
					ShootStopAll();
					break;
				case Team.Enemy:
					ShootStartAll();
					break;
			}
		} else {
			ShootStopAll();
		}
		yield return Timeline.WaitForSeconds(retargetingRate);
	}
}

これにより、いままでPlayerやNPCのUpdate関数の中で行われていた目標更新関係の処理を削除しました。

イメージ

Chronosで時間の制御【UnityでRTSを作る 9】

f:id:MackySoft:20170502031647p:plain

前回の更新から少し間が空きましたが、生きてます。

今回はAIの改良の記事を書こうと思っていたのですが、
現在はAIの改良に悪戦苦闘しています。RTSのAIって複雑ですね。
ではAIの話じゃなければ何かというと、時間の制御のお話です。

モチベーションが下がりすぎないように切り替えていくスタイルです。

Chronosってなに?

Chronosって何かというと「ポーズ・倍速・スロー・巻戻し」などの演出を行うためのアセットです。


Chronos - Time Control for Unity

今回はこのChronosを使って「ポーズ・倍速」を実装します。

準備

Chronosを使うために最初にいくつかの準備をします。

Timekeeperをシーンに追加

まず最初にTimekeeperをシーンに追加します。
"GameObject/Timekeeper"メニュ-から追加できます。
f:id:MackySoft:20170502204755j:plain
Chronosを使うにはTimekeeperがシーンに存在している必要があります。*1

そのメニューでTimekeeperを追加すると以下のような構成になっています。
f:id:MackySoft:20170502212937j:plain

それにアタッチされているGlobalClockコンポーネントがとても重要な物です。

パラメータ 説明
Key タグみたいなもの。GlobalClockはTimekeeper.Clock(Keyの名前)で取得できる。
Parent 親となるGlobalClock。親が設定されていると親のTimeScaleの影響を受ける。
TimeScale 時間の速さ。1が通常。
Paused ポーズ

Timelineを時間を制御したいオブジェクトに追加

Chronosで時間を制御したいオブジェクトにTimelineコンポーネントを追加します。
f:id:MackySoft:20170502212823j:plain

これでChronosの準備ができました。

Chronosをゲームに適用

ここから実際にChronosによる時間の制御を行います。

まずGameManagerに便利プロパティを用意しました。

GameManager.cs

using Chronos;

public static class GameManager {
	
	public static GlobalClock Clock {
		get { return _Clock ? _Clock : (_Clock = Timekeeper.instance.Clock("Root")); }
	}
	private static GlobalClock _Clock = null;

	public static TimeState TimeState {
		get { return Timekeeper.GetTimeState(Clock.localTimeScale); }
	}

}

移動や回転

速さにChronosを適用するには

Time.deltaTime

ではなく

Timeline.deltaTIme

を使用します。

CubeKun.cs(重要部分を抜粋)

[RequireComponent(typeof(Timeline))]
public abstract class CubeKun : AIPath {

	public float sleepVelocity = 0.4f;
	public float armSpeed = 10;

	public Timeline Timeline { get; private set; }

	protected override void Awake () {
		base.Awake();
		Timeline = GetComponent<Timeline>();
	}
	
	protected virtual void Update () {
		RotateArm();
	}

	public void RotateArm () {
		if (Target) {
			for (int i = 0;Arms.Length > i;i++) {
				var dir = Target.Tr.position - Arms[i].position;
				dir.y = 0;
				var newDir = Vector3.RotateTowards(
					Arms[i].forward,
					dir,
					armSpeed * Timeline.deltaTime,
					0
				);
				Arms[i].rotation = Quaternion.LookRotation(newDir);
			}
		} else {
			for (int i = 0;Arms.Length > i;i++) {
				Arms[i].localRotation = Quaternion.Euler(Vector3.zero);
			}
		}
	}

	public void Move () {
		if (!canMove || !target) return;
			
		var dir = CalculateVelocity(GetFeetPosition());
		RotateTowards(targetDirection);
			
		dir.y = 0;
		if (dir.sqrMagnitude > sleepVelocity * sleepVelocity) {

		} else {
			dir = Vector3.zero;
		}
		tr.Translate(dir * Timeline.deltaTime,Space.World);
	}
		
	public override Vector3 GetFeetPosition () {
		return tr.position;
	}

}


Bulletの移動処理にはGameManager.ClockのdeltaTimeを使います。

Bullet.cs(重要部分を抜粋)

public class Bullet : MonoBehaviour {
	
	private void Update () {
		Tr.Translate(Tr.forward * speed * GameManager.Clock.deltaTime,Space.World);
	}
	
}

コルーチン

WeaponのリロードやShootコルーチンにChronosを適用していきます。

そこで、現在使用されている

new WaitForSeconds(seconds);

ではなく

Timeline.WaitForSeconds(seconds);

を使用します。

Weapon.cs(重要部分を抜粋)

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

public class Weapon : MonoBehaviour {
	
	public float interval = 0.5f;

	public float reload = 2;

	public CubeKun parent;

	private IEnumerator ShootCoroutine () {
		Magazine--;
		while (!IsEmpty) {
			Shoot();
			yield return parent.Timeline.WaitForSeconds(interval);
		}
	}

	private IEnumerator ReloadCoroutine () {
		if (IsEmpty || IsReloading)
			yield break;
		ShootStop();
		IsReloading = true;
		yield return parent.Timeline.WaitForSeconds(reload);
		Ammo = I_Ammo;
		IsReloading = false;
		ShootStart();
	}

}

時間制御用のボタン

停止・通常・倍速を切り替える為のボタンを作りました。

TimeControlButton.cs

using UnityEngine;
using UnityEngine.UI;

using Chronos;

[RequireComponent(typeof(Button))]
public class TimeControlButton : MonoBehaviour {
	
	public Sprite pausedIcon;
	public Sprite normalIcon;
	public Sprite acceleratedIcon;
	private Button button;
	private Image image;

	private void Awake () {
		button = GetComponent<Button>();
		image = transform.GetChild(0).GetComponent<Image>();
	}

	private void Start () {
		button.onClick.AddListener(OnClick);
		SetIcon();
	}

	private void Update () {
		if (Input.GetKeyDown(KeyCode.T)) {
			OnClick();
		}
	}

	private void OnClick () {
		switch (GameManager.TimeState) {
			case TimeState.Paused: GameManager.Clock.localTimeScale = 1; break;
			case TimeState.Normal: GameManager.Clock.localTimeScale = 2; break;
			case TimeState.Accelerated: GameManager.Clock.localTimeScale = 0; break;
		}
		SetIcon();
	}

	private void SetIcon () {
		switch (GameManager.TimeState) {
			case TimeState.Paused: image.sprite = pausedIcon; break;
			case TimeState.Normal: image.sprite = normalIcon; break;
			case TimeState.Accelerated: image.sprite = acceleratedIcon; break;
		}
	}

}

このコンポーネントをボタンに追加し、それぞれのアイコンを設定します。
f:id:MackySoft:20170502211307j:plain

動画

*1:Timekeeperはシングルトンなのでシーンに1つだけです。

兵舎の実装【UnityでRTSを作る 8】

今回は前回の記事に引き続き、拠点の実装をします。
今回の拠点は「兵舎」です。

兵舎の実装

CubeKunWarsでは兵舎はユニットをスポーンさせるオブジェクトです。

モデル

兵舎のモデルを用意しました。
キューブ君をかたどっています。
f:id:MackySoft:20170427203917j:plain

コールバックの追加

前回の記事のBasePoint.csにチーム変更時に呼ばれるコールバックを追加します。

BasePoint.cs

public Team Team {
	get { return _Team; }
	set {
		_Team = value;
		gameObject.SetLayer(value.ToString());
		switch (value) {
			case Team.Independent: SetColor(Color.white); break;
			case Team.Ally: SetColor(allyColor); break;
			case Team.Enemy: SetColor(enemyColor); break;
		}
		OnTeamChanged();//Add
	}
}
[SerializeField]
private Team _Team = Team.Ally;

protected virtual void OnTeamChanged () { }

これは兵舎からのキューブ君のスポーンの開始/停止をする際に使います。

スポーン

ここでBarrack.csを作成します。
prefabのCubeKunを一定間隔でスポーンさせ続けます。

Barrack.cs

public class Barrack : BasePoint {

	public CubeKun prefab;
	public float time = 5;

	private Coroutine spawnCoroutine = null;

	public void SpawnStart () {
		if (spawnCoroutine == null)
			spawnCoroutine = StartCoroutine(SpawnCoroutine());
	}

	public void SpawnStop () {
		if (spawnCoroutine != null)
			StopCoroutine(spawnCoroutine);
	}

	private IEnumerator SpawnCoroutine () {
		while (true) {
			Spawn(prefab);
			yield return new WaitForSeconds(time);
		}
	}

	public CubeKun Spawn (CubeKun prefab) {
		if (!prefab)
			throw new ArgumentNullException("prefab");
		var ins = Instantiate(prefab,transform.position,transform.rotation);
		ins.Team = Team;
		return ins;
	}

	protected override void OnTeamChanged () {
		switch (Team) {
			case Team.Independent:
				SpawnStop();
				break;
			case Team.Ally:
			case Team.Enemy:
				SpawnStart();
				break;
		}
	}
}

動画


拠点の実装【UnityでRTSを作る 7】

今回は弾薬補給拠点の実装をします。

モデル

弾薬補給型拠点のモデルを作りました。
f:id:MackySoft:20170422235041j:plain

陣営の追加

陣営というよりは「無所属」を追加しました。

public enum Team {
	Independent,//Add
	Ally,
	Enemy
}


レイヤーも追加しました。
f:id:MackySoft:20170424192402j:plain

拠点の実装

拠点の基底クラスを用意します。

BasePoint.cs

[RequireComponent(typeof(Health))]
[RequireComponent(typeof(BoxCollider))]
[RequireComponent(typeof(Rigidbody))]
public abstract class BasePoint : MonoBehaviour {

}

陣営

TeamプロパティのSetterではレイヤーと色の変更を行います。

BasePoint.cs

public Team Team {
	get { return _Team; }
	set {
		_Team = value;
		gameObject.SetLayer(value.ToString());
		switch (value) {
			case Team.Independent: SetColor(Color.white); break;
			case Team.Ally: SetColor(allyColor); break;
			case Team.Enemy: SetColor(enemyColor); break;
		}
	}
}
[SerializeField]
private Team _Team = Team.Ally;

public Color allyColor = Color.blue;
public Color enemyColor = Color.red;

protected virtual void OnValidate () {
	Team = _Team;
}


レイヤーの変更をするためのSetLayer拡張メソッドはこちらの記事で紹介しています。
mackysoft.hatenablog.com

色の変更

SetColor関数で実際に色の変更を行います。

BasePoint.cs

public Renderer[] Renderers {
	get { return _Renderers != null ? _Renderers : (_Renderers = GetComponentsInChildren<Renderer>()); }
}
private Renderer[] _Renderers;

public void SetColor (Color color) {
	for (int i = 0;Renderers.Length > i;i++) {
#if UNITY_EDITOR
		if (!Application.isPlaying)
			Renderers[i].sharedMaterial.color = color;
		else
#endif
			Renderers[i].material.color = color;
	}
}

色の変更はOnValidate関数により編集中も行われるので、
RendererはAwake関数ではなくGetterで初期化し、
SetColor関数は編集中と実行中の両方に対応するために少し分岐があります。

衝突時の処理

拠点のダメージや拠点の制圧処理などの処理を実装します。

BasePoint.cs

public BoxCollider Box { get; private set; }
public Health Health { get; private set; }
public Rigidbody Rigid { get; private set; }

protected virtual void Awake () {
	Box = GetComponent<BoxCollider>();
	Box.isTrigger = true;

	Rigid = GetComponent<Rigidbody>();
	Rigid.isKinematic = true;

	Health = GetComponent<Health>();
}

protected virtual void OnTriggerEnter (Collider other) {
	var bullet = other.GetComponent<Bullet>();
	if (bullet) {
		if (Team != Team.Independent)
			Health.Value -= bullet.power;
		return;
	}
	var cubekun = other.GetComponentInParent<CubeKun>();
	if (cubekun) {
		if (Team == Team.Independent || Team != Team.Independent && Health.IsDead)
			Team = cubekun.Team;
		OnCubeKunEnter(cubekun);
	}
}

protected virtual void OnCubeKunEnter (CubeKun cubekun) { }

主な処理はOnTriggerEnter関数に実装します。

弾に当たったとき、陣営が無所属でなかったらダメージを受けます。
レイヤーの設定で敵の弾が当たらないようになっているので、
敵味方の区別は必要ありません。

キューブ君に当たったとき、「陣営が無所属」または「無所属でなくて体力が0」の時にキューブ君と同じ陣営になります。(拠点の制圧と奪取)
その後、当たったキューブ君を引数にしてOnCubeKunEnterコールバックを呼びます。

弾薬補給型拠点の実装

まず、Weaponクラスに弾薬を回復する関数を実装しました。
現在は弾薬を初期化するだけです。

Weapon.cs

public void Replenishment () {
	Magazine = I_Magazine;
	Ammo = I_Ammo;
}


CubeKunクラスに持っている武器にアクセスするためのプロパティを用意します。

CubeKun.cs

public Weapon[] Weapons { get; private set; }

protected override void Awake () {
	Weapons = GetComponentsInChildren<Weapon>();
}


材料が揃ったので弾薬補給型拠点を実装します。

AmmunitionStore.cs

public class AmmunitionStore : BasePoint {

	protected override void OnCubeKunEnter (CubeKun cubekun) {
		if (Team != cubekun.Team) return;
		for (int i = 0;cubekun.Weapons.Length > i;i++)
			cubekun.Weapons[i].Replenishment();
	}

}

OnCUbeKunEnterコールバックが呼ばれると、
衝突したのが同じ陣営のキューブ君ならそのキューブ君の武器の弾薬を補給します。

完成イメージ

武器弾薬【UnityでRTSを作る 6】

そろそろ拠点の実装をする頃合いだと思うので、
最初に弾薬補給型の拠点を作るべく、武器弾薬を実装します。

弾薬

ステータスの実装

最初に弾薬に関する変数とプロパティを用意します。

Weapon.cs

[Header("Ammunition")]
[SerializeField]
private int _Ammo = 10;

[SerializeField]
private int _Magazine = 3;

public int I_Magazine { get; private set; }
public int I_Ammo { get; private set; }

public bool IsEmpty {
	get { return Magazine == 0 && Ammo == 0; }
}

public int Ammo {
	get { return _Ammo; }
	set {
		_Ammo = Mathf.Clamp(value,0,value);
		if (Ammo == 0 && Magazine > 0 && Application.isPlaying)
			StartCoroutine(ReloadCoroutine());
	}
}

public int Magazine {
	get { return _Magazine; }
	set { _Magazine = Mathf.Clamp(value,0,value); }
}

private void Awake () {
	I_Magazine = Magazine;
	I_Ammo = Ammo;
}

private void OnValidate () {
	Magazine = _Magazine;
	Ammo = _Ammo;
}

Ammo(弾薬)とMagazine(弾倉)は共に0が下限になります。
Ammoには弾薬が0になって弾倉がまだあるときに、
後述のリロード処理を行い始めます。

また、「I_〇〇」というプロパティは「初期状態の~」という意味があります。
今回の場合は初期状態のAmmoとMagazineを保存します。

弾の発射でステータスを変化させる

弾の発射を行うための関数にステータスを変化させる処理を追加します。

Weapon.cs

public void Shoot () {
	for (int i = 0;firingPoints.Length > i;i++) {
		bullet.Shot(firingPoints[i],Parent,power,speed,time);
	}
	Ammo--;
}

private IEnumerator ShootCoroutine () {
	Magazine--;
	while (!IsEmpty) {
		Shoot();
		yield return new WaitForSeconds(interval);
	}
}

Shoot関数にはAmmoを減らす処理、
ShootCoroutineには発射開始時にMagazineを減らす処理を実装しました。
またwhileの条件を「弾薬が完全に空でないこと」にしました。

リロード

次はリロード処理を実装します。

Weapon.cs

public float reload = 2;

public bool IsReloading { get; private set; }

private IEnumerator ReloadCoroutine () {
	if (IsEmpty || IsReloading)
		yield break;
	ShootStop();
	IsReloading = true;
	yield return new WaitForSeconds(reload);
	Ammo = I_Ammo;
	IsReloading = false;
	ShootStart();
}

reload変数がリロード時間です。
リロードが始まると現在の弾の発射が止まり、reload秒待ちます。
待った後はAmmoを初期状態に戻し、再度弾の発射が始まります。

動画