戦闘【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(); } }
エイミング
この記事では腕を回転させることで狙いを定めさせます。
そのため、最初に腕の参照をしなければいけません。
腕の参照
これが標準的なキューブ君の階層です。
腕の名前が「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を追加しました。
腕の回転でエイミング実装した pic.twitter.com/LdhTvAUYtY
— Macky (@macky_soft) 2017年4月19日
敵味方の区別【UnityでRTSを作る 4】
今回は戦闘には欠かせない敵味方の区別を行います。
レイヤー
プレイヤー、味方、敵、味方の弾、敵の弾のレイヤーを用意しました。
当たり判定
レイヤーの当たり判定をいじります。
プレイヤー及び味方の弾は敵だけに当たり、
敵の弾はプレイヤー及び味方だけに当たります。
また、弾がぶつかり合うことはありません
レイヤーを変更するための拡張メソッド
スクリプトからレイヤーの一括変更を行うのが結構めんどくさかったので、
そのための拡張メソッドを用意しました。
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に自動でレイヤーを変更する処理を用意しました。
陣営変更時に自動でレイヤーも変更してくれるやつ pic.twitter.com/Pt4jkXBbnE
— Macky (@macky_soft) 2017年4月17日
武器の陣営分け
前回の武器のソースコードから変更点があります。
弾の発射は前回では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同士で戦わせます。
NPCの移動と武器【UnityでRTSを作る 3】
今日はNPCの移動及び武器を実装しました。
NPCの移動処理
これは前回の記事で紹介したCubeKunクラスを継承させてMoveTo(移動のための汎用関数)にターゲットの位置を渡してあげるだけなので簡単でした。
NPC.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace MackySoft.CubeKunWars { public class NPC : CubeKun { private new void Update () { if (target) MoveTo(target.position); } } }
武器
弾の処理
武器から発射する弾のコードです。
弾のステータスと単純な移動処理だけです。
Bullet.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace MackySoft.CubeKunWars { [RequireComponent(typeof(BoxCollider))] public class Bullet : MonoBehaviour { public Transform Tr { get { return _Tr ? _Tr : (_Tr = transform); } } private Transform _Tr; public int power = 20; public float speed = 10; private void Update () { Tr.Translate(Tr.forward * speed * Time.deltaTime,Space.World); } } }
武器による弾の発射
武器には弾の発射と、その処理を一定間隔でループさせるためのコルーチンを実装します。
Weapon.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace MackySoft.CubeKunWars { public class Weapon : MonoBehaviour { public Transform[] firingPoints; public float interval = 0.5f; [Header("Bullet Settings")] public Bullet bullet; public int power = 20; public float speed = 5; public float time = 5; private Coroutine shootCoroutine = null; private void Start () { ShootStart(); } public void ShootStart () { if (shootCoroutine == null) shootCoroutine = StartCoroutine(ShootCoroutine()); } public void ShootStop () { if (shootCoroutine == null) return; shootCoroutine = null; StopCoroutine(shootCoroutine); } public void Shoot () { for (int i = 0;firingPoints.Length > i;i++) { var ins = Instantiate(bullet); ins.Tr.position = firingPoints[i].position; ins.Tr.rotation = firingPoints[i].rotation; ins.power = power; ins.speed = speed; Destroy(ins.gameObject,time); } } private IEnumerator ShootCoroutine () { while (true) { Shoot(); yield return new WaitForSeconds(interval); } } } }
体力
武器があっても減る体力が無いと意味がないので、体力を実装するHealthクラスを用意します。
Health.cs
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; namespace MackySoft.CubeKunWars { [Serializable] public class OnValueChangedEvent : UnityEvent<int> { } public class Health : MonoBehaviour { [SerializeField] private int _Value = 100; [Header("Events")] public OnValueChangedEvent onValueChanged; public UnityEvent onDie; public int Value { get { return _Value; } set { _Value = value > 0 ? value : 0; onValueChanged.Invoke(Value); if (IsDead) onDie.Invoke(); } } public bool IsDead { get { return Value == 0; } } private void OnValidate () { Value = _Value; } } }
それに従ってCubeKunクラスに「弾に当たった時にダメージを受ける」処理を追加しました。
CubeKun.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using Pathfinding; namespace MackySoft.CubeKunWars { [RequireComponent(typeof(Health))] [RequireComponent(typeof(Rigidbody))] public abstract class CubeKun : AIPath { public float sleepVelocity = 0.4F; public Transform Tr { get; private set; } public Rigidbody Rigid { get; private set; } public Health Health { get; private set; } protected override void Awake () { base.Awake(); Tr = transform; Rigid = GetComponent<Rigidbody>(); Rigid.constraints = RigidbodyConstraints.FreezeAll; Health = GetComponent<Health>(); Health.onDie.AddListener(() => Destroy(gameObject)); } protected virtual void OnCollisionEnter (Collision collision) { var bullet = collision.gameObject.GetComponent<Bullet>(); if (bullet) { Health.Value -= bullet.power; Destroy(bullet.gameObject); } } public void MoveTo (Vector3 position) { if (canMove) { var dir = CalculateVelocity(GetFeetPosition()); RotateTowards(targetDirection); dir.y = 0; if (dir.sqrMagnitude > sleepVelocity * sleepVelocity) { } else { dir = Vector3.zero; } Tr.Translate(dir * Time.deltaTime,Space.World); } } public override Vector3 GetFeetPosition () { return Tr.position; } } }
プレイヤーの移動【UnityでRTSを作る 2】
前回の記事ではざっくりと開発計画をしましたが、
ついに今回ゲーム制作が始まります。
今回はプレイヤーの移動です。
モデル
まずはプレイヤーのモデルの紹介です。
キューブ君(CubeKun)と呼びます。
ゲーム制作を始めた当初に作ったキャラクターです。
このキューブ君がプレイヤーとなります。
そしてこのプロジェクトの名前は「CubeKun Wars」となりました。
(変更の可能性もあります)
カメラ
まず最初にカメラを制御するためのコードを書きます。
トップダウン型のゲームなのでカメラが下に向いている前提のコードです。
CameraController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace MackySoft.CubeKunWars { public class CameraController : MonoBehaviour { public Transform target; public float height = 10; private Transform tr; void Start () { tr = transform; } void Update () { if (!target) return; tr.position = target.position + Vector3.up * height; } } }
移動処理
経路探索にはA* Pathfinding Projectを使用します。
CubeKun.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using Pathfinding; namespace MackySoft.CubeKunWars { public abstract class CubeKun : AIPath { public float sleepVelocity = 0.4F; public Transform Tr { get ; private set; } protected override void Awake () { base.Awake(); Tr = transform; } public void MoveTo (Vector3 position) { if (canMove) { var dir = CalculateVelocity(GetFeetPosition()); RotateTowards(targetDirection); dir.y = 0; if (dir.sqrMagnitude > sleepVelocity * sleepVelocity) { } else { dir = Vector3.zero; } Tr.Translate(dir * Time.deltaTime,Space.World); } } public override Vector3 GetFeetPosition () { return Tr.position; } } }
Player.cs
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace MackySoft.CubeKunWars { public class Player : CubeKun { public Transform Pointer { get; private set; } protected override void Awake () { base.Awake(); canMove = false; Pointer = new GameObject("pointer").transform; target = Pointer; Camera.main.orthographic = true; } private new void Update () { canMove = Input.GetMouseButton(0); canSearch = canMove; if (canMove) { Pointer.position = Camera.main.ScreenToWorldPoint(Input.mousePosition); MoveToPointerPosition(); } } public void MoveToPointerPosition () { MoveTo(Pointer.position); } } }
動画
クリックし続けるとキューブ君がカーソルのある方向へ向かいます。
Make RTS with Unity Part.1[Player Mover]
どんなゲームを作るか【UnityでRTSを作る 1】
これから作り始めるRTSっぽいゲームの話をします
開発日記を始めるわけ
「誰にも見せずやってても続かなかった」からです。
2年間ゲームを作ってて一番開発が続いたのが、
YouTubeに進歩を投稿していたプロジェクトです。(今はありませんがブログも同時進行でした)
そんなわけで今一度、開発日記を初めようと思います。
ゲームの内容
タイトル通り、RTSのような群衆同士をを戦わせるゲームを作ります。
ただし通常のRTSと違って、プレイヤーは神様ではなく、
NPCと同じようにユニットとして存在し、戦うことができます。
ルール
- お互いにマップに配置された拠点を取り合いながら戦います
- 拠点はユニットのスポーン、弾薬の補充など、戦略的に重要な意味を持ちます
- 先に相手を全滅させたら勝ちです
開発手順
メインシーンから作る
タイトル画面などのUIが主体となるシーンは後回しです。
めんどくさいうえに大した達成感がないからです。
小さく作る
最初から立派なものを作らず、モデルは少なく、UIもてきとーに作ります。
まずは最低限のゲームサイクルを作ります。
使用するアセット
現在使用予定のアセットです。
A* Pathfinding Project
ユニットの移動に使用します。
Chronos
ポーズ、倍速処理に使用します。
Dynamic CullingGroup
製作中の自作アセットです。
ランダムマップになる予定なので、動的なカリングをするために使用します。