Unity_2Dシューティング
Git管理
まず、リポジトリを作る
Cloneする
"C:\Users\watas\2d_shooting"
自分で2d_shootingというフォルダを新規作成して、そこにクローンする
https://scrapbox.io/files/691ae36aa8bda447c87f17d2.png
.gitignoreに追加
code:txt
/Assets/Fonts/NotoSansJP-Medium SDF_skoni.asset
Unity Hub で、作ったこのディレクトリをベースにプロジェクトを立ち上げる。
注意
プロジェクト名を空欄にする
保存場所: C:\Users\watas\2d_shooting
プロジェクト名: 2d_shooting
↑のようにはしない。入れ子構造になってしまうので。
なので、パスを記載し、プロジェクト名を無記入とするのが正しい。
? 無記入にはできない。
tmpみたいな場所にUnityプロジェクトを立ち上げ、Unityを落としてから(大事)、git管理下としたところに配置すればよい。
Unity Hubでプロジェクトパスを直すには
Remove project from listから外す
その後、Add +からもう一度登録しなおせばよい。
プロジェクト初期設定
文字化け対策: .editorconfig
コンパイルの高速化: staticの変数が更新されないことに注意する
覚えること
プレイヤー操作(左右+発射)
弾(Projectile)の生成/削除
敵の生成・移動AI
敵の弾
当たり判定の組み合わせ(弾→敵、敵→プレイヤー)
オブジェクトプール(弾の大量生成の最適化)
UI(スコア、残機)
Wave 管理(ステージ制)
GameManager の実践強化
カメラワーク(スクロール or 固定)
着手する
最初から色々やろうとすると、結構大変な気がするので最小構成でやる。まずはPlayerを動かすところから。
Playerを動かそう
RigidBody2D, Collider2Dを与える
New Input Systemを使いたい
Udemyのアクションゲームではどう使っていたっけ?
プロジェクトにデフォルトで存在する、InputSystem_Actionをクリック
このウィンドウを適当にUnityのウィンドウに収めて、操作する
デフォルトでいろいろ用意されてはいるけど、自分で用意しよう
フォルダを分けて、新規作成 Create > Input Actions で作ってダブルクリック
うまくいかない場合、Project Settings > Player > Active Input Hanging を弄る
コントロールスキーム作成
Action Propertiesを調整
https://scrapbox.io/files/691b18636199c0903b91fa60.png
Actionsの+から色々テンプレートがつくれるので、そちらに応じてボタン設定をしていく
C#スクリプトの用意
チェックをつけると、自動でいい感じに作ってくれるのでこれを使う。
https://scrapbox.io/files/691b196896474d92370f3348.png
このスクリプトにPlayer側から呼び出しをかけて操作を行う
code:Player.cs
private PlayerInputSet input;
// Awake()
input = new PlayerInputSet();
// Awakeの次に実行される inputはAwakeでクラス生成して、OnEnableで有効化するという流れ
private void OnEnable()
{
input.Enable();
// started: 触れた瞬間 performed: その間 canceled: 離したとき
input.Player.Movement.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
input.Player.Movement.canceled += ctx => moveInput = Vector2.zero;
}
private void OnDisable()
{
input.Disable();
}
この状態だと、右上とたおしたときにベクトルがまざって0.7位の値になる
それが嫌なら、UnityのInputで、ModeをDigitalにすれば0 or 1の値になる
ただし斜め移動が1.4くらいになるので、それがいやならデフォルトの設定のままでよい。
デジタルのほうが操作感がいいので、そうしよう
値が取れるようになる
code:Player.cs
// OnEnable
// started: 触れた瞬間 performed: その間 canceled: 離したとき
input.Player.Movement.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
input.Player.Movement.canceled += ctx => moveInput = Vector2.zero;
private void FixedUpdate()
{
// 上下左右移動
var next = rb.position + new Vector2((moveInput.x * speed * Time.fixedDeltaTime), (moveInput.y * speed * Time.fixedDeltaTime));
rb.MovePosition(next);
}
ちょっとリファクタリングして完成
code:c#
// InputSystemを使わない場合
if (Input.GetKey(KeyCode.W))
yInputDir = 1f;
if (Input.GetKey(KeyCode.D))
xInputDir = 1f;
var next = rb.position + new Vector2(
(xInputDir * speed * Time.fixedDeltaTime), (yInputDir * speed * Time.fixedDeltaTime)
);
こんな感じで設定してたけど、InputSystemの出力するctxをReadValue<Vector2>で読み取り、それをmoveInputで格納して使えるようになった。
弾丸を準備しよう
code:txt
# 考えた設計
・攻撃ボタンが押されると、弾のGameObjectをInstantiateで生成
・そのために、BulletというGameObjectを作り、RigidBody2DとCollider2Dを持たせる
・Prefabに登録して、Bullet.csに割り当て。Instantiateで複製されるものの基とする
・linearvelocityを前向きの設定にして、飛ばす
まずはBulletというObject, そしてスクリプトを作る
OnEnable()のとき、rb.linearVelocityを前向きにセットして、その後Destroy()させれば良い。
rb.velocityが使えなかったので、linearVelocityで、→(x軸がプラス)の方角に飛ばすようにした。
code:Bullet.cs
private void OnEnable()
{
rb.linearVelocity = new Vector2((1 * speed), 0);
Destroy(gameObject, lifeTime);
}
Player側で、弾打ち機能を作る
Bullet.csの、弾が前向きに飛ぶ処理
Playerの子要素として、CreateEmptyでFirePointを作り、適当にPlayerオブジェクトのさきっちょに配置
BulletもPrefab化して、各値を割り当ててInstantiate()で生成→これで飛ばせた。
Bullet側リファクタリング
code:Bullet.cs
//rb.linearVelocity = new Vector2((1 * speed), 0);
rb.linearVelocity = (Vector2)transform.right * speed;
こうすると、真下に飛んで行ってしまう
CloneされたBulletのrotateを見ると、90度になっている。しかしBulletのPrefabベースは特に問題がない。
弾の生成は、 Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);で作られている。なのでfirePointを見に行ったが、同様に特に問題がない。
しかし、Playerは三角形を270度回転させて、横向き機体っぽい見せ方にしている。firePointはPlayerの子要素のため、0度であってもその角度を引き継いでいる。
つまり、以下の挙動になっている
firePoint自体のローカル座標は0度
ただし、ワールド座標では270度が乗った状態
対策
Instantiate(bulletPrefab, firePoint.position, Quaternion.identity);
Quaternion.identityで、回転を引き継がないようにしてやればOK
敵を用意しよう
手順
1. 敵を作って移動させる
2. 弾との接触でやられる処理をつくる
3. その敵を定期沸きさせる、Spawnerを作る
まずは敵を作ろう
Enemyを作り、RigidbodyとColliderを持たせる。
重力やRotationを切っておくとよい
スクリプトを割り当て、左に向かっていくようにする
? Enemy.csは今後敵の派生を考慮し、継承前提のクラスにしたほうがよいだろうか
一旦後から考えて、まずは被弾処理とSpawner処理に重点を当てよう。実装して仕様が増え、重複が見えてきてから共通化するほうが実務の動きっぽく見えるだろうから。
弾との被弾処理を作る
弾、敵どちらもColliderをisTriggerに設定
Bullet.csでOnTriggerEnter2D()を使う
Spawnerを作ってみよう
EmptyObjectを作って、右端に置く
EnemyをPrefabにして、これに配置。定期的にInstantiate()すればよい
Y軸をある程度散らして、沸き位置を調整したい
code:c#
// y軸はランダムで沸かせる
float spawnX = gameObject.transform.position.x;
float spawnY = Random.Range(-5.0f, 5.0f);
Vector2 spawnPosition = new Vector2(spawnX, spawnY);
Instantiate(enemyPrefab, spawnPosition, Quaternion.identity);
これでできたけど、画面の比率に対して、上手く沸くようにしてあげたい
code:c#
// y軸はランダムで沸かせる
viewportSpawnX = 0.8f;
viewportSpawnY = Random.Range(0f, 1.0f);
viewportSpawnPosition = new Vector2(viewportSpawnX, viewportSpawnY);
worldSpawnPosition = cam.ViewportToWorldPoint(viewportSpawnPosition);
Instantiate(enemyPrefab, worldSpawnPosition, Quaternion.identity);
0から1の間で定義して、それをCameraのWorldPointに変換して作るようにした。
気になる点
実際のSTGは、同じパターンで沸くのを、覚えていって効率化していくのが楽しいという要素があるはず。なので、固定沸きにすべきか?
動きが正面に進むしかないのでパターンを持たせたいが、どう定義するのがいいだろうか?
→これらも、まずSTGの基本要素が実装できてから考えるのがよい
プレイヤーやられ
敵の種類とか、クリア
継承やステージパターン、移動スクリプトは敵が増えたら整理していくとよい。
敵の種類を増やす
体力が高く、上下にゆらゆら動く敵
体力を持たせた
上下に動く
Sin波を使う
code:cs
private void FixedUpdate()
{
time = time + Time.deltaTime;
float newY = baseY + Mathf.Sin(time * frequency) * amplitude;
float newX = transform.position.x - speed * Time.deltaTime;
rb.MovePosition(new Vector2(newX, newY));
}
TODO: 弾が当たったら白く光らせる
敵のHPを管理する共通コンポーネントを作る
現在はBulletが、敵のHPを変える、被弾エフェクトをする、敵をDestroyするとなっていて、責務がぐちゃぐちゃ
これを管理するコンポーネント EnemyHeallth を作るって管理する。
作って持たせた
Balletでは、collision.GetComponent<EnemyHealth>();で取得する
そこから、EnemyHealth.TakeDamage(damage)などを呼べば解決するようになった
いろんなEnemyに持たせてみる
Enemyで作っていた敵を、EnemyAに変更
EnemyHealthを持たせて、体力などを共通クラスにすることができた。
敵の共通クラスを作る
EnemyA, EnemyBなどあるが、親クラスEnemyBaseを用意する。
rb, speed, lifetime等はすべてこちらで管理ができるはず。
EnemyBase.cs
public abstract class EnemyBase
抽象クラスであることを明示的にしている。このクラス単体ではインスタンス化できない。
共通処理、共通フィールドの定義場所として使う。
protected virtual void Awake()など
protected
子クラスはアクセスできる。
virtual
付与することで、子クラスがoverrideできるようになる。
子クラス側のbase.Awake()等の処理
親クラスの共通処理を先に実行することができる。
記載しない場合は、親のAwake()は一切呼ばれないので、明示的に呼ぶこと。
abstractメソッド
code:c#
// 親側でこう書いたとする
protected abstract void Move();
親側で中身を書くことができない。
子クラスでは、必ずこのメソッドをoverrideしなくてはならない。多態性(ポリモーフィズム)。
弾の判定をタグ管理からレイヤー管理に変える
弾のcollision先のタグがEnemyなら、処理を進めている
これを、Physics2Dのレイヤーマトリクスで衝突制御を実装するようにする。
やること
レイヤーの作成
Physics 2Dのレイヤー衝突マトリクスの設定
コードの変更
レイヤーを作成し、割り当てる
Player, PlayerBullet, Enemyがあればよい。作って割り当てる。
Playerは子要素としてFirePointを持っているが、これにはつけない(子要素にはつけなかった。)
衝突マトリクスの設定
とりあえず、PlayerとPlayerBulletはぶつからないようにした。
https://scrapbox.io/files/691c3ccaeffaaf46308ff8b8.png
コードを書く
enemyLayerは、int型で受け取れる。
AwakeでenemyLayerを設定し、if (collision.gameObject.layer != enemyLayer)で比較すればOK。
自身の体力を管理できるようにする
PlayerHealthを作って割り当てる。
体力、最大体力、無敵フラグ、無敵秒など
無敵時間のコルーチンを作る
無敵フラグをON
playerとenemyの衝突を、一時的に検知しないようにする
⭐被弾をわかりやすく、spriteを調節する (かなり頻出だと思う)
code:c#
float elapsed = 0f; // 経過時間
float blinkInterval = .1f;
while (elapsed < invincibleTime)
{
sr.enabled = !sr.enabled;
yield return new WaitForSeconds(blinkInterval);
elapsed += blinkInterval;
}
無敵が3秒として、elapsedをwhileで増やしていく
その間、Sprite Rendererをdisable -> enableを切り替えて点滅させる。
ゲームオーバーを作る
まずは、ゲーム全体の状態を管理できるGameManagerを用意する
基本設計
自身をstaticでインスタンス化し、publicとする
フラグなどはすべてセッタで操作するようにする
DontDestroyOnLoadで保管するように設定する
敵の出現パターンを作る
いわゆる、Waveを実装することになる
タイマー制(3秒間はAを出す、6秒間はBをn引き出す, 10秒でWaveがおわり)
データベースをベースにする(ScriptableObject)
実務的なのは、ScriptableObjectを使った動きと思われるのでこちらで進めてみよう。
どういう構造になるだろうか?
WaveData
何秒後にどの敵を、どこに出すかの構造
UdemyのRPG講座でやった。
StageSpawner
WaveDataを受け取る
時間を見ながら、WaveDataに書かれた順番に敵を出す
Waveの遷移を管理する
WaveDataを作る
これを作ると、Projectメニューから構造を作れて、インスペクタで色々設定が作れるようになる
必要な情報は, time(敵の出る秒数), どの敵を出すか(Prefab), viewportY(度の高さに出すか)
WaveSpawnerを作る
WaveDataを読み込み、敵を沸かせるSpawnerを作る
? Scene上に敵は映っているのに、Game上に表示されない
z軸が悪さをしている。
code:cs
var viewportSpawnPosition = new Vector2(viewportSpawnX, viewportSpawnY);
var worldSpawnPosition = cam.ViewportToWorldPoint(viewportSpawnPosition);
Debug.Log(worldSpawnPosition.z); // -10
Vector2で作ったものを渡しているので、内部的にz = -10くらいのVector3に変換されている
カメラの位置と合わなくなってしまっているので、z = 0を指定する
code:c#
var viewportSpawnPosition = new Vector3(viewportSpawnX, viewportSpawnY, 0f);
var worldSpawnPosition = cam.ViewportToWorldPoint(viewportSpawnPosition);
worldSpawnPosition.z = 0f;
? なぜ、0fを指定しているのにもう一度設定しているか
cam.ViewportToWorldPointで変換した時点では、z = -10である
このメソッドの0fというのは、"カメラから0fの位置"という意味。
なのでカメラの位置が指定される。同じだと見えなくなるので、Game画面では見えなかった。
zを正しく0にしてやることで見えるようになる。
WaveとStage
Waveという概念について
タワーディフェンスとかでもある。
基本的に、Wave1がおわり = Stage1クリアという訳ではない。
Waveは、敵の出現パターンの1まとまり
Stageは、複数のWaveの集合である
Wave1 (開幕、弱めの敵)Wave2(ちょっと変な敵) ->Wace3(中ボス)->...->WaveN(ボス)みたいな
東方でも確かにこんな感じだ。
複数のWaveを1つのStageとして取り扱えるようにする
StageDataをつくり、WaveをListでを持たせる。
WaveSpawnerで、Wave1がおわったらWave2を流すようにする
それぞれ実装
WaveSpawnerを外部から呼べるようにする
WaveSpawnerを、StageController から呼ぶためのコンポーネントにする
Stageを制御するスクリプトを作る
今回は、StageControllerとしよう。挙動は、以下を実現する
Objectとして配置。スクリプトを用意。SerializeFieldでデータを持たせる。
Start()で、持たせたStageData.waves[currentWaveIndex]を実行
Update()で進捗を管理する
すでにステージ自体をクリアしていないか?
ウェーブを流している最中ではないか?
↑2つが問題なければ、indexをあげる
今のインデックスが、StageData.waves.Countより高くなれば、終わったということ
クリア処理
上げたインデックスで、再度ウェーブ実行処理を行う
Wave側
Spawnerを配置したxをベースに、設定したyに敵がわく
自身で処理を実行することはしない(StageControllerに任せる。)
Update()で進捗管理する
渡される予定のdata(StageData.waves[index])がnullでないか?
スポーンさせる処理が既に終了していないか?
↑が問題なければ、スポーン処理を実行していく
敵の弾を実装する
敵がDestroy()しない限り、時間経過で打つようにする
今の時間 % 5とかで、定期的に打たせる?それとももっといい方法があるだろうか。
定期的に打たせる処理は、クールタイムの実装が基本。
code:c#
// Update()などで
var fireInterval = .2f;
var fireTimer = 0f;
fireTimer -= Time.deltaTime;
if (fireTimer <= 0f) {
Fire();
fireTimer = fireInterval; // クールタイムの付与
}
弾を打つ敵と、打たない敵がいるはず
なのでこれもHealthと同じようにコンポーネントで管理する。
EnemyShooter
どんな弾(Prefab)か
発射位置
発射間隔
どう打つ
PlayerとEnemyBulletが触れても反応しない
Layer Collision Matrixを忘れていないか?
EnemyBulletにただしく、EnemyBulletレイヤーがついているか?
チェックしたらいけるはず。
BulletをPlayerBulletにリネーム
Layerの値を、静的クラスで管理するようにする
code:c#
// 毎回こんな感じで参照取得しているので、static classでまとめて呼び出す
private int playerLayer;
playerLayer = LayerMask.NameToLayer("Player");
? Enumとstatic class
static class ⭐
Unityでセットした値
タグ名など
Enum
難易度選択の値
状態異常の一覧などを管理するのが良い
※今回の場合、Enumで管理を試みるとUnityでレイヤを再設定したときに値がずれるので不適。
ダメージ処理のリファクタリング
EnemyBulletとEnemyBaseで、Playerとぶつかったときの処理はほぼ同じ。
どちらもぶつかったらダメージ
→ダメージコンポーネントDamagePlayerOnTriggerを作って、弾と敵に割り当てればよい。
damage, 自壊するかをSerializeFieldで、Unity側で設定
こうすることで、Aは2ダメージ, Bは1ダメージ, この弾は1ダメージとか切り分けられるようになった
さらに、インターフェースを使ったリファクタリングを行う
アタッチしたとき、記述した機能を付与しなければならなくなる
今回は「誰がダメージを与えられる存在なのか」をインターフェースで管理する
手順
インターフェースを定義。
インターフェースを、EnemyBulletに持たせる(= ダメージを与えられる存在になる)
PlayerHealthに、OnTriggerEnter2Dを持たせる
ここで、ぶつかってきた相手がIDamageSourceを持ってるかチェック
持っているなら、インターフェースからダメージを取り、受ける処理を実行。
持ってなければ、何もせずreturnするという感じ
EnemyBullet
IDamageSourceを持たせる
Enemy
親である、EnemyBaseに持たせる
個別にダメージを管理したい場合は、Unity上から弄ればよい
どうよくなった?
DamagePlayerOnTrigger
🔵貼り付けることで、ダメージを与える存在かどうか明確になる
🔴DamagePlayerOnTriggerが、PlayerHealthを操作していた。責務の視点からすると好ましくない
🔴Player以外にダメージを与えるとき、DamageNpcOnTriggerとかを新しく用意しなければならない。インターフェース側もNpcHealthが必要だが、こっちはさらにもう1ファイル、そのヘルスに対応したトリガーの作成が必要(しかも、おそらくLayerの指定以外はほぼ別のトリガーと同じ内容)
IDamageSource
🔵貼り付けることで、ダメージを与える存在かどうか明確になる
🔵どう体力が減るかは、PlayerHealth自身が操作できる
🔵IDamageSourceがダメージをきめて、PlayerHealthがライフを調整する形になった
どうダメージを受けるかが、PlayerHealthで管理できるようになった。
たとえばアーマーを持っている場合はダメージを半減させる、がヘルス側で完結する
🔵今後Player以外にダメージを与えるときも、IDamageSourceを付与して、ダメージを受ける側のNpcHealthなどで、IDamageSourceからダメージを受けとればよい
Playerが、敵にダメージを与えるケースにも適用してみよう
PlayerBulletにIDamageSourceを付与する
EnemyHealthに、PlayerBulletレイヤの場合、IDamageSourceからダメージを取るようにする
? 弾が役目を終えたら、PlayerBulletを消す必要がある(貫通弾になっちゃう)
これはこれでおもしろいが、調整が必要
PlayerBulletをDestroy()させる必要があるが、これはPlayerBullet自身を弄るため、PlayerBulletに責務がある
PlayerBulletに、OnTriggerEnter2d()で、Enemyレイヤとぶつかったとき、Destroyするように書けばよい。
たしかに、こう見るとかなり実装がしやすくなった
背景とかSEをつけてみる
息抜きで。と思ったが、権利関係が結構大変そう。
後からでもできるので、やっぱり後から。
アイテム実装
イメージ
親クラスを作り、いろんな種類を作る
敵からドロップ
パラメータを引き継いで、ちょっとずつ強くなっていく。次のステージもちこし
必要なもの
ItemBase
抽象クラスで作る。
PlayerStatus
Playerに持たせていたSpeed, Damageを分割する。
スピード
レイヤがPlayerとぶつかったら、PlayerSpeedをあげる
SpeedUpItemが、status.AddMoveSpeedを呼ぶことで責務が分かれる
PlayerのFixedUpdateでの移動処理は、PlayerStatusのMoveSpeedを使うようにする
このケースは、PlayerがPlayer.cs, PlayerStatus.csどちらも持っているので、GetComponent<PlayerStatus>();で解決する。
ダメージアップ
PlayerBulletがPlayerStatusを呼ばなければならないが、Bulletはそのコンポーネントをもっていない。
PlayerStatusを探しに行くこともできるが、処理に手間がかかる。
なので、弾が作られた瞬間にダメージを渡すようにして解決させる
その前に、まずPlayerShooterとして攻撃周りを分けよう
PlayerShooter
所持するPrefab
Firepoint
弾の生成処理
インターバルの時間などを持たせた(これはStatusにわたしてもいい。成長要素として)
Statusからダメージを参照させる
Status: ダメージを用意
Shooter: 弾を生成。PlayerBullet.Init(status.damage)を呼び出す
Bullet: Initで生成された弾のダメージを設定。
ファイアレートアップ
Statusに、Shooterの持つインターバルを渡して管理するようにする。
敵にダメージを与えたとき、赤くしたい
EnemyHealthは、Enemyオブジェクトと同じ階層にあるのでスプライトを参照できる。srをgetcomponentし、被弾時にカラーを変えるようなメソッドを呼ぶ。
書いてみる
code:c#
private float colorInterval = .05f;
private void Update()
{
if (sr.color != baseColor)
{
colorInterval -= Time.deltaTime;
if (colorInterval <= 0f)
{
sr.color = baseColor;
colorInterval = .05f;
}
}
}
カラーが変わったときだけ走らせる
returnを挟むべき
色が変わったときIntervalを減らして、0になったら元の色に戻す
これでも問題ないが、.05fがマジックナンバーになっているので片方がずれると不具合につながる
ブラッシュアップする
code:c#
private float flashTimer = 0f;
private void Update()
{
if (flashTimer <= 0f)
return;
flashTimer -= Time.deltaTime;
if (flashTimer <= 0f)
{
sr.color = baseColor;
}
}
public void TakeDamage(float damage)
{
...
flashTimer = flashDuration;
タイマーを0秒、間隔を.05fと別々に持たせる
トリガーが発生したとき、タイマーに間隔を設置する
そうなったときにUpdate()が感知する
タイマーからTime.deltaTimeを引いていき、0になったら基に戻す。
ライフを出したい
Playerの子要素として、出そう
まずは、階層自体の修正
Player自体をRotateさせているので、ライフなどもそれを考慮した形でずれている
スプライトなどの階層を調整する
Player
Graphics
Collision(STGあるあるの、実際の当たり判定)
FirePoint
LifeUIHolder
? Rigidbody, colliderの場所
親であるPlayerに集約させるのが良い。
RidigbodyのTransformが、基本的に本体とみなされる。
ライフを配置する手順は2通り
手動で3つ配置するのか、
コードでPrefab等から生成したライフを配置するか。お手軽なのは前者だが、今回は柔軟に対応ができる動的生成で実装する。
構成
LifeUIHolder - LifeUI.csを付与する
LifeIcon 丸いアイコンのPrefab or Sprite Renderer
LifeUI.cs
PlayerHealthを参照
[SerializeField] private PlayerHealth health;として、PlayerのGameObjectを割り当てればstatus.maxLife等で参照できる。本体を渡してしまって構わない。
Start()で、MaxLifeの分だけInstantiateして、x軸に間隔をあけて並べる
PlayerHealthの現在体力に応じて、アイコンのSetActiveを変える
InstantiateしたアイコンをListに入れておいて、setactiveで切り替える。
Health側のライフの読み取りプロパティについて
code:c#
private int currentLife;
public int CurrentLife => currentLife;
CurrentLifeは、currentLifeをそのまま返す。返す値は動的に変えるので、増減した値もそのまま返るようになる。
ボスを実装する
EnemyBaseを基にして、ボス固有の体力や特殊な動きは派生クラスで対応する。
クリア条件の変更
全てのWaveがIsFinishedとなったらクリアだった
これを、StageControllerがbossPrefabを1つ作成し、Bossが倒れたらクリア判定とすればよい
StageControllerに、フェーズ状態を追加する
Enumで各フェーズを記載し、switch文で切り替えていく。
Waveフェーズ Waveを流し続ける。
Waveをすべて流し終えて、画面に敵が残っていない(全部倒したか、lifeTimeが切れた)場合、ボスフェースに
倒したらクリアフェーズ。
ボスを作る
動き
ゆっくりフェードインしてきて、左右に動く
演出の間は無敵にしてあげたいが。
相手の登場演出中無敵と弾を打たないようにする
無敵
EnemyBaseにIsInvincibleを置く
ダメージを受ける(無敵かどうか)処理は、EnemyHealthの責務
TakeDamage()で、そのフラグ中は何も起きなくした
弾を打たない
EnemyBaseにCanShootを設置
EnemyShooterがUpdate()中にFire()を実行しているが、このフラグ中は何も起きなくした
ボスの発射する弾の種類を増やそう
今はEnemyShooterにEnemyBulletを配置して、それを打つだけ
どうすれば増やせるか
EnemyBulletBaseを作って、水平弾, 自機狙い, ランダムとかを継承して作っていく?
->EnemyBulletだけで問題なさそう。Shooterが、同じPrefabを、打ち出す方向やパターンで制御する
EnemyShooterをパターン制御する
Enumで、どんな弾かを定義しておく
発射する弾を定期的に変える
通常弾→自機狙い...
BossEnemyがShooter側で定義されたEnumを、時間に沿って切り替え、Shooterはそれに従い放つだけにする.
? BossPatternのように、コンポーネントにしなくていいのか?
設定データは、分割しなくてよい
Monobehaviorクラス自体も不要。Update, Awake, Startが不要な、値だけのデータは同じファイルに別クラスとして定義してしまって構わない。
実装する
code:BossEnemy.cs
public class BossPattern
{
public ShootPattern pattern; // 発射パターン
public float duration; // パターン継続時間
public float fireInterval; // パターン中の発射間隔
}
https://scrapbox.io/files/69218062ecc45c2b64d6d9e0.png
こんな感じで、どんな弾、どんな間隔、どんなインターバルで打つかを指定できるリストを作る
EnumとfireIntervalはシューター側にあるので、セッタを作っておく
次に、このリストを時間ごとに回す処理。
[0], [1], [2], ...尽きたらまた[0]に。
code:c#
if (patternTimer <= 0f)
{
currentPatternIndex++;
if (currentPatternIndex >= patterns.Count)
currentPatternIndex = 0; // patterns配列より増えたら0にしてやるだけでよい
ApplyCurrentPattern();
}
//...
private void ApplyCurrentPattern()
{
shooter.SetPattern(p.pattern);
shooter.SetFireInterval(p.fireInterval);
patternTimer = p.duration; // 指定秒そのパターンを継続
}
⭐Enumで定義するのか、publicクラスで定義するのか、staticクラスで定義するのか
Enum
状態や種類、パターンを名前付きの数値で表現する。
プレイヤー状態(Idle, Run, Attack)
シューティング弾タイプ
ダメージのタイプ(Physics, Magic)
publicクラス
データをまとめたり、1個のオブジェクトが複数設定を持つときに使う。
[System.Serializable]でUnity側でいろいろな設定を付与することができる
BossAttackPattern
パターンの名前、持続時間、弾の発射間隔
Wave構成
時間、持つPrefab, どこに出すかのposition
RPGのスキルデータ
mpCost, cooldown, power
staticクラス
変化しない定数、共通の値をまとめるときに使う。
レイヤー番号、タグ名、数値の定数など
ステージの流れを作ろう
stage1クリア -> stagre開始 -> クリアしたら3開始 -> クリア
ボス討伐するとGameManager.Instance.StageClear();が呼ばれる ので、そこでシーン遷移処理をするのがいいかも。しばらくしたらゆっくり暗くなって次のステージみたいな演出もはさんでみる。
責務について
Enemy: Dieするだけ
StageController: ステージが終わったかどうかの判定するだけ
GameManager: シーン遷移するだけ
フェードアウトする処理を作る
Canvasに、UI > ImageのObjectを作る(黒く塗りつぶす)
⭐このImageのコンポーネントとして、CanvasGroupを追加する
これでImage自体の透明度alphaを、CanvasGroupから調整できるようになる
GameManager側で、[SerializeField] private CanvasGroup fadeCanvas;として割り振れるようになるので、そうする
n秒かけて、暗くしたい等の処理 = コルーチンが良い。
Update()で加算させることもできるが、実装が複雑化する。
code:c#
private IEnumerator FadeOutCo()
{
float t = 0f;
float duration = 2f;
while (t < duration)
{
t += Time.deltaTime;
fadeCanvas.alpha = Mathf.Lerp(0f, 1f, t / duration);
yield return null;
}
}
Mathf.Lerp()
今回の場合、0fから1fまで、t / durationの割合で変化させる
0.5秒の場合、0.5 / 2 = 25%, 0から1の間の25%なので、0.25
1秒経ったら0.5, 2秒で1になる
yield return null;
yield return
これがそもそも、次のアクションまで渡した時間分待つという意味
new WaitForSeconds(2f);などを渡していると、次まで2f秒かかる
nullだと?
1フレーム処理を待つという意味合いになる
なので、1フレームごとにalphaの値が上書きされていく形になる
新しいステージを作ろう
Sceneとして作る。File > New Sceneで。
ロード処理を書く
Scene 'Stage2' couldn't be loaded because it has not been added to the active build profile or shared scene list or the AssetBundle has not been loaded.
エラーになった。作ったらBuild Profileに作ったシーンを入れていく
https://scrapbox.io/files/6922bb201b278ed09d17c664.png
SceneManager.LoadScene("Stage" + currentStageIndex);
これでよい
? GameManagerやSpawnerが置きなおしになるが、どうする?
Save as...で、Stage1をベースに2,3を用意して、調整していこう
GameManagerをDontDestroyOnLoadとする
PlayerStatusを引き継ぐようにする
GameManagerがデータ引継ぎの対応をする。classを作成
流れをつかむ
PlayerStatusのAwake()から始まる
Awake()した最初(stage1)はデフォルトの値を持つことになる
GameManagerを呼び出す。
持ち越すためのデータをGameManager.cs内部のクラスに格納
内部フラグをONにしておいて、次は違う分岐方法で格納する
アイテムを取ったとき
status内のAddMoveSpeed()などを呼ぶわけだが、ここにGameManagerの持つ内部クラスにも情報を同期させる処理を加える
このままクリア
→Stage2。同じく、PlayerStatusのAwake()から始まる
GameManagerのRnunDataを呼び出して、その中にデータが格納されている。それを取り出し、セットする
→そのまま同じ処理へ...
GameManagerのDontDestroyOnLoadでの課題
stage1から2に進んだ。GameManagerは残り続けるが、紐づけたフェードアウト用のオブジェクトやゲームオーバーUIは見つけられなくなってしまった。
今回のケースは、CanvasがGameManagerとは独立して存在しているから。なので、子要素として持たせてしまおう。
手持ちのデータはいつ初期化する?
IsStageClearとか、フェードアウトに使った黒背景のalpha値など
-> LoadSceneの前に挟むのが良い。もっと大規模になると、SceneManager.sceneLoadedを使った運用になる
code:cs
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
ResetManagerState();
// ついでにStageControllerやPlayerなどを探して参照をセットしたりもできる
}
OnSceneLoaded
Sceneロード時、自動で走るメソッド。上記の設定で割り当てられる。
背景を割り当てよう
素材を用意する。(1920 x 1080)で、2つ並べても違和感ない程度のものを。
アセット配下に格納して、hierarchyにドロップするとObjectとして作られる。
2つ横に並べて、親オブジェクトでまとめておく
敵が背景の後ろに行く
レイヤーの問題。Edit > Project Setting > Tags and Layersに行きたいが
? LayerとSorting Layerの違い
Layerは物理、当たり判定などに使う
Sorting Layerは、描画管理のためだけに使う
これはSprite Rendererから割り当てられる。
背景スクリプトについて
親オブジェクトに、子2枚を配置
親オブジェクトにスクロールさせるスクリプトを紐づける。
code:c#
// 子要素の横幅を取る
var sr = GetComponentInChildren<SpriteRenderer>();
width = sr.bounds.size.x;
この場合、親の直下の子の、最初のデータを参照することになる。Unity側の挙動に子要素のどれを受け取るかきめているので、少し柔軟性に欠ける。
下記のほうが堅牢になる。
code:c#
// Unity側で明示的に割り当て、呼んでやる。
if (baseRenderer == null)
Debug.Log("横幅を指定するためのbaseRendererがセットされていません。");
width = baseRenderer.bounds.size.x;
座標の設定
https://scrapbox.io/files/6923f7dde947b222b7dff31b.png
この親要素のtransform.positionは、矢印の座標の原点のこと。
背景を←にうごかす(x軸に、マイナスに動かす)
xがwidthだけマイナス(-19.2, 0)となったら、widthだけxをプラス(+19.2)してやると、また(0,0)の座標に戻る。
code:cs
private void Update()
{
// 2枚含む親ごと、左へ動かす
transform.position += Vector3.left * scrollSpeed * Time.deltaTime;
// 親が -width まで来たら、右に width だけ戻す
if (transform.position.x <= -width)
{
// (-19.2, 0) を (1, 0) * 19.2 = (19.2, 0) だけ加算して、 (0, 0) に戻す
transform.position += Vector3.right * width;
}
}
低速移動を実装
Shiftで低速移動させたい
InputActionsのShiftを設定
指定した変数がtrueの間(おしっぱの間)、速度を落とせばよい。
低速移動中に、srを透けさせて当たり判定を可視化させたい
コルーチンを使う
isSlowがtrueならコルーチンを走らせる
code:c#
if (isSlow)
{
if (beingTransparencyCo != null)
{
StopCoroutine(beingTransparencyCo);
}
beingTransparencyCo = StartCoroutine(BeingTransparencyCo());
}
Update中に走らせるコルーチンなので、必ずifで重複チェックをする。
おしっぱの間、コルーチンとして走らせている
これならコルーチンじゃないほうがいいだろうか?
リファクタリング
OFF -> ONになった瞬間に、コルーチンを開始
ON -> OFFになった瞬間にコルーチン停止、元の透明度に
コルーチン上だと0.5になるけど、結局そのあと1に上書きされて処理がreturnされるのでこれでOK
ボタンが切られたときの実行順(おそらく?)
コルーチンのwhileが終了
コルーチンのwhile後の処理 0.5記入が実行
そのあと、UpdateTransparench(false)が呼ばれて、1に上書きという感じ
コルーチン停止
プレイヤーの能力値を可視化する
細長い■で表現して、コックピットっぽくしたい。LifeUIの知識がそのまま使えるはず。
まず、Horizontal Layout Groupコンポーネントを使って土台を作る
code:イメージ
StatusUIHolder ←(Vertical Layout Group)
├─ DamageRow ←(Horizontal Layout Group)
│ ├─ Label(TextMeshPro)
│ └─ BarHolder ←(自前で並べる or Horizontal Layout)
│ ├─ Bar
│ ├─ Bar
│ └─ Bar
├─ FireRateRow
│ ├─ Label
│ └─ BarHolder
└─ DistanceRow
├─ Label
└─ BarHolder
垂直に、子要素は平行(Horizontal)に並べるという形
? 結構大変だったので慣れるために使いまくったほうがいいかも。
ステータスの■の色をアイテムの色と合わせる
Enumを作る
ItemBase
Enumを持たせる。
持たせたEnumはUnity上で設定しておく。
デフォルトとして白色に。AwakeでSerializeFieldで設定した色を割り振る。
取得時に、GameManager.StatusUIHolder.UpdateAllで更新させる。持たせたEnumと、自身の色を渡す。
StatusUIHolder
引数から受け取ったstatus, Enumのtype, colorを取り扱う。
Enumで分岐させるswitch文を走らせる。Row側の関数を実行し、■を増やす。
colorも渡しているので、■をInstantiateするときにその色にできる。
弾丸をオブジェクトプールとして扱う
弾丸をInstantiate -> Destroyして運用しているが、あまり推奨される動きではない
概要
在庫管理をする親クラスObjectPoolと、管理される側のクラスPooledObjectがあり、Start()時に親がPooledObjectを生成して、非アクティブと設定してStack(Listみたいなデータ構造)へと格納していき、欲しい分だけ用意する(在庫を作る)。
ObjectPool
記載の通り、フィールドを用意して、Start()で生成してStackに非アクティブで格納。
PooledObject
使う
格納されたStackからpopして、オブジェクトを取り出す
取り出したobjectをactiveにして、画面に出す。全ての在庫がなくなっていた場合、自身でInstantiateする流れにする
しまう
つかったオブジェクトを、pushしてStackに戻す。
オブジェクトを非アクティブとする。
弾丸に割り当ててみる
親クラスBulletPool, 在庫クラスPooledBulletの用意。空のゲームオブジェクトを作り、親クラスを割り当てておく。
Unityにおける、InvokeとCancelInvoke
Invoke
指定した秒数後、そのメソッドを1回だけ実行できる。
つまりプールにしまう処理をInvokeにて、lifetimeを指定することで、指定秒数後にしまえる
CancelInvoke
引数なしだと全てのInvokeを、引数ありだと指定したInvokeメソッドを取り消すことができる
今回設計した挙動の流れ
player
BulletPool
Awake()で弾丸を指定数生成。足りないときは生成する。
PlayerShooter
弾を呼ぶためのbulletPoolオブジェクトをSerializeFieldで紐づけてる。PlayerBulletPool。
撃つとき、Poolの取得関数を呼ぶ。Poolの取得関数で、キューから出し、transformをShooterの位置にセット。Activeにすることで、生成されたように見える。
この生成される弾はPooledBulletである必要がある。また同時に弾でもあるので、PlayerBullet側でセットしておく必要がある。
PlayerBullet
Object自体に、PooledBullet.csを割り当てておく。
呼ばれたときの初期化メソッドを準備(生成ではなく、Activeになったときに呼ぶので、Start等ではダメ)
ダメージ、飛距離, 方向など
Invokeで、lifetime後にプールに戻すような処理を実行すると実質的な有効時間となる
Enemy
基本は同じ
EnemyShooter
弾を呼ぶためのbulletPoolをセットしたい。
ただし、Enemyは基本的にPrefabであり、EnemyShooterはその子要素にある。
⭐Project上のPrefabアセットは、Sceneのインスタンスを参照することができない(Type mismatch表示となる)。
なのでSerializeFieldで割り当てることはできないため、コードで割り当てる処理を書くとよい。
code:c#
private void Start()
{
...
bulletPool = FindAnyObjectByType<EnemyBulletPool>();
これで弾を、bulletPool.Get()等で呼べるようになった。
EnemyBullet
この辺りはPlayerと同じ。
アイテムドロップ
ドロップアイテムは時間で切り替わるようにしたいが、まずはアイテムを落とさせよう。
10体倒せばアイテムを落とすようにしたい。
責務
EnemyHealth : Die()だけを検知する
EnemyDrop: アイテムだけを落とす処理を書く
GameManager: 敵カウント。% 10 = 0とかになったら、EnemyDropを呼ぶ
アクションイベントを使った
⭐どう便利なんだろうか
code:c#
# EnemyHealthのDie()メソッド(イベントを使わないとき)
// GameManagerの敵を倒した処理
GameManager.Instance.OnEnemyDied(this);
// UIManagerで敵を倒したときに走らせる処理
UIManager.Instance.UpdateKillCount();
// QuestManagerで敵を倒したときに走らせる処理
QuestManager.Instance.NotifyEnemyDeath(this);
こんな感じでEnemyHealthにガリガリ書いていくと思う(= 依存が強くなる)が、アクションイベントがあると
code:c#
# EnemyHealth Die()メソッドで、こう書いておく
OnAnyEnemyDied?.Invoke(this);
EnemyHealthのコードを一切触らず、呼び出し側だけで完結できる
code:c#
# GameManager で、敵討伐時にやりたいことがある
private void OnEnemyDied(EnemyHealth deadEnemy)
{ ....
# 登録
private void OnEnable()
{
EnemyHealth.OnAnyEnemyDied += OnEnemyDied;
}
とすると、EnemyHealth.Die()が走ったときに、GameManager側で登録したOnEnemyDiedも実行されるというわけ
おまけ
デリゲート: 関数そのものを変数として持つことができる
code:c#
public delegate void MyDelegate(int x); // 値を渡せる関数箱
MyDelegate d = SomeMethod;
d(10); // SomeMethod(10) が呼ばれる
アクション: 戻り値を持たないデリゲートが頻出するので、それを型としてまとめたもの
code:c#
Action // 引数なし
Action<int> // 引数1つ
Action<EnemyHealth> // 引数1つ(EnemyHealth)
Action<int, float> // 引数2つ
イベント: デリゲートを外部へ公開するための指定
code:c#
// 別のコード上から、+= -= などで格納や削除ができる
public static event Action<EnemyHealth> OnAnyEnemyDied;
アイテムが変わるようにする
CyclingItemをつくって、秒数ごとに色々な効果が付与されるようにしよう
Start()で、開始時の色と持つEnumを変える。今回、最初はDamageの色。
Update()で、1秒ごとに色と持つEnumを変える。
Apply()は取得時に走る。持つEnumに併せて効果を選択。
⭐考え方
仕様をまとめる。状態、遷移、振る舞いに分ける
敵が落とすアイテムが1秒ごとに切り替わる...
(状態) Damage, FireRate, Distanceの3つ
(遷移)1秒ごとに、切り替わってループする
(振る舞い)Playerに触れた時のApplyが異なる
継承か、コンポジションか
コンポジション
クラス内で別のクラスを持つこと(GameManagerがやってる。)
何が変化するかを書き起こしてみる
色と、効果
Updateか、Coroutineかどちらがいいか考える
一定の間隔で状態遷移するタイプなら、Updateでもよい
PlayerBulletとEnemyBulletの共通化
対応した
プレイヤーの無敵時間判定
Enemyレイヤーだけしか見てないけど、なぜかbulletに当たっても大丈夫なのかなぜか調べておく
isInvincibleは有効化されていて、TakeDamage()でダメージを受ける前に子の無敵フラグだけを見ているからだった。一応bullet側も追加しておいた。
効果音をつける
まず、gitignoreに追加
音を出す対象のGameObjectに、Audio Sourceコンポーネントを追加
SerializeFieldでそのコンポーネントと、鳴らすSEを紐づける。
? 音を鳴らすのは誰の責務?
発射ならshooterだが、将来的にはAudioManagerを作るとよい。
今後、色々なSEをつけるつもりなので作ってしまおう
タイトル画面
フォントを使う
ttfを配置して、Unity上でTextMeshProに置き換える
https://scrapbox.io/files/692823bea4d1ca454dd05209.png
演出をいくつか入れてみる
DOTweenを入れた
ライブラリみたいなもの。というかライブラリ。Rubyでいうgem。
これは、TextMeshProUGUI, Image, CanvasGroup等に対して使えるものなので、使う場合はcomponentとしてそのあたりを紐づけておく必要がある。
Managerを整理する
Title画面から始まるので、StageSceneにGameManager, AudioManagerは不要。
ステータスUIを、Titleの場合だけSetActive(false)としたい
とりあえずalphaを弄って対応。
ポーズ機能
画面をちょっと暗くして、操作説明、つづける、がんばる
ポーズフラグ
GameManagerの責務と判断。
画面を暗くしたいので、ONになったらそれを画面いっぱいに出せばよい
画面を止めたいが...
IPausableというインターフェースで、ポーズで止まるという性質を持たせるのが、とても奇麗
今回は簡易的に、Time.timeScaleで調整してみよう
0とすることで、Rigidbodyの挙動, Time.deltaTimeを使った移動, コルーチンが一斉に止まる
Update()等は回り続けるので、Playerの入力処理は手動で対応する。
操作説明
Z: 発射などとすると、依存が発生して説明文を書き換える必要があるが。
New Input Systemに表示用の文字列を取得するAPIがある。
? WASDというように、子階層になっているものはどう取得するか
ボタンの色を変える
ハイライトしたとき、枠と一緒にボタンの色も変える
Animatorを使う
LoadSceneをstaticクラスにまとめる
タイトルから読みだす処理、戻る処理をどうまとめるか
PauseMenuをオフにするとか、シーン遷移するとか。
GameManagerに各ボタンの処理をまとめた。
? Title->Stage->Titleと動かしたとき、GameManagerがDontDestroyとなるためボタンメソッドへの参照がずれる
これはシングルトン判定で、Titleに戻ったときにGameManagerを消しているから参照が切れている。つまり、Titleでしか使わないUIボタンには、Titleでボタンメソッドも配置したほうがよい。
方針を変える
ボタンメソッドを配置し、GameManagerのInstanceメソッドを呼べばよい
ポーズ画面のUIはどうする
Title以外のシーンでは使うので、GameManagerにぶら下げている
煩雑化したら分ける方針で。
ポーズ画面のanimationが動作しない
time.deltatimeを止めているから動かないっぽい
AnimatorのUpdate ModeをUnscaled Timeに変更する
Animatorコンポーネント -> Update Mode -> Normalから、Unscaled Timeでよい。
コンパイルが遅い...
DOTweenを入れてから明らかに遅い。なんとかしたい。
Assembly-CSharp.csprojは、85kbなのでそんなに重いようには見えない
単純に、Unityを再起動したら結構早くなった
2-3にちつけっぱにすると内部キャッシュ、監視データが溜まるのでこまめに再起動。
プレイヤーの移動閾値調整
そもそも、画面外にプレイヤーを収めるにはどんな手法があるか
Camera.WorldToViewPointでClampする
⭐Cameraのワールド端座標を使って、移動可能範囲を固定する
BoxColliderだけのもの(透明)を作って、出られなくする
⭐で対応する
クランプしよう
ある値を下限から上限の範囲に押し込める設計のこと(はみ出さない)
Playerの大きさを取る
transform.positionはPlayerオブジェクトの真ん中なので、見た目の半分ぶんを考慮する
現在、PlayerにRigidbody, Colliderを付与している。Graphicsは別枠。Graphicsのスプライトレンダラから取得すればよい
https://scrapbox.io/files/69298afb820580ad5951f93b.png
code:c#
private void Awake()
{
...
var extents = graphics.bounds.extents; // ワールド座標での半径
Debug.Log(extents);
// ex: (0.25, 0.25, 0.05) という値が返る
halfWidth = extents.x;
halfHeight = extents.y;
...
}
graphics.bounds.extentsについて
中心から端までの距離を返してくれる。他にはcenter, sizeなどがある。つまり、ワールド空間で見たときにこのスプライトの半分がどれくらいなのかを返してくれる。ちなみに透過部分については考慮されないので、透明部分があるスプライトの場合自動で縮むということはない。なので、なるべく丁寧に使いたいならちゃんとスプライトをトリミングしよう。
四隅のワールド座標を取る
code:c#
// カメラからプレイヤーまでのZ距離(2Dならほぼ一定)
float zDist = transform.position.z - cam.transform.position.z;
// ビューポート(0,0) = 左下, (1,1) = 右上
Vector3 leftBottom = cam.ViewportToWorldPoint(new Vector3(0f, 0f, zDist));
Vector3 rightTop = cam.ViewportToWorldPoint(new Vector3(1f, 1f, zDist));
// プレイヤーの半径ぶん内側にClamp
next.x = Mathf.Clamp(next.x, leftBottom.x + halfWidth, rightTop.x - halfWidth);
next.y = Mathf.Clamp(next.y, leftBottom.y + halfHeight, rightTop.y - halfHeight);
zDist
カメラを見るとわかるが、2Dゲームはデフォルトz = -10の値が設定されている
この場合、transform.position.zを0 - (-10)で10として、カメラから10離れている場合の画面端を計算しようとしている
※Playerを例えばz=5として置きたかったら、このように書いてはいけないということ。その場合は静的クラスなどでPlayerのz軸などを調整して決める。
クランプ
速度計算後の座標が、画面端 + graphics.bounds.extentsにおさまるようにする。
https://scrapbox.io/files/6929966de1ccbe2af1da7e3e.jpeg
Stage1...みたいな演出を入れよう
最後はクリア!タイトルへ、でいいと思う
GameManagerがcurrentStageIndexを持っているから、これを使う
ふわっとだして、消すくらいでいいと思う
コルーチンだと思うが、今回はDOTweenを入れたのでそちらで書いてみよう
ボタンの表示処理について
SetActiveか、alpha値を弄るかどちらが良いか
アニメを使いたいならalphaだが、シンプルな作品ならばSetActiveが好ましい。
きれいなのは、まずSetActive(true)として、その後CanvasGroupでalphaを弄る。
ゲームをやり直したとき、ステータスを初期化させる
ステータスの初期化の流れをおさらい
StageSceneのロード。PlayerStatusがgm.InitRunDataIfNeeded()
HasRunDataがあるかどうかでチェック
なので、ここでHasRunDataを初期化すればOK。フラグも。
StatusUIに配置したBarも初期化したい
責務のおさらい
Barを消すのは、StatusRowUIの責務
GameManagerは、Barを消す処理を呼びたい
ポーズ中にStageCallの表示をなんとかする
timeScaleが止まるので、単純に暗くしてやればよいだけ
? 優先度はどうなっているんだろう?
Canvasは、SortingLayerで決まるわけではない。単純にCanvasの上にあるものから順に描画され、下にある子要素ほど手前に表示されることになる。SortingLayerはSpriteRenderer用のため、Screen, Space, Overlay, Canvasには影響しない。
なので、今回は単純にポーズ画面を一番下にすればよい。
StageCall中にタイトルに戻ると、表示され続ける
まず、StageCallHolderをGameObjectとし、透明度とSetActiveで制御できるように設計する
タイトルに戻る系の処理で、SetActiveをfalseとすればよい。
GameManagerがごちゃついてきた
OnSceneLoadedを、もう少し上手に使えるはず。
ここは、シーン共通の初期化 + シーンごとの分岐処理の場所として使えると良い。
シーンごとにやりたいこと
一時フラグのリセット
TitleとStageで、UIの表示を切り替えたい
StageCallを出したい
現在、StartStage, LoadNextStageで、遷移した後にStageCallの表示なども担当していた
この処理は、シーンをロードするだけ
遷移後のStageCallは、OnSceneLoadedで出すようにすれば整理できる。
他にも使える
例えばタイトルボタンに戻す処理
UI非表示, status, killCount, StageIndex リセットと、やることが多い。他にもいろんな場所に配置されているので、ボタンの数だけメソッドを配置しないといけないので大変。以下のようにすると、かなり共通化できる。
ボタンは、シーン遷移だけ
リセット処理は、OnSceneLoadedで、シーンに応じた設定を行うだけ
リセット処理を作る
キルカウンタ、stage(はどうする?)
OnSceneLoadedで解決。
バグ
ボス倒してタイトルに戻ると、遷移する
戻るボタンにコルーチンを止める処理を挟んで解決。
SEの微調整
PlayerにAudio Sourceがいつの間にか付与されてて、いかにもこれで調整できそうだがこれは誤り。今回はAudioManagerを作ってそちらで管理しているはず。こっちは消しておこう。
ステージ調整
ダメージ
パワーアップごとに1.2ずつとかであげていこうか(2とかにすると結構大変)
と思ったが、intで設計してしまった
少数ではなく、ダメージを最低10とかの単位にすればいい感じに調整ができる
上限設定
15Lvくらいにしてみようか
damage: 40
fireInterval: 0.1fにしたい 0.275ずつへらして、さいごはちょっとだけ減る感じにしよう
distance: 2f
ステージのボリュームUP
検証が遅い...GameManagerを、Stage2とかシーン途中からPlayボタンを押したときでも動かせないだろうか
デバッグ用のステータスを挟む
GameManagerに即遷移を挟む
Playerの強さも出したい
HasRunDataを入れて値を入れるようにした。
stage3
アイテム取ると、合計19(-3で16)くらいのレベル
ステージボスについて
いくつか共通化する。同じクラス名は2回定義できないので、EnemyBossBaseをつくる
レベルがMAXになったステータスは光らせたい
StatusRow側で調整すればよい。maxLevelをStatusから取る
別クラスの値を公開する手順
code:PlayerStatus.cs
// ↓
public int MaxLevel => maxLevel;
Damage Lv1: 10 Lv15: 80
interval: Lv1 0.5f Lv15: 0.08 0.03 * 14 = 0.42
バグ修正
OnTringgerEnter2Dで重なっておけば、ダメージを受けない
Enter以外にも、異なるトリガーを入れておけばよい
OnTrigger
ライセンス
一旦完成
ビルドしてUnity Roomにあげてみよう
File > Build Profiles > Switch PlatformでWebにする
Edit > Project Settings > Player
Resolution and Presentation
Default Canvas Width / Height 画面比率
Publishing Settings
Compression Format: Gzip or Brotli(推奨)
Decompression Fallback: Enabled
Data Caching: オン
Memory Size: 256〜512MB(作品による)
Brotli 1080pでビルドして、20分程度
⭐UnityRoomはGzip
3分くらいかかった(一度ビルドしていると、時間がかからなくなるのかもしれない)
アップ設定
https://scrapbox.io/files/692c0bee2fd79bfa3b6cd541.png
入れた内容
readmeにまとめておくとよい
New Input System
GameManagerのシングルトンパターン
弾にObjectPoolを使った
味方ステータスのステージ引継ぎ
ScriptableObjectでの敵パターン用意
Wave制, Stage制の採用
外部ライブラリの使用