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

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

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を初期状態に戻し、再度弾の発射が始まります。

動画

戦闘【UnityでRTSを作る 5】

前回の記事では陣営を実装しました。
今回は陣営が分かれたNPCに戦闘を行わせたかったので戦闘を実装します。

攻撃目標を見つける

Linqを利用して攻撃目標を見つける関数を用意しました。
最も近い敵を見つけられます。

関数を呼ぶたびにFindObjectsOfTypeは避けたかったので、
シーンのCubeKunはリストに入れます。

CubeKun.cs

public static List<CubeKun> list = new List<CubeKun>();

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

protected new void OnDisable () {
	base.OnDisable();
	list.Remove(this);
}

public CubeKun GetTarget () {
	return
		list.Where(c => c != this && Team != c.Team).
		OrderBy(c => Vector3.Distance(GetFeetPosition(),c.GetFeetPosition())).
		FirstOrDefault();
}


そしてCubeKunにTargetという抽象プロパティを追加します。

CubeKun.cs

public abstract CubeKun Target { get; set; }


Playerには自動プロパティで実装しました。
Updateで攻撃目標が存在しない場合に攻撃目標を探します。
Update関数をオーバーライドしていますが、これは後述のエイミングの実装によるものです。
ちなみに、プレイヤーの移動を実装した記事から移動方法が変化しています。

Player.cs

public override CubeKun Target { get; set; }

private RaycastHit hit;

protected override void Update () {
	base.Update();

	if (!Target) Target = GetTarget();

	canMove = Input.GetMouseButton(0);
	canSearch = canMove;
	if (canMove && Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),out hit)) {
		Pointer.position = hit.point;
		MoveToPointerPosition();
	}
}


NPCにはTargetのSetterにtarget(経路探索の移動目標)を共に更新する処理を実装しています。
プレイヤーはポインターの方へ移動しないといけませんが、
NPCは攻撃目標に向かえばいいからです。

NPC.cs

public CubeKun Target {
	get { return _Target; }
	set {
		_Target = value;
		target = value ? value.Tr : null;
	}
}
private CubeKun _Target = null;

protected override void Update () {
	base.Update();
	if (target) {
		MoveTo(target.position);
	} else {
		Target = GetTarget();
	}
}

エイミング

この記事では腕を回転させることで狙いを定めさせます。
そのため、最初に腕の参照をしなければいけません。

腕の参照

これが標準的なキューブ君の階層です。
f:id:MackySoft:20170418213951j:plain
腕の名前が「Arm_L」と「Arm_R」です。
左腕には「MachineGun」という名の武器を持たせています。

CubeKunに腕のTransformの参照を持たせる配列を用意します。

CubeKun.cs

public Transform[] Arms { get; private set; }


さらにAwake関数に腕を見つける処理を追加します。

Arms = new Transform[] {
	Tr.FindChild("Arm_L"),
	Tr.FindChild("Arm_R")
};

腕を回転させる

Targetの方向に腕を回転させるためにUpdate関数を用意します。

CubKun.cs

public float armSpeed = 10;

protected virtual new void Update () {
	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 * Time.deltaTime,
				0
			);
			Arms[i].rotation = Quaternion.LookRotation(newDir);
		}
	} else {
		for (int i = 0;Arms.Length > i;i++) {
			Arms[i].localRotation = Quaternion.Euler(Vector3.zero);
		}
	}
}

armSpeedが腕の回転速度を決めます。
攻撃目標が存在する場合は、狙いを定め、存在しない場合はデフォルトの値に戻します。

完成イメージ

余談ですが弾がやたらと見えづらかったのでTrailRendererを追加しました。


敵味方の区別【UnityでRTSを作る 4】

今回は戦闘には欠かせない敵味方の区別を行います。

レイヤー

プレイヤー、味方、敵、味方の弾、敵の弾のレイヤーを用意しました。
f:id:MackySoft:20170417004127j:plain

当たり判定

レイヤーの当たり判定をいじります。
f:id:MackySoft:20170417004456j:plain
プレイヤー及び味方の弾は敵だけに当たり、
敵の弾はプレイヤー及び味方だけに当たります。
また、弾がぶつかり合うことはありません

レイヤーを変更するための拡張メソッド

スクリプトからレイヤーの一括変更を行うのが結構めんどくさかったので、
そのための拡張メソッドを用意しました。

GameObjectExtension.cs

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

public static class GameObjectExtension {

	public static void SetLayer (this GameObject gameObject,int layer, bool needSetChildrens = true) {
		if(!gameObject) return;

		gameObject.layer = layer;
		
		if(!needSetChildrens) return;
		
		foreach(Transform childTransform in gameObject.transform)
		  SetLayer(childTransform.gameObject,layer,needSetChildrens);
	}

	public static void SetLayer (this GameObject gameObject,string layerName,bool needSetChildrens = true) {
		SetLayer(gameObject,LayerMask.NameToLayer(layerName),needSetChildrens);
	}
}

GameObjectの子要素のレイヤーも一括で変更できるようになります。

陣営を分ける

ここから実際に陣営を分けていきます。
そのために陣営を表す列挙型を用意しました。

public enum Team {
	Ally,
	Enemy
}

ユニットの陣営分け

PlayerとNPCの基底クラスのCubeKunクラスに陣営を表す抽象プロパティを用意します。

CubeKun.cs

public abstract Team Team { get; set; }

つまりCubeKunを継承しているPlayerとNPCには別々の処理を記述します。

Player.cs

public override Team Team {
	get { return Team.Ally; }
	set { throw new NotImplementedException(); }
}

Playerは必ず味方陣営に所属します。

NPC.cs

public override Team Team {
	get { return _Team; }
	set {
		gameObject.SetLayer(value.ToString());
		_Team = value;
	}
}
[SerializeField]
private Team _Team = Team.Ally;

private void OnValidate () {
	Team = _Team;
}

Setterに自動でレイヤーを変更する処理を用意しました。

武器の陣営分け

前回の武器のソースコードから変更点があります。
弾の発射は前回ではWeaponクラスのShoot関数で行っていましたが、
今回からはBulletクラスのShot関数を呼ぶことになります。
そこに弾のレイヤーを変更する処理を挿みます。

Bullet.cs

public CubeKun Parent { get; private set; }

public void Shot (Transform point,CubeKun parent,int power,float speed,float time) {
	var ins = Instantiate(this,point.position,point.rotation);
	Parent = parent;
	gameObject.SetLayer("Bullet_" + Parent.Team.ToString());
	ins.power = power;
	ins.speed = speed;
	Destroy(ins.gameObject,time);
}

これによりWeaponのShoot関数が簡略化されました。

Weapon.cs

public CubeKun Parent { get; private set; }

private void Start () {
	Parent = GetComponentInParent<CubeKun>();
	ShootStart();
}

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

次回は陣営を分けたNPC同士で戦わせます。