読者です 読者をやめる 読者になる 読者になる

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

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

勢力を可視化【UnityでRTSを作る 18】

勢力情報を可視化するUIを作りました。
f:id:MackySoft:20170520184504j:plain

ForceBar.cs

UIを制御するスクリプトです。

ForceBar.cs

using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class ForceBar : MonoBehaviour {

	public static ForceBar Instance {
		get { return _Instance ? _Instance : (_Instance = FindObjectOfType<ForceBar>()); }
	}
	private static ForceBar _Instance = null;
	
	public RectTransform allyBarTr;
	public RectTransform enemyBarTr;
	public Text allyCount;
	public Text enemyCount;

	private void Start () {
		BarUpdate();
	}

	public void BarUpdate () {
		float p = (float)CubeKun.list.Count(c => c.Team == Team.Ally) / (float)CubeKun.list.Count * 2;
		allyBarTr.localScale = new Vector3(p,1,1);
		enemyBarTr.localScale = new Vector3(2- p,1,1);
		allyCount.text = CubeKun.list.Count(c => c.Team == Team.Ally).ToString();
		enemyCount.text = CubeKun.list.Count(c => c.Team == Team.Enemy).ToString();
	}

}

ユニットのスポーン・死亡時にBarUpdate関数を呼んで現在の状態を適用します。

UIを設定

f:id:MackySoft:20170520203018j:plain
ピボットはAllyBarのxが0、EnemyBarのxが1になっています。

イメージ

GithubにCubeKunWarsのソースコードを公開【UnityでRTSを作る 17】

Githubの使い方を少し覚えたので、
CubeKunWarsのソースコードを公開してみました。
github.com

開発日記に書ききれてない部分とかもあるので、
わからないところがあったらそっちを見てみてください。

オブジェクトプールで最適化【UnityでRTSを作る 16】

昨日公開した記事でオブジェクトプールを紹介しました。
今回はそのオブジェクトプールをCubeKunWarsに適用します。
mackysoft.hatenablog.com

StatusGUIをプーリング

オブジェクトプールを作る前に実装していたDisable関数が役に立ちました。

Disable プーリングシステムを実装したときに使用したい。

HPバーの実装【UnityでRTSを作る 11】 - ヒキニートがゲームを作るブログ

過去の自分、ありがとう。

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

protected virtual void OnDisable () {
	status.Disable();
}

protected virtual void Start () {
	status = PoolManager.GetPoolSafe(StatusGUI.Prefab.gameObject).Get<StatusGUI>(StatusGUI.Canvas.transform);
	status.Initialize(this);
}

CubeKunをプーリング

DisableではHealthやTargetの初期化初期化を行います。
Weapon周りの初期化はもう少し調整する必要があります。

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

public static CubeKun GetInstance (CubeKun prefab,Vector3 position,Quaternion rotation) {
	return PoolManager.GetPoolSafe(prefab.gameObject).Get<CubeKun>(position,rotation);
}

public void Disable () {
	gameObject.SetActive(false);
	Health.Value = Health.Max;
	Target = null;
	for (int i = 0;Weapons.Length > i;i++) {
		Weapons[i].ShootStop();
		Weapons[i].Replenishment();
	}
}

protected override void Awake () {
	Health.onDie.AddListener(h => Disable());
}

Bulletをプーリング

最後に弾の再利用が出来るようにします。
これが一番プーリングによる影響が大きいと思います。

TrailRendererのClearをしないとTrailが以前の場所から再スポーンした場所に伸びてひどいことになります。
オブジェクトの初期化は正しく行いましょう。

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

private Coroutine disableCoroutine = null;

public TrailRenderer Trail { get; private set; }

public void Shot (Transform point,CubeKun parent,int power,float speed,float time) {
	var ins = PoolManager.GetPoolSafe(gameObject).Get<Bullet>(point.position,point.rotation);
	Parent = parent;
	ins.gameObject.SetLayer("Bullet_" + parent.Team.ToString());
	ins.power = power;
	ins.speed = speed;
	ins.Trail.Clear();
	ins.DisableStart(time);
}

private void Awake () {
	Trail = GetComponent<TrailRenderer>();
}

public void Disable () {
	DisableStop();
	gameObject.SetActive(false);
}

public void DisableStart (float time) {
	DisableStop();
	disableCoroutine = StartCoroutine(DisableCoroutine(time));
}

public void DisableStop () {
	if (disableCoroutine != null)
		StopCoroutine(disableCoroutine);
}

private IEnumerator DisableCoroutine (float time) {
	yield return Timeline.WaitForSeconds(time);
	Disable();
}

動画

プロファイラーを見てるとなんとなく処理が軽くなった気はします。

Github

今回使用したオブジェクトプールはこちらでダウンロードできます。
github.com

オブジェクトプール

オブジェクトの生成と破棄を回避する「オブジェクトプール」を作りました。

どんなオブジェクトプール?

個人的に欲しかった機能を盛り込みました。

使い方

まず以下からダウンロード・展開しUnityプロジェクトに入れます。
github.com
f:id:MackySoft:20170517233323j:plain

PoolManagerをシーンに追加

インスタンスを取得すると勝手に生成されるので必ず追加する必要はありませんが、
事前にパラメータを設定しておくこともできます。
f:id:MackySoft:20170517181308j:plain
(Prefabがnullだと開始時に例外を投げてプールが削除されます)

スクリプティング

PoolManagerはMackySoftネームスペース下にあるので、
usingエリアに追加します。

using MackySoft;
プールを取得

プールは以下のどちらかで取得できます。
"GetPoolSafe"は自動でプールを追加してくれます。

Pool pool = PoolManager.Insance[prefab];

Pool pool = PoolManager.GetPoolSafe(prefab);

それかプールをキャッシュして使うこともできます。

Pool pool = PoolManager.AddPool(prefab);
オブジェクトインスタンスを取得

プールからインスタンスを取得する方法は以下の4つが用意されています。

GameObject ins = pool.Get(parent);

GameObject ins = pool.Get(position,rotation,parent);

var ins = pool.Get<COMPONENT_NAME>(parent);

var ins = pool.Get<COMPONENT_NAME>(position,rotation,parent);
オブジェクトを無効化

PoolManagerは非アクティブなオブジェクトを、
未使用のオブジェクトとして認識します。

Destroy(pooledGameObject);//NO NO NO

pooledGameObject.SetActive(false);//OK with this!

インスペクター

再生中はプールされたオブジェクトの数が表示されます。
また、インスペクターからプレハブの変更及びプールの追加ができません。
f:id:MackySoft:20170517181344j:plain

関連記事

mackysoft.hatenablog.com
以下の記事で紹介されたオブジェクトプールをベースにして作りました。
tsubakit1.hateblo.jp

実機テストで見えた問題【UnityでRTSを作る 15】

実機テストをしました。


今回は実機テストをしたことで気づいたことをまとめます。

動作が重い

重たい。

今上がっている最適化の方法は

  • コードを最適化する。
  • DynamicCulling(開発中のアセット)を使って見えないオブジェクトの一部処理を停止する。
  • ユニットの数を制限する仕様を追加する。
    (ユニットにコストを付けて、全体のコストが一定以上にならないようにする)

操作性の違い

現在の「プレイヤーを引っ張るように移動させる」方法だと移動がしづらいです。
タブレットでテストしたのでスマホでの操作性はわからない)

画面端に仮想パッドを置くなどの対応を考えています。

PCと所々違うところがある

PCだと問題が起こらない箇所に問題が隠れていることがあります。

めんどくさがらずに定期的に実機テストをしないと、
知らないところで問題が山積みということになりそうです。

ユニットに指示を出すー実装【UnityでRTSを作る 14】

今回は前回の「仕様の決定」に続いて、その実装を行います。
mackysoft.hatenablog.com

HPBarを改良

こちらの記事で紹介したHPBarを改良して、
ユニットが選択されているかどうかを可視化します。
mackysoft.hatenablog.com

今回からはHPBarはHP以外の情報も扱うようになるのでStatusGUIに名称変更します。

CKWBehaviour.csに変更を加えます。

protected HPBar hpBar = null;

から

protected StatusGUI status = null;

に名前を変更します。

レイアウトに変更を加える

HPBarに矢印を付け加えました。
f:id:MackySoft:20170514213942j:plain
この矢印の色を変化させて「無所属・味方・敵・選択中の味方」を判別します。

StatusGUI.cs

StatusGUIを制御するStatusGUI.csを用意します。
HPBar.csの名前を変更したものに、いろいろ手を加えました。

StatusGUI.cs

using System;
using UnityEngine;
using UnityEngine.UI;

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

	#region Variables

	private static StatusGUI _Prefab = null;
	private static Canvas _Canvas = null;

	[Header("References")]
	public Image hpBar;
	public Image arrow;
	
	[Header("Color")]
	public bool colorChange = true;
	public Color safetyColor = Color.blue;
	public Color warningColor = Color.yellow;
	public Color dangerColor = Color.red;

	private RectTransform hpBarTr;
	private UIFollowTarget uft;
	
	#endregion

	#region Properties

	public static StatusGUI Prefab {
		get { return _Prefab ? _Prefab : (_Prefab = Resources.Load<StatusGUI>("Prefabs/UI/StatusGUI")); }
	}
	
	public static Canvas Canvas {
		get { return _Canvas ? _Canvas : (_Canvas = FindObjectOfType<Canvas>()); }
		set { _Canvas = value; }
	}
	
	public CKWBehaviour Behaviour { get; private set; }

	#endregion

	public static StatusGUI Instantiate (CKWBehaviour behaviour) {
		var ins = Instantiate(Prefab,Canvas.transform);
		ins.Initialize(behaviour);
		return ins;
	}
	
	private void Awake () {
		hpBarTr = hpBar.GetComponent<RectTransform>();
		uft = GetComponent<UIFollowTarget>();
	}

	public void Initialize (CKWBehaviour behaviour) {
		if (Behaviour) Disable();
		Behaviour = behaviour;

		uft.target = Behaviour.Tr;
		Behaviour.Health.onValueChanged.AddListener(OnHealthValueChanged);
		UpdateArrowColor();

		gameObject.SetActive(true);
	}

	public void Disable () {
		if (!Behaviour)
			throw new NullReferenceException();

		Behaviour.Health.onValueChanged.RemoveListener(OnHealthValueChanged);
		Behaviour = null;

		gameObject.SetActive(false);
	}

	public void UpdateArrowColor () {
		if (!Behaviour)
			throw new NullReferenceException();
		switch (Behaviour.Team) {
			case Team.Independent: arrow.color = Color.white; break;
			case Team.Ally: arrow.color = Color.blue; break;
			case Team.Enemy: arrow.color = Color.red; break;
		}
	}
	
	private void OnHealthValueChanged (Health health) {
		float p = (float)health.Value / (float)health.Max;
		hpBarTr.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;
		}
	}
}
名前 説明
arrow 矢印UIの参照。
UpdateArrowColor 矢印の色を更新します。

インスペクターではこのように設定します。
f:id:MackySoft:20170514214933j:plain

ユニットを選択

ここからユニットへの指示を実装します。

準備

まず最初にプレイヤーに笛の当たり判定を用意します。
ピクミンを参考にしているので「笛」です)

この笛の当たり判定に当たった味方が選択状態になります。

コライダーのisTriggerにはチェックを入れます。
f:id:MackySoft:20170514220733j:plain

本当はパーティクルを使って範囲を表現したいのですが、
今はめんどくさいのでSphereメッシュで済ませます。
f:id:MackySoft:20170514221041j:plain

選択処理

NPCに選択・選択解除を実装しました。

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

private Player player;

public void Select (Player player) {
	this.player = player;
	player.selectingList.Add(this);
	status.arrow.color = Color.green;
}

public void Deselect () {
	player.selectingList.Remove(this);
	status.UpdateArrowColor();
	player = null;
}
名前 説明
player 自身を選択しているPlayerの参照。
Select 指定したPlayerで自身を選択する。
StatusGUIの矢印を緑色にする。
Deselect 自身の選択を外す。

笛の制御

笛の大きさの制御や当たったときの処理を実装します。

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

public class Player : CubeKun {

	public float whistleRange = 1;

	public List<NPC> selectingList = new List<NPC>();

	private Transform whistle;

	public bool IsWhistling { get; private set; }

	protected override void Awake () {
		whistle = Tr.FindChild("Whistle");
		whistle.localScale = Vector3.zero;
	}

	protected override void Update () {
		if (Input.GetMouseButtonDown(0)) {
			if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),out hit)) {
				IsWhistling = hit.collider.GetComponentInParent<Player>() == this;
			}
		}

		Mover.canMove = Input.GetMouseButton(0);
		Mover.canSearch = Mover.canMove;
		GameManager.Clock.paused = !Mover.canMove;
		IsWhistling = IsWhistling && Mover.canMove;

		if (IsWhistling) {
			whistle.localScale = Vector3.one * whistleRange;
		} else {
			whistle.localScale = Vector3.zero;
		}

		if (Mover.canMove && Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),out hit)) {
			Pointer.position = hit.point;
			Mover.Move();
			RotateArm();
		}
	}

	protected virtual void OnTriggerEnter (Collider other) {
		if (IsWhistling) {
			var npc = other.GetComponentInParent<NPC>();
			if (npc && GetTeamRelative(npc) == Team.Ally && !selectingList.Contains(npc)) {
				selectingList.Add(npc);
				npc.Select(this);
			}
		}
		
	}
}
名前 説明
whistleRange 笛の当たり判定の大きさ。
selectingList 選択中のNPCのリスト。
whistle 笛の当たり判定の参照。
IsWhistling 笛を鳴らしているかどうか。

目標設定

良いアイデアがなかったので今回は
「選択したユニットはプレイヤーと同じ目標を狙い続ける」
という形で仮実装しました。

イメージ

地味ですが選択したユニットの矢印が緑色に変化します。

ユニットに指示を出すー仕様を決める【UnityでRTSを作る 13】

今回の記事は「ランダムマップの生成」になるはずでしたが、
ランダムマップの実装が現時点では記事にできるレベルじゃないので、
内容を変更して「ユニットに指示を出す」の実装をします。

結構長いので記事は複数に分けます。

今回は「ユニットに指示を出す」でぶつかった問題を整理して仕様を決めます。

ぶつかった問題

ユニットへの指示の実装でぶつかった問題をまとめました。

ユニット選択

  • カメラに映っているプレイヤーの周りのユニットしか選択できない。
  • プレイヤーの移動が画面を押し続けることなので、短形選択とかができない。
  • ほとんどの場合、プレイヤーにユニット選択をする時間猶予がない。

どの問題もプレイヤーのオブジェクトが存在することで起きてる制限ですね…
RTSとプレイヤーを同居させるのはかなり大変そうです。

名案!
TimeLockerのような「プレイヤーが動いている間だけ時間を進める」という機能を実装すれば、プレイヤーにユニットを選択する猶予を与えられるのでは?

目標設定

ユニットの選択とほぼ共通した問題があります。
そして何より問題なのが、良い解決策が浮かばない。

実装方法の候補

選択式

指示を出したいユニットをタップして選択する方法です。
ある程度自由が利きますが、複数の指示を出すのが大変です。

ピクミン

プレイヤーから一定の範囲内のユニットを全て選択する方法です。

選択式よりは操作性は上がりますが、
かゆいところに手が届かないという問題があります。

またプレイヤーが移動しながらこれを行う場合、
操作性を損なわない為には何かしらの工夫が必要となります。
(片手だけで移動と選択を同時に行いたい)

目標設定

これに関しては現時点では良いアイデアが浮かびませんでした。

今回はとりあえず「選択したユニットはプレイヤーと同じ目標を狙い続ける」
という形で仮実装しようと思います。

仕様の決定

考えた結果、ユニット選択にはピクミン式」を採用します。
シンプルな操作性で実装したいからです。

具体的な仕様

  • プレイヤーが動いている間だけ時間を進める
  • 選択をしたい場合プレイヤーを長押し
  • 選択をしながら移動する場合はプレイヤーをタップして引っ張るように移動
  • 選択したユニットはプレイヤーと同じ目標を狙い続ける
  • 選択は任意のタイミングで外せる

動いている間だけ時間を進める

今回は「プレイヤーが動いている間だけ時間を進める」だけを実装します。

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

protected override void Update () {
	Mover.canMove = Input.GetMouseButton(0);
	Mover.canSearch = Mover.canMove;
	GameManager.Clock.paused = !Mover.canMove;
	if (Mover.canMove && Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),out hit)) {
		Pointer.position = hit.point;
		Mover.Move();
		RotateArm();
	}
}

移動ボタンを押していなかったらClockを一時停止させます。

この変更で時間制御ボタンのポーズ処理が要らなくなったのでそれを削除します。

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

using Chronos;

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 = 1; 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;
	}
}

イメージ

ランダムマップ実装はしましたが見ての通りのお粗末な出来です。
そのうちしっかりと実装します。