Unity_2Dアクション
https://private-user-images.githubusercontent.com/73050501/531178193-fdf2d275-ec8d-4716-b8c2-8c3cc04dad31.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjkyNjMzMzEsIm5iZiI6MTc2OTI2MzAzMSwicGF0aCI6Ii83MzA1MDUwMS81MzExNzgxOTMtZmRmMmQyNzUtZWM4ZC00NzE2LWI4YzItOGMzY2MwNGRhZDMxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjAxMjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwMTI0VDEzNTcxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTg1NGNiZDUzMWIyNzViYmNhZDdiYTE0NjAxNjM1ZDNkM2Y4ZDNhMmVlNDY4YWZiMzRlZGNiYTNmYjU1MDM4MTEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.5vEHqBbSR8co83OmaKu4wQe-8FbvOoXULty0AjRL_OM
クレジット
https://scrapbox.io/files/6947950079ee5de195b19151.png
つかわないかも
ほんと
BGM
タイトル
https://www.youtube.com/watch?v=BdP24588mlo&list=PLGsLilmIwnDahdp0gCTuFnwV03lhTbWx1
normal
hard
https://www.youtube.com/watch?v=pYwP27dZTN4&list=PLZ1emuJ65jC1L7sjHdFF0eivBzcfmaaGD&index=8
Git管理
プロジェクト初期設定
まず、どんなゲームにするか
2通り
ゲームとしてではなく技術チェックの場として、サンドボックス的な感じで作る
ちゃんと完結型の、小さいが1本の作品と作る〇
どうできるだろう
しっかりしたクオリティの、大きめのステージを1つ作成してその中で戦う。
敵がWaveで沸いていくので、プレイヤーを操作して倒していく。
一定量倒すとレベルが上がる。スキル振りができる。
最後にボス。
スケジュール
1: Playerの操作、仮ステージ用意、カメラ追従
2: StateMachine, 近接攻撃、ダメージ処理、簡易AI、討伐処理
3: 敵の種類作成、被弾エフェクト、仮アニメなど
4: Waveとしての用意、敵スポーン、スキルUIの仮用意
5: 実装した操作を、スキルで解放できるようにする
6: ボス用意
7,8: バッファ。サンドボックスでもブラッシュアップでもなんでも。
ビルド
テーマはファンタジー
剣と魔法で近距離 or 遠距離でツリー分岐できるように
基本攻撃は近接ですが、魔法ビルドになるとMPを吸収できるように。
スキルツリーは近距離ならダッシュ、遠距離ならテレポート
実装を目指すこと(READMEに記述する内容)
NewInputSystem
StateMachine
GameManager
Animator
Gizmo
スプライト割当
スキルツリー
セーブシステム
タイルセットを使う
カメラ追従
敵VFX
Scriptable Object
初期設定
GIt管理
文字化け対策
コンパイル高速化
Edit > Project Setting > Editor > Enter Play Mode Setting
まずは
フォルダをざっくり分けた。
シーンにStageとPlayerを配置して物理チェック。
今のうちにRigidbodyやcolliderと、スプライトを分けておく
⭐Scaleを弄らない。
NewInputSystemの導入
パラメータ
割当以外にも、操作感を弄って調整する値にもSerializeFieldはつけて構わない
StateMachine作成
Playerの上位クラス Entityの作成
rb, co, srの取得
右向き判定や地面判定など、敵味方で共通の処理を今後入れていく
向きのロジックを作る
反転というのは、transform.roration.yを180にすること
Playerを動かす際に与えられたxが、以下ならそうしてやればよい
+ かつ いま向いているほうが右じゃないなら、Flip
- かつ いま向いているほうが右なら、Flip
ジャンプ処理
PlayerJumpState
Idleから、inputのジャンプボタンが押されたら、このStateに移動してやればよい。
moveでも、移動してやりたい。
→どちらにも、ジャンプが押されたら...という処理を書くのか?
PlayerGroundStateという、その2つを包括するState(スーパーステート)を作って管理する
それをidle, moveが継承すれば、ジャンプが押されたときどちらでも反応するようになる
状態と、必要な条件
Jump
Enter時(押されたとき)、Y方向にplayer.jumpForceを加えてやる
linearVelocity.yが< 0になったら、FallStateに遷移してやる
Fall
Enter時、-Y方向に力が加わっている
地上判定になったら抜け出してやる
Fallが抜け出すために(Idle)を設定するために、地上判定のチェックが必要
衝突検知Collision Detection
Raycastが、地面レイヤーを検知したら地上にいるとすればよい
Gizmoで書いて、どれくらいの距離が必要かを把握
その後、Raycastでその距離に向かって判定を伸ばしてやるとよい
ジャンプしながら動きたい
今入力受付は、idleとmoveのみなので、空中操作ができないので調整してやればよい
Jump / Fallのスーパーステート、AirStateを作る
どちらの状態でも、inputを受け付けるようにしてやるために作った
素材を探そう
Animatorを使いたい。
お借りすることにする。まずは.gitignoreに追加してgit管理をやめよう。
ダークナイトを使う
Playerの子要素に、Animator オブジェクトを作成してSprite Rendererを割り当て
じゃぎじゃぎなら、スプライトを加工しよう
compression none, Filter Mode Point, Pixels Per Unit 64くらいで。
https://scrapbox.io/files/692da16167ff2171b6d18212.png
こんな感じになった。真ん中の軸(Pivot Pointがずれているので、直してやる
こんな感じ→https://scrapbox.io/files/692da1cd2cc4713ef9abd1fb.png
個別設定だと大変なので、一括で変更しよう
Sprite Mode をMultipleにして、...と思ったが、unityでは一括操作できないらしい
一旦自分で切っちゃおう。
アニメーション
Animator componentを付与
Animation Controllerを作成(Playerとした)
作ったControllerを、コンポーネントに割り当て
クリップ作成して好みのレートにすればよい
⭐アニメががたがたな場合
スプライトをautoでスライスすると、余白が勝手に設定されたりする
なので、そのスプライト中の最大のx, yをすべてのスプライトに割り当てたら安定した。一旦これで。
移動と待機時のアニメ切り替え
新しいclip、playerMoveを追加してアニメを用意
Animatorを開く
idle
idleというboolパラメータ作成
make transitionでidleからexitへ伸ばす
Conditionsで、idleがfalseとなった場合にexit
Has Exit Timeのチェックを外す
Transition Duration を0に
move
moveというboolパラメータ作成
make transition
Entry -> move
move = true
move -> exit
move = false, Has Exit Timeのチェック外して, Transition Durationを0
⭐この作ったboolステータスを、Playerから使えるようにする
まず、EntityStateでAnimatorでセットしたboolパラメータの名前を表すprotected string animBoolName;を定義する。
次に、EntityでAnimator animを定義。これはrigidbodyの取得などと同じだが、AnimatorはPlayerの子要素に入れているので、anim = GetComponentInChildren<Animator>();とする。
EntityStateを継承するPlayerStateで、入口処理Enter()時の全ての共通処理として、player.anim.SetBool(animBoolName, true);とする。anim.SetBoolはアニメーターcomponentのメソッドで、指定のアニメパラメータの値を調整できる。例えばplayer.anim.SetBool("idle", true);とすると、↓のidleがtrueになり、playerIdleのアニメーションが流れることになる。
https://scrapbox.io/files/692db1e75e8e39fff6064a5b.png
出口処理Exit()には、anim.SetBoolをfalseとして設定しておく。
これで、PlayerがStart時、まずidleStateに入ると、idleの入口処理と並行して親EnterのPlayerStateのsetboolが走ってidle = true。moveに入るとidleの出口処理でidle = falseになり、moveの入口処理と並行して親EnterのPlayerStateのsetboolが走ってmove = trueとなる...というような挙動が実現できるようになった。
ジャンプ / 落下時のアニメを用意する
アニメ側
jumpとfallを作ると、Animatorに素材ができているが、今回はブレンドツリーを使うので、こちらは使わない。ブレンドツリーではパラメータに応じた複数アニメをブレンドできる
Create State > New Blend Tree 。
作ったブレンドツリーに入って、Add motion field
Automate Threshholdsをオフ
playerJumpとplayerFallをセット。Threshholdをfall -1, jump 1と設定
Parameterに、yVelocityとして、このanimパラメータを使うことにする。
jumpFall というboolを作って、Make Transition設定。
enter -> jump/fall -> exitに、適切なbool値を設定。
コード側
PlayerでjumpState = new PlayerJumpState(this, stateMachine, "jumpFall");と定義することで、入口処理でanimBoolNamejumpFallがtrueとなるため、ブレンドツリーのjump/fallに入る。ブレンドツリー側ではyVelocityというパラメータでjump / fallのアニメを使い分けることになるため、animにはyVelocityという値も渡す必要がある。
yVelocityは今回、PlayerStateで扱うようにする。LogicUpdate()で、所持しているrbの持つrb.linearVelocity.yを、常に渡すようにする。yが+ならjump, -ならfallで分岐するようになる。
リファクタリング
PlayerStateでplayer.anim.SetBool(animBoolName, false);としているが、EntityStateに持たせることができれば、anim.SetBoolで敵味方共通して使えるようになりそうだが。
現在rbはEntityStateで管理できているので、そちらと同じにすればよい。PlayerState生成時には、コンストラクタでPlayerインスタンスが渡される。なので、以下にする
EntityStateにprotected animとする
PlayerStateのコンストラクタにanim = player.animとする
EntityStateのEnter, Exitにanim.SetBoolとすれば、EnemyStateを作ったときはenemy.animを入れてやれば同じ処理でboolの操作ができるようになる。
ウォールスライド
壁にはりつきっぱなしになる不具合の修正
Materialを作成し、Friction(摩擦)を0にして、それをplayerのrbに割り当てた。
壁チェック
地面判定と同じように、Raycastで行う。ただし、向いている向きに沿ってx軸の+に伸ばすか-に伸ばすかを決める。
そのフラグがONになり、空中にいたらWallSlideStateに遷移すればよい
アニメ
airAttackのものを反転して使ってみる。自分でclip studioを使って反転。
各boolを割り当て。
WallSlideState
AirState中に壁に触ったらEnter.地面に触れたらExit。
? ジャンプ中にも壁に張り付いてしまうので、少し調整したい
FallState中に、wallDetectedがtrueになったら移行する
つまりAirStateの管理下ではなくなった(Jumo中は違う)ので、PlayerWallSlideStateは、PlayerState自体の個別ステートとしよう
抜ける条件について
例えば、逆方向に入力したり、ジャンプを押したりしたら離脱させたい。
壁ジャンプ
wallSlide中に、ジャンプが押されたら遷移する
Enterで指定方向に飛ばす
yが0を下回るか、壁に触れたらまたwallslideに
ダッシュ
Animator
クリップ作成、アニメ割当
make transition dash true, dash falseで。各チェックを外す。
PlayerDashState
Dashは、緊急回避の手段としたい。つまりGround, Airのすべての状況で使いたい。どちらも包括するPlayerStateがあるのでそちらでダッシュボタンが押されたときに動作させるようにしよう。
ダッシュ持続時間のために、タイマーを作る
今後、敵味方で使うので、EntityStateでタイマーを定義。LogicUpdateでTime.deltaTimeを計測しておく。DashStateに入ったら、入口処理でstateTimerに1など値を設置。値は減っていくので、0になったらChangeStateすればよい。その際、groundDetectedがtrueならidle,falseならfallに移行すればよい。
2段ジャンプを作ってみたい...
ジャンプ可能回数と、現在のジャンプ数(何回やったか)をPlayerに持たせる
GroundState
Enterで、ジャンプ回数をリセット
ジャンプが押されたとき
&& jumpCount < maxJumpsをジャンプ移行条件に変更
JumpState
入るたび、現在のジャンプ回数を加算
AirState
ジャンプが押されたとき&& jumpCount < maxJumpsをジャンプ移行条件として追加。GroundStateと同じ。
WallSlideState
入るときに、現在のジャンプを1にする
2段ジャンプ後はりついたときのリセット
落下してはりついたとき、空中ジャンプが2回できるようになるのを防ぐ
WallJumpState
ジャンプを押せば、JumpStateに移行できるようにする
できた!
空中制動の調整
入力しているときだけ、その方向に進ませたい
と思ったけど、結構手が忙しくなる。一旦このままでもいいかもしれない。
アタックモーション
アニメ作成
State
まずは、GroundStateで攻撃を入力したら移行するようにしよう。
? いつ抜け出すか。
アニメーションイベントトリガーを使う。アニメーションの最後のフレームと同時にメソッドを呼ぶことができる。つまり、アニメが終わったら、stateを戻すような処理を書いて割り当てればよいだろう。
参考にした実装の流れを理解する
Playerは、stateMachine.currentStateを持っている。stateはEnter時、triggerCalled = falseとなるようにした。PlayerのAnimator componentに、PlayerAnimationTrigger.csを割り当てた。この中にアニメで呼ぶメソッド(PlayerAnimationTrigger.CurrentStateTrigger())を作成。攻撃アニメの最後のフレームで呼ぶようにした。このメソッドの動きは、以下の通り。
player.CallAnimationTrigger() -> currentState.CallAnimationTrigger() -> EntityStateの、protected bool triggerCalled;がtrueになるという仕組み
攻撃ステート中は、LogicUpdate()でtriggerCalledを常に監視する。trueになったら、idleStateへと遷移する。
アニメイベント自体は、現在のStateに何かが起きたという検知をするだけ。AnimationTrigger()がChangeState()などとしてしまうと、animの責務が崩れる。なので、検知があったことだけを知らせる仕組みにしている。
コンボアタック
2段目の攻撃、3段目の攻撃を作る。フレーム最後にトリガーを挟んでおく。
animator
自動で作られた攻撃3つのアニメ管理レイヤーは消して、新しくSub State Machineを作る。この中で何を再生するかを決定し、どうしたらexitになるのかなど、3つの攻撃モーションに関して管理することになる。
まず、basicAttackがtrueになったらEntryから遷移させる。Sub State Machine配下で管理。攻撃モーション3つのclipをドラッグ&ドロップで持ってきて対応する。
https://scrapbox.io/files/692ecf8d52cb7e85f5ca6f9e.png
indexをつくって、1ならbasic1に、2ならbasic2に、、3ならbasic3にするように設定。exitの条件は、basicAttackがfalseの時。
AttackState調整
comboIndexを持たせて、Enter()時にanim.SetIntegerで設定。Exit()時に、comboIndex++としておく。こうなれば次呼ばれたときにcomboIndex = 2になっているため、basicAttack2が呼ばれることになる。
comboIndexの保持を調整する。攻撃後しばらくなにもしなければ、indexがリセットされるようにしよう。Playerに、どれだけindexを保持するかの時間を持たせる。
キューさせる
現状、攻撃にidleStateが挟まる。先行入力を利かせる
AttackStateのUpdate()中にボタンが押されたとき、先行入力を有効にする。AttackStateから、idleでなくもう一度AttackStateへ。ただしこのやり方だと、以下の問題が発生する。
AttackStateの出口処理で、anim.SetBool(animBoolName, false);が発生する。次に同じ入口処理で、anim.SetBool(animBoolName, true);が発生する。ChangeStateは、以下の通り
code:stateMachine.cs
public void ChangeState(EntityState newState)
{
currentState.Exit(); // 出口処理
currentState = newState;
currentState.Enter(); // 新しいStateの入口処理
}
Exit()でfalseとした同メソッド中で、Enter()でtrueに設定されている。そのため、結果的にanim.SetBoolが動作していないような挙動が発生し、スタックする。なので、少しのフレームの間だけでもバッファを持たせなければならない。Coroutineに1Fだけ待たせるメソッドが用意されているので、それを使えばよい。AttackState中に連打されたとき、playerのCoroutine経由でChangeStateするようにする。
code:Player.cs
private IEnumerator EnterAttackStateWithDelayCo()
{
yield return new WaitForEndOfFrame();
stateMachine.ChangeState(basicAttackState);
}
先行入力の受付時間をSerializeFieldで管理したい
現状、AttackState中すべてのタイミングで連打したときに次のモーションに移る。
Playerに攻撃先行入力受付時間AttackInputBufferTimeと、最後に攻撃入力した時間lastAttackInputTimeを持たせる。LogicUpdate()中に攻撃ボタンが押されたとき、lastAttackInputTime = Time.timeとする。攻撃が終わったとき(TriggerCalled)、Time.time - player.lastAttackInputTime <= player.AttackInputBufferTime;という式を判定する。
ex) 現在52.00秒 input時間が 51.99秒とすると、0.01秒 bufferTime より <=なら、true
51秒とすると、1.00秒 bufferTimeより <=じゃなかったら、false
これで、トリガーが終わったときの時間と、プレイヤーの入力時間を比較することができる。トリガーが53秒におわったとして、最後の受付が51秒とかなら、falseになってidleStateに戻るが、52.9秒くらいに攻撃が押されていたら、先行入力とみなし、AttackStateに遷移させる。
攻撃中、入力方向で違う向きに攻撃を出す
AttackState Enter()時に入力をチェックし、attackDir = player.moveInput.x != 0 ? ((int)player.moveInput.x) : player.facingDir;という感じで-1 か +1の値を保管。攻撃のx加速度を加えるメソッドApplyAttackVelocity()のx座標に、この値をかけ算してやればよい。
ジャンプ攻撃
どんなものがいいだろう。振ったとき、ちょっとだけ滞空してほしい。
PlayerAirAttackStateとする。
AirState中に、攻撃ボタンを押されたら遷移
Enter()時、x,yの慣性を完全に止める
タイルを配置して、Collisionとして扱う
window > 2d > tile palette
TilePaletteウィンドウで、create new palette で好みの名前を付けて、ファイルに保存
TileSetを分割する。多分結構大変。
まずクリスタで、キャンパスを16の倍数にする
160 * 320にした。
分けていく。土は土、草は草だけのタイルにする。
pixel per unitを16にして、grid by cellsizeで16として切る。
並び替え
? タイルを追加するときは、編集モードを終了させてからやる。ばらける可能性がある
選択がs, 移動がmなど。マウスホバーすればいくらでも出てくる。
消すときは4番目のツール(U) にShiftで、選択範囲を一括で消せる
置いた!
コライダーをつける
オブジェクトで、2d > tilemap > rectangleを選択するとタイルセット用のobjectが出る。Groundレイヤの付与をすること。Tilemap Collider 2dを付与。
コライダーの最適化を行う。Composite Operationをmergeに設定。Composite Collider2dを別途付与する必要があるので付与。これで、書かれたタイルはすべて1つのオブジェクトとして扱える。このままだとステージ自体が落ちてしまうので、Rigidbody2Dの設定も必要(Body TypeをStaticに設定。)
Composite ColliderのGeometry TypeをoutlinesからPoligonsにする。輪郭だけでなく、内部にも判定を付与してくれる。
背景とsorting layerの設定
まず背景は、空のオブジェクトに、sprite rendererを持たせる。
sorting layer > Backgroundを作る。そして、Default(1)より手前(0)にすると、基本的に作成されるものより後ろになる。
PlayerのAnimator要素のスプライトレンダラにも、Playerというsortinglayerを作って、(2)として割り当てる。これでplayerが最も前に出ることになる。
タイルマップ用のGroundも設定。タイルマップは、Tilemap Rendererからレイヤを割り当てられる。
code:txt
Layer 0 : Background
Layer 1 : Ground
Layer 2 : Player
Layer 3 : Default (こうしておくと新しい要素を作ったとき、一番前に出るのでみやすい)
カメラ
window > package manager > Unity REgistryで、Cinemachineを入れる
おわったら、オブジェクト作成でCinemachine > Target Canvas > 2D cameraで作る
既存カメラと共通の親を作り、まとめる。
トラッキングターゲットにPlayerを入れると、Playerに追従する。
より調整する
Cinemachineを入れると、従来のMain Cameraオブジェクトに、オプションが追加される。このコンポーネントで調整した値は、既存のカメラの値をオーバーライドする。
https://scrapbox.io/files/692ff745b1963ca1e0f5c87b.png
Cinemachine Camera側
Chinemachine Position Composerで、Screen positonのyをさげておくと、2Dアクションっぽい、キャラがちょっと下にいる感じになる
デッドゾーンの設定
指定したエリア内なら、歩いてもカメラが追従しなくなる
ハードリミット
カメラに、黄色い点があるが、カメラはそれを追従する
その黄色い点は指定したもの(今回Player)を追いかけるが、それの制限。
ダンピング
ある程度の余裕。0にすると、playerが動いた瞬間にカメラも動く。上げるとカメラの動く速さがにぶくなる
Lookahead
プレイヤーの動きを予測して、カメラを向ける。右にいっているなら、より右を移すなど。
カメラの移動制限を作る
Confinerを作る。空のオブジェクトで、polygon colliderを作成。points を5 > 4にして、カメラの範囲にコライダーを広げる。isTriggerにしておくことで、このコライダーの範囲をカメラの移動範囲とすることができる。
CinemachineCamera > Add Extension > Cinemachine Confiner 2D
背景用のタイルマップを作る
草にのってしまうので、背景にしよう
オブジェクトで、2d object > tilemap > rectangular 。 これで別枠として、各種タイルを貼り付けていける。
Flip()の不具合修正
タイルマップ上でジャンプすると、たまに反転する。タイルマップのcomposite colliderのoffset instanceを0にすることで、滑ってflipが作動してしまうことを防いだ。
敵を作る
まず、敵自体のEnemy, EnemyStateを作る
ObjectにAnimator子要素を追加。Animatorコンポーネント を割り当て。Animator Controllerを作る。スプライトレンダラを割当て。rb割当。重力加速度をplayer側と合わせる。collision detectionもcontrinuousに。freezeRotationのzを固定して、こけないように。コライダーの割当。box colliderを使ってみる。
sorting layerでEnemyを追加。playerの後ろにする。
code:各種ファイル構造
<敵味方>
Entity
Enemy
DemonHound
Player
<state>
EntityState
EnemyState
EnemyIdleState
PlayerState
PlayerGroundState
PlayerIdleState...
という感じ。敵はさらに入れ子になるが、基本Idle等も使いまわせるようにできれば楽。
DemonHound.cs
Playerと同じく、stateMachine.Initialize(idleState); で、初期設定 + 指定したstateの入口処理を行えばよい。
Entity.cs
地面判定に、今までオブジェクト自体のtransformを使っていたが、serializeFieldで割り当てることにする。Playerは自身を割り当てればよいが、敵はEmptyObjectを作って、自身の手前をチェックするようにしてやると、地面から落ちなくなる。
速度に応じて、アニメの速さを調整する
AnimatorのパラメータにmoveAnimSpeedMultiplierを作って、インスペクタで割り当てた。
https://scrapbox.io/files/69303a21c29225da61f974fe.png
BattleState, AttackStateの作成
BattleでPlayerをおいかけて、Attackに移行するようなしくみ
Animator
ブレンドツリーで、moveとidleのアニメを作る。xVelocityというfloat アニメパラメータを用意して、その値でアニメを切り替える。-1と+1のときmove, 0のときidleとすればよい。
プレイヤーの感知機能を作る
WallDetectionなどと同じイメージで作ればよい。idle, move中に感知すればよいので、そちらのスーパーステートを作って管理すればよさそう。まず、Entity.csに書かれたGizmosの関数をprotected virtual voidとして、オーバーライドできるようにする。Enemy.csで、プレイヤー感知のGizmoをオーバーライドする形で追加するときれいに書ける。PlayerとEnemyという新しいレイヤを作って、それぞれに割り当て。
BattleStateに入った後、Playerの情報をもらって、そちらに向かって進行したい。ただ、EnemyBattleStateでどうやってPlayerを取得するべきか?下記でも可能だが、可能ならやらないほうがいい(重い)。
code:EnemyBattleState.cs
private Transform player;
public override void Enter()
{
base.Enter();
player = GameObject.Find("Player").transform;
}
感知させたときにRaycastを使ったが、そのRaycastから情報を得るほうが楽。player = enemy.PlayerDetection().transform;とするとよいだろう。
敵と味方が押し返すのを防ぐ
Edit . Physics2d > Layer Collision Matrixで制御すればOK
敵の攻撃時、ジャンプさせたい
トリガーかなあ?demonHound用のtriggerを作り、割り当てることにした。
攻撃処理の実装
攻撃アニメーションの剣を振ったモーションにトリガーを挟む。この処理は敵味方どちらでも使うので、Entityとしよう。また戦闘関連の処理をEntityCombatとしてまとめる。(Entity自体に書くこともできる。ステータスやヘルスなどを分けるのと同じ。)挟むトリガーは、攻撃時にPhysics2Dで円形の判定を作って、感知したコライダー全てを配列に入れる。その配列をforeachでダメージを与える処理を作っていけばよい。
EntityHealthとして体力を持たせる。TakeDamage()やDie()などの基本的な処理を持たせる。ただ、敵はダメージを受けた時にBattleStateに入ってもらいたい。なので、敵にはEntityHealthを親としたEnemyHealthを持たせて、overrideしよう。ただし、現状問題がある
code:EnemyBattleState.cs
// public override void Enter()
base.Enter();
if (player == null)
player = enemy.PlayerDetection().transform; // 感知したRaycastのtransform
敵は目の前に検知処理を伸ばして、playerを補足する。その後、そのplayer情報を参考に近寄ってくる。後ろから殴った場合の考慮が必要。TakeDamage()にはdamageだけでなく、Entityのtransform情報も送ってやるとよい。今回は、以下のようにした
EnemyHealthでTakeDamageをオーバーライド。攻撃してきた相手がPlayerだったなら、TryEnterBattleState(Transform player)の実行。
このメソッドは既にbattle, attackだった場合は考慮しない。player情報をenemy.playerに格納してbattleStateへ。
enemyBattleStateでは、enemy.playerを参照し、距離などを決める。感知した場合はレイキャストからplayer情報が参照できるし、後ろから殴られた場合は、TakeDamage()からとってきたplayerを参照するようにして対応する。
VFXを作ろう
ダメージを受けた時に、スプライトを真っ白にすればダメージを受けている感が出るので、そういった実装を作る。
Materialsでテンプレートを用意する。ShaderをGUI > Text Shader に設定。これをAnimatorのsprite render等に張り付けると、まっしろになる。なので、ダメージを受けた時にAnimatorにこのマテリアルを付与し、すぐはがせばよい
ノックバック
Entity, EntityCombat, EntityHealthのどこにノックバック時間やベクトルの変数を置くか。大きな敵なら控えめ、弱い敵なら思いっきり吹っ飛ぶなど、受け手をイメージし、healthに配置することにする。
敵のやられ処理
Die()したとき、EnemyDeadに遷移させ、その状態では跳ねあがって画面外に移動させ、その後Destroyさせればよいだろう
EntityにDeathという、死亡時の共通処理を書く。そして、Enemy.csでstateMachine.Changestate(enemyDeadState)へと変えるようにオーバーライド。Health側で、Die時にentity.Death();を呼ぶ。Stateの入口処理として、アニメを切り替えるためのsetboolが動くが、今回はアニメを使わない。また、最後のフレームで止めるようにするので、DeadStateの入口処理ではbase.Enter()は行わないようにする。anim.enabled = falseとすることでアニメ処理が切れるので、入口処理でアニメを切るようにすればよい。
画面外に消えてほしい。これはcolliderを無効化すると、勝手に落ちていくようになる。なので、ちょっとはね上げる力を与えて、coを切り、重力加速度を与えてやると、画面外に飛び出る。
Playerのやられ処理
EntityHealthのDie()で、Entity.Death()が呼ばれる。PlayerでオーバーライドしてdeadStateに変えるように。そこで、inputをうけつけなくしてやれば完成
PlayerのDeathに購読させて、色々な要素を起こそう
画面振動を入れよう!
カメラを揺らすのは、カメラの責務。カメラにCameraManagerを空objectとして持たせる。同じComponent階層からCinemachineを取得し、揺らせばよい。
CameraManagerに、Cinemachine Impulse Sourceを割り当てる。いっぱい種類があるので、間違えない(Colliderを使うものなどがある)
Cinemachine CameraにAdd Extensionからコンポーネントを付与する。Add Componentから検索していれるとヒットしなかったので、ちゃんとここからセットすること。
https://scrapbox.io/files/693157031d26c3267952ae07.png
インターフェース導入
Chestという、攻撃をすると開く物体を作った。これにIDamagableを付与して、ダメージを受けられる共通の約束のある物体として扱えるようにしよう。
code:IDamagable
public interface IDamagable
{
public void TakeDamage(float damage, Transform attacker);
}
EntityCombatで、下記のように書く。
code:EntityCombat.cs
public void PerformAttack()
{
foreach (Collider2D targetCollider in GetDetectedColliders())
{
// 元々
//EntityHealth targetHealth = targetCollider.GetComponent<EntityHealth>();
//targetHealth?.TakeDamage(damage, transform); // ?. : null条件演算子
IDamagable damagable = targetCollider.GetComponent<IDamagable>();
damagable?.TakeDamage(damage, transform);
}
}
ターゲットコライダーが、IDamagableを持っていたら、それぞれのTakeDamage()を実行するようになった
⭐下記のようにも書けるが、PHP的な悪い慣習の書き方になるのでNG。
code:c#
var collider = targetCollider; // 対象が何かわからず、何かわからないもののTakeDamage()を呼ぶ
targetCollider?.TakeDamage(damage, transform);
VFXその2
斬ったとき、エフェクトを出そう
斬撃VFXというGameObjectを作成し、アニメーターを用意。アニメを設定し、Prefab化する。
EntityVFXで管理できるようにする。Prefabを割り当てられるSerializeField, 割当カラーの設定。このPrefabをInstantiateするようなメソッドを用意。これは、EntityCombatのPerformAttackで、IDamagableの条件を満たしたときにInstantiateで表示するようにすればよい。
VFX自体には、万能に使えるVfxAutoController.csを作って割り当てた。Destroyの時間設定や、表示される場所を適度にずらしたりする役目を持たせる。
体力バーを作る
敵の子要素として、Canvasをもたせ、その中にUI > Sliderを持たせる。CanvasのRender ModerをWorld Spaceにして、GameObjectとして扱えるようにする。Canvas名をHealthBarUIなどにしておき、positionを変更。元がUI Canvasなので、サイズがとても大きいはず。Scaleを調整して、敵の頭上などに置いておこう。
子要素であるSliderの調整。塗りつぶす色などを調整するときはFill Area > Fillの色。SliderのValueの値に応じて変わるので、この値をEntityHealthのcurrentHpと紐づけてやればよい。今回、Handle Slide Area(バーのノブの部分)はいらないので、消した。HPバーのガワ部分が丸めの四角になっているので、Backgroundのスプライトを変える。Fill Area > Fillのスプライトも変えておく。
ギャップを埋める。valueを1としても、HPバーが埋まらない。 / value=0としても0にならなかったりする。
https://scrapbox.io/files/6932557b7e8440e1409f060f.png
Fill Areaの範囲をScene画面のRect Tool等で調整しよう。
EntityHealthの調整。EntityHealthを割り当てている、子要素に今回UIを作っているので、healthBar = GetComponentInChildren<Slider>();で取れる。取得後は、healthBar.value = currentHp / maxHp;として値を更新できる。
UIが反転しないようにする。Flip()で敵の要素全てが反転するので、体力は反転しないようにする。これはUIにScriptを割り当てて、Rotationを固定するのが良いだろう。
code:cs
public class UIMiniHealthBar : MonoBehaviour
{
private void Update()
{
// UIバーが反転して、体力の減りも反転してしまうのを防ぐ
transform.rotation = Quaternion.identity;
}
}
これでもできるが、敵が画面にたくさんいるとUpdate()が無数に走ることになる。なので、これもFlip()へのアクションイベントで対応しよう。
Player側のヘルスも作ってみる
スライダーのBackgroundに好みのHPバー画像をくっつける。適当に引き延ばされているので、いい感じに調整する必要がある。スプライトエディタで、引き延ばしてもいい部分、ダメな部分を設定する。
https://scrapbox.io/files/69325e812b1d8261345f5e08.png
https://scrapbox.io/files/69325efc23bdd940a4eb704f.png
ハートのアイコンを割り当てる。このときSprite Rendererでなく、Imageとして割り当てる。(Canvas配下はImageで管理する。レイヤー調整などが大変になるので)
ステータスシステム
EntityStatusを作る。maxHp, Attack, Defense, maxMpを持たせて、EntityHealth側では、currentHp = entityStatus.maxHp;などとして所持するようにすればよい。
EntityStatus.csとStatus.csとを分割する
EntityStatus側では、それぞれ計算されたステータスデータの結果を返す。ステータスの束を持ち、それぞれの値を返すようにしておくとよい。例えば、攻撃力 10 + 斧を装備して + 5 とした場合、このクラスからは15を返す。このクラスの責務は、「攻撃力が15」というデータを返すこと。
Status側では、計算自体の処理を行ったりする。補正の足し引きすべてをStatusの責務とする。単一のステータスのみを返す。例えば、攻撃力 10 + 斧を装備して + 5 とした場合、このクラスからは元になった値の10と、補正値5を管理するパーツとして扱う。このクラスの責務は10と5を計算して、15という値をつくること。
いろんな値を用意した
ダメージ計算
EntityCombat.csでEntityStatus.csの値を読み、計算していく。ダメージベースはatk - def。
回避エフェクト
ttfフォントを落とした。GameObject > 3D > text - textmesh proで作って、ttfファイルを右クリック > create > textmeshpro > font asset > bitmapで作る。フォントはスプライトと違うpositon参照なので、出現場所は画面を見ながら適度に調整していくのがいい。
GameManagerの用意
多少用意できてきたので、ゲームを管理するGameManagerなどでクリアフラグを管理しよう。
まずPlayerのEntityHealthをPlayerHealthに置き換えて、Die()をoverrideできるようにしておく。ゲームオーバーになったときなどにデータを弄ることになる。(you died!などのUIを出したり。)
UI(Canvasすべて), PlayerなどをPrefab化しておくことで、全てのシーンでPrefabを上書きするだけで共通の状態を持たせることができる。なので、Prefabにしておこう。
ポータルを用意してテスト
Waypoint。Enumで、それぞれ状態を持たせる。入口と出口、それ以外。lv1の出口とlv2の入口をつなげたり、そういったときにenumで管理する。
シーンは、GameManagerの責務とする。
ゲーム自体のStateを、Enumで管理する
まずはざっくり、Ready(カウントダウンとか), Playing(戦う), Result(クリアかgameoverか)
スポナーを作る
レベルアップシステムを考える
PlayerLevel.cs
Start時にLv1の時のステータスを呼び出し、Status.csの基礎値にセット。また、現在の経験値CurrentExpも持たせる。またレベルが上がったとき用のアクションイベントを持たせておく。GameManagerとかで、LvUP通のUI等を出すときに使う。
PlayerLevelTable.cs
ScriptableObject。とはいえ、メソッドなども持たせられて他のクラスで宣言すればそのメソッドを使える。レベルとその時点でのステータスを持たせて、levels[0]とかで、Lv1のステータスやLv2のステータスを取得できるようにしておく。
GameManager.cs
新しい状態としてLevelUpを追加。Start()時にPlayerLevelのアクションイベントに購読しておく。購読させるのはHandleLevelUp()。StateをLevelUpに変えて、ログを出しておく。timescaleを0.01とかにして、疑似的に止めておこう。
Enemy.cs
Death()をオーバーライドし、GameManager.Instance.playerLevel.AddExpで自身に紐づくEXPを渡す。
EnemyReward.cs
Enemyにくっつけて、serializeFieldで経験値を持たせておけばよい。将来お金とかも持たせられる。
自分の体力のUIを画面依存の場所にしたい.画面下真ん中くらい。
CanvasにUIInGameをつくって、shift + altで目一杯広げておく。PlayerのHealthBarのsliderをひっぱって、この要素に入れる。shft alt で広げたので、いい感じの場所に配置できる。
経験値バー
UIInGame.csで管理。HPバーと同じく、EXPバーを参照できるようにする。
Canvasに、UIInGameという要素を用意。その中に体力バーと同じ階層で、ExpBarを持たせた。UIInGame.csに、このExpBarの管理をするUpdateExpBar()を設置。GameManagerに、[SerializeField] public UIInGame UIInGame; として、UIInGameを持たせる。Enemy.csでEnemyが倒れた時にDeath()の内部で、GameManager.Instance.UIInGame.UpdateExpBar();として、GameManager.Instanceから呼び出すようにした。
この書き方だと、Enemy → GameManager → UIInGame → Sliderというように、EnemyがUIを触ることになっている
責務を切り分ける
Enemyは、経験値を増やす
PlayerLevelは、経験値が変わったというイベントを発火する
UIInGameは、そのイベントで経験値バーを発火するようにする
レベルアップでの体力バー
最大値が増えるが、currentHpはどうしようか?現在は、最大値だけ増える。満タンのほうが嬉しいと思うし、簡単になると思うので導入してみよう。
Player.Death()とPlayerHealth.Die()の責務分割
Player.Death()は、ゲーム的振る舞いの担当。例えばEnemy.Death()では経験値の分配を行って、DeadStateに変えている。Player側も同じで、カメラを揺らす。PlayerのStateを変える。敵のStateをBattleからIdleに戻す。
PlayerHealth.Die()は数値的事実を扱う。つまり、一番最初に死亡を判断するのはここ。イベントの始点とする。
現状の整理
はじまりは、EntityHealth.TakeDamage()からDie()が呼ばれる。PlayerHealth側でoverrideしてbase.Die()が呼ばれるので、そこからEntity.Death()が呼ばれている。EntityDeathは何も書かれてない。Player.Death()が走る。InvokeでEnemyのBattleStateを解除する。カメラを揺らす。自身をDeadStateにする。ここでbase.Dieは終わり。PlayerHealthに戻って、Invokeで購読されたGameManagerのGameOverを走らせたりする。
code:txt
この状態でダメージを受けて HP0 になると:
PlayerHealth.ReduceHp
→ base.ReduceHp(EntityHealth)で Die() 呼び出し
Die() はオーバーライドされているので PlayerHealth.Die() が実行される
その中で base.Die()(EntityHealth.Die)
→ entity.Death()(Player.Death)が 1回目 呼ばれる
その後 OnDied?.Invoke() → Player.HandleDied() → Death() が 2回目
= つまり Player.Death() が毎回 2回呼ばれている という状態
healthがダメージ計算をして、以下の流れに簡単にした。
Player.dieを呼んで画面を揺らすなど
Enemy側にhealthの持つイベントを購読させて、state管理
Player死亡時を、より詳細に
画面をゆっくりにする
GameManagerの責務。
死亡時、DeadStateに変える
完全にPlayerの責務。
カメラを揺らす
PlayerHealthのOnDiedイベントに紐づけて、CameraManagerが揺らすべきか。
実装できてきた内容
code:txt
・HPバー、EXPバーの用意
・敵を攻撃出来て、倒すとレベルが上がる設計(スキルは未実装で、通常攻撃だけ)
・敵のスポーン(敵のPrefabが1つ設定できて、それが定期的にInstantiateするだけ)
・指定の秒数耐えたらクリア(クリア画面などは未実装で、フラグだけ)という設計
・playerの体力がなくなったらゲームオーバー(画面は未実装で、フラグだけ)
# 今後
ゲーム全体のStateの整理, 簡易リザルト用意
Spawnerの設計
イベント整理
UI整理
スキルなど?
GameManagerのStateを明確にする
Canvasに、UIResultとしてまとめる。リトライはSceneLoadでまとめることにする。その際、やり直した場合GameManagerからPlayer,SpawnerなどSerializeFieldで割り当てていた参照がはずれてしまうことになった。なので、初回も2回目以降も、SerializeFieldから参照しない形で各コードを取得するように書き直すのがよい。
敵のヘルスバーを、被弾時から表示するように
UIMiniHealthBar.csでSliderを制御する。EnemyHealthでTakeDamage()の時に、同じようにイベントで通知してやれば良いだろう。
LogHelperを用意して、デバッグを楽にした
スポナーの詳細設計
現状は、敵を出す、場所を決めるという2つの責務を担っているので、より詳細に分ける。
敵の出す場所をEnemySpawnPointsで管理するようにする。出現させる敵の詳細は、WaveManagerで管理するようにした。WaveManagerはステージ情報(WaveがまとめられたScriptableObject)と、EnemySpawnPointsを持たせる。
クリア条件
通常Waveは、全員倒すか時間経過。
ボスウェーブは、ボスを倒さないとクリアにならない。
クリア条件の調整
Stageが一巡(全てのWaveが呼ばれたら)したらクリアとする。
次に、Waveのクリア条件に全滅 or 時間制を導入。
敵の討伐数チェックは、EnemyHealth.Die()にアクションイベントを紐づけるのはどうだろう。ただ、敵はPrefabから呼ぶ。Prefabから生成した瞬間に購読させるのか?それはできないので、staticイベントにして、個別関係なく、何かしらのEnemyHealth.Dieが呼ばれたとき、共通で呼ばれるようなイベントにしてやるとよい。
GameManagerのStateを細分化する
カウントダウンを出したい。UIをCanvas配下に、新しく持たせる。
ボスアラートを出す。isBossWaveで判断できそうだ。Waveを流す前に、isBossWaveをチェックして、そうならそのアラートを挟めば良さそう。
導入できた。ボスアラートはStateを変えなくても良さそう。いい感じ!
優先して作るべきものはどれだろう?
code:txt
・敵やVFXをInstantiate, Destroy()しているのでObjectPoolにする
(通常の小規模ゲームならいらないと思うが、ポートフォリオとして実装しておきたい)
・BossWaveの条件を敵全討伐にしているが、「ボス」が討伐されたら、雑魚敵がいっぱいいてもクリアにしたい
(WaveConfigをもう少し弄る必要がある)
・現状雑魚敵が1匹しかいないので、ボス敵を用意してみる
・タイトル画面がないので、タイトル画面を作ってみる
# 優先度↑
ボス実装 + ボス撃破でクリア(雑魚残っててもOK)
雑魚をもう1〜2種追加(近接 + 遠距離など)
スキルの枠組み実装(最低限の近接・魔法分岐)
タイトル画面・リトライ/タイトル戻りを整える
余裕があれば ObjectPool(弾/VFX)を導入・READMEでアピール
さらに時間があればポーズ・音量調整・チュートリアルなどの細部
ゲームのゴールを作って、技術スタックなどは後からでも構わない。なので、ボスの処理を実現しよう。
ボスを倒したら、雑魚が残っていてもクリアにしよう
WaveClearTypeに、KillBossを追加
HandleEnemyDied()で、EnemyHealthのDie()時に、EnemyHealth.IsBossがtrueなら、クリア通知をGameManagerに送ればOK。
ボスづくり
DemonHoundをベースに作り替えていく。変える項目をメモしておこう。
まずはベースのスクリプトと、IdleStateを作る。
DarkKnightのAnimationControllerを作り、割当。EnemyHealthで体力は弄らない。EntityStatusで設定すること。Animatorのツリーがズームしすぎとかで見当たらないときは、AでOK。Animatorを割り当てなおしたときは、SpriteRendererなどのレイヤ設定を行う。xVelocity等のパラメータはfloat。アニメはidle, move, attackの三種で基本よい。Attackのloopだけ切る。battleはブレンドツリーで作る(コピペしてくるとよい)。
スプライトでめちゃめちゃ困った
勝手に画像が縮小される原因は、Max Size部分のせい。これを引き上げる。
アニメーショントリガー。加速したいフレーム, 攻撃したいフレームでAttackトリガーを挟む。終わりにCurrentStateトリガーを挟む。各種multiplierの設定を忘れない(MoveにmoveAnim...,ブレンドツリーのidle/MoveにbattleAnimSpeed割り当てる。
プレイヤーが死んでもstate変化しないのでチェック✅
プレイヤーが遠ざかったらstate変化の使用はもともと取り外したんだった。Prefabで配置するのではなく、ちゃんとスポナーから沸かせたら死亡後の攻撃を止めてくれる。
持続攻撃をつくりたい
持続開始トリガーと、持続終了トリガーを作る。
近距離攻撃と、突進攻撃を作る
まずは、突進攻撃だけの設計をする。アニメが2種できる。stateも2種作る必要がある。
EnemyBattleStateをベースに、darkKnightだけのBattleStateを作る(そこでAIをつくり、どっちの技を打つか決める。)
近距離攻撃の設計
攻撃ごとにダメージ倍率を決めたい。
今、3連攻撃のVelocityは決められているので調整ができるだろう。まずEntityCombatに現在の倍率(デフォルト1.0)を持たせる。それを、アニメindex割当のタイミングで設定すればOK。
? TODO: 空中攻撃・スキル・Bossの攻撃にも割り当てる。
攻撃ごとにノックバックを決めたい
ダメージノックバックはEntityHealthで決定されている(受け手が決めている)。IsHeavyKnockbackは生きているけど、もう使わないかもしれない
攻撃側で設定できて、設定されていない場合は今までの値が考慮されるようにした。Playerはできる。
Playerへの割当の流れ
Playerに、攻撃パラメータを定義する。
PlayerBasicAttackState.Enter()で、ダメージ倍率の設定と、KBの設定を自身のEntityCombatに割り当てる。
EntityHealthがTakeDamageを実行する
KBは、攻撃側のEntityCombatを見て、EntityHealthが決定
ダメージは、攻撃側のEntityCombatが、決定
⭐PlayerBasicAtatckState.Exit()で、倍率をリセットしておくこと。
DemonHoundにも設定してみる
Enemyに、基礎倍率、KB力、KB時間の3つを付与する。
EnemyAttackState.csで、ダメージ倍率とKB設定を割り当てる。
これでOK
DarkKnightにも設定してみる
Prefabを画面に出して、Prefab上で設定していくとよい
DarkKnightに、各種攻撃パラメータを定義する。
EnemyStateのコンストラクタの意味をおさらいしておく
スキルを作ろう
PlayerSkillControllerを作ってPlayerに割り当てる。スキルが使えるかどうか、ボタン入力時に判断させられるようにすればよい。ダッシュは全ての状況からキャンセルして使えるので、PlayerStateにて管理している。そこで、SkillControllerの値を見てできるかどうかを考慮すればよい。
ダッシュアタック専用のTriggerを作る。通常のAttackTriggerを使うと、ダッシュレベルが1であろうと2であろうと判定してしまった(倍率やKBは考慮されるが。)
ダッシュ終了時に攻撃判定を出そうと思ったが、うまくいかなかった。これは、アニメのトリガーにたどり着く前にダッシュの全体Fが短く、Exit()していたから。アニメのフレームを16にして、Loop Timeをなくした。
全体時間、0.25秒
アニメを4コマ、16にするとうまくいった
AnimatonのSample(Sample Rate)は1秒間に何フレーム進むか。
code:txt
1フレームの長さ(秒) = 1 / Samples
アニメ時間(秒) = 総フレーム数 / Samples
code:txt
Sampleが1なら、
1 / 1 = アニメ1枚 1秒
8なら、
1 / 8 = アニメ1枚 0.125秒
15なら、
1 / 15 = アニメ1枚 0.066... 秒 = 実質4Fくらいなので、4 * 15 = 60F(1秒あたり)
===
1F = 1 / 60 = 0.016
今回アニメを16, 4枚のイラストにした。
1 /16 = 0.0625 * 4枚 = 0.25秒ということになる
スキルアンロックの仕組み
code:txt
必要な内容
・スキルUIパネルがある
・スキルUIパネルをマウスオーバーすると、説明が開く
・クリックすると、アンロック
画面にとりあえずスキルアイコンを設置。SkillControllerに、レベルを上げるメソッドを配置。
スキルアイコンをクリックすると、ヘルスバーが入力で動くようになってしまった
SliderのInteractableをオフ。NavigationをNoneにする。
経験値バー, 敵のHealthBarもやっておこう。
スキルをもう1つつくろう
knockbackAttack。
アニメ用意、アニメーターbool割当
knockbackAttackState作成。Enterで技パラメータ割当、Exitでリセット。
地上も空中も使いたい。PlayerStateを親とする。
ボタンはDにしようか。PlayerInputSetで追加。
たまにSetBoolが解除されない
base.Exit()をしてなかった。
スキルコントローラに、Dashと同じように入れていく
Dashと共通化させるためにSOベースで作るのが良いだろう。EnumとしてSkillIdを作る。これはスキルの種類。SOとしてSkillDefinitionを定義。1スキルにつき1つ存在して、この中でスキルに関するパラメータをまとめる。ダッシュLv2はこう、Lv3はこうという感じで。SkillRuntimeStateでは、スキルに用意している現在レベル、クールタイムを取得できるようにする。ゲームからは、PlayerSkillControllerで基本的に弄ることになる(ここにskill詳細のゲッタ等をいれていくことになる。)。
SkillRuntimeState
プレイヤーが今どういう状態なのかを管理する。
⭐実際どんな流れになるのか
var levelData = player.Skill.GetCurrentLevelData(SkillId.Dash);
PlayerSkillControllerで、EnumでスキルIDを指定することでstateを取得する。stateというのは、SkillRuntimeStateのこと。SkillDefinition_DashとかそういうのをSOで作ってPlayerSkillControkkerに割り当てているはず。ここから引数で指定したIDのデータを取る。
https://scrapbox.io/files/69381beb4c935034125b2c3e.png
このstateはSkillRuntimeState.csクラスに属するので、クラスに書いてるCurrentLevelDataが使える。なお、currentLevelは初期0だが、アイコンクリックすると1,2と上がっていったデータが保管される。なので、2ならdefinition.GetLevelData(2)が走る。
SkillDefinition.csにはGetLevelDataがある。指定された配列のlevels[i-1]を取るという感じ。
https://scrapbox.io/files/69381d061d0ee8fb3079f82d.png
SkillButton.csの抽象化
int lv = playerSkill.GetLevel(SkillId.Dash);みたいな部分を親で持つためには、
code:c#
# 親
protected abstract SkillId TargetSkillId { get; }
int lv = playerSkill.GetLevel(TargetSkillId);
# 子
protected override SkillId TargetSkillId => SkillId.Dash;
として、抽象化すれば良い。
クールダウン回復描写
黒いalpha = 0.8くらいのImageをアイコンの上から当てる。それをSkillButton.csから制御できるようにすればよい。
UIのコーディング
開閉の実装
Start()時にPlayerLeve.ApplyLevelStatus()で、ステータスがPlayerに割り当てられる。同じStart()でUI側の文字にステータスを適用statusPanel.Init(entityStatus, playerLevel);すると、実行順序が同じタイミングなのでうまくいかない。PlayerLeve.ApplyLevelStatus()をStartでなくAwake()に移行させた。
スキルにマウスオーバーしたときの挙動をしっかりしたものにする
SkillDefinitionを分解して、詳細な説明文を出せるようにしよう。
パネルの整理
スキルの名前
MasterLevel
SummaryText
区切り線
CurrentLevelHeader
CurrentLevelBody
NextLevelHeader
NextLevelBody
スキルアイコンの共通化
画面上に出すスキルアイコンと、UIで出すスキルは同じものを使いたい。ただ、微妙に用途が異なる。Prefab化したいが
⭐Prefabについての誤解
[SerializeField]で割り当てられない?→PrefabからInstantiateするときはNGだが、Prefabを最初から配置して、ヒエラルキー上に青い線が付いた状態(「Prefab Override」運用)なら、別に割り当てられる。
⭐Prefab Variant
あるPrefabを親とした、別のPrefab。親(Base)に共通部分を持たせて、異なる部分だけをVariantで運用する形になる。
遠距離攻撃を作ろう(敵)
idle/moveの作成
Attackモーション
AttackTriggerは必要がないはず。攻撃終わりにCurrentStateTriggerだけセット。
遠距離攻撃について
既存のEntityCombatは、近距離専用としよう
なのでAttackStateも、RangeAttackStateとして変更する
弾の発射処理
射出アニメに、ShootProjectileTrigger()を付与(このタイミングでSpawn()させる)
EntityにProjectileSpawnerを持たせる
ProjectileをPrefabに割り当て、それを↑に設定。Colliderもつける。
ProjectilePointを敵に持たせて、射出される場所を指定
ダメージ計算処理の共通化
EntityCombatのCalculateDamage()とか回避率計算処理は、弾でも使う。なので共通化の必要がある。
敵の振り向き判定が微妙
今は、BattleStateでDirectionToPlayerが走る
スポーン時に敵を左右どちらかに振り向かせたい
右向き固定なので、スポーン時にランダムでFlipさせるようにした
敵をもう少し賢くしたい
y軸を見て攻撃を辞めてほしい
タイマーを持たせて、自身のy軸の範囲から外れた場合、タイマーを走らせる。それが切れたらidleStateへ戻す。
ガンナーが動かなくなった
whatIsGroundの指定がなかった。
敵を複数体殴ったとき、おんなじコマになる
KBに少しだけばらつきを入れた
段差問題
もう少し賢くしたい。壁を見てジャンプするようにする
wallCheckを足先に置く
カメラ広げる
cinemachine confinerのコライダーを弄ること。
HardLimitもなくす。
プレイヤーの魔法攻撃スキル
clip作成。bool作成。トリガー設定。
PlayerMagicBoltState作成。地上空中どちらでも使いたいので、PlayerStateが親。
EntityProjectileSpawnerをcomponentで持たせる。ProjectilePointをObjectで持たせる。Animator componentのProjectile Spawnerにも割り当てないとダメ。
とりあえず試したいなら、既存の弾を割り当てて撃ってみるとよい(vfxがないので当たるとエラーになる)
スキルアイコン化、パラメータをスキルとして割り当てられるようにする
SkillIDに定義追加、SkillDefinition_MagicBolt追加。
UIInGameに、SkillButtonBase_UIInGameを追加して、このスキル用の割当を設定。UIStatusMenuにも追加。
アイコンクリックするとレベルが上がるようにする
ボタンが押される->OnPointerEnter -> GetLevelの var state = GetState(id);がnullの状態になっている。
PlayerSkillControllerのcomponent側にも、Dataを配置しないと駄目。
スキル取得とクールタイムを有効化する。MagicBolt入力を想定しているplayerStateに、CanUseとOnUseを使うようにする。
code:c#
// MagicBolt
if (input.Player.MagicBolt.WasPerformedThisFrame()
&& player.Skill.CanUse(SkillId.MagicBolt)) // 追加
{
player.Skill.OnUse(SkillId.MagicBolt); // 追加(クールタイム処理)
stateMachine.ChangeState(player.magicBoltState);
return;
}
弾に、スキルSOの持つ威力, KB, KB時間を考慮させる
EntityCombat側はどうなっていたか
EntityCombatがcurrentDamageMultiplier, currentKnockbackPower, currentKnockbackDurationを持つ。例えばダッシュだとDashStateに入った瞬間、SetDamageMultiplier等を呼んでcurrent変数にセット。Exit()するときにリセットしておく。
Projectile側も同じようにする
ノックバックは、EntityHealth.TakeDamage() -> EntityHealth.CalculateKnockback()で計算している。ここが現状EntityCombatのみを考慮した設定になっているので、Switch文などでCombatのdefaultKBをとるか、ProjectileのdefaultKBをとるか選別する。Enumがいいだろうか?
⭐と思ったが、この設計はEntityHealthでどんどん場合分けが増えていくアンチパターンとなる(気づかなかった...)。なので、攻撃データ全てをまとめて、EntityHealthはそれを受け取り、ただ処理していくだけの設計にするのがきれい。
綺麗に設計する
DamageContextという、ダメージ計算に関する構造体を作る
Combat側でも、Projectile側でもその構造体にダメージ, KB等の情報を入れてやる
EntityHealth.TakeDamage(DamageContext ctx)という形で受け取るようにすると、非常にシンプル
MagicBoltStateでスキルダメージなどのセットをするケース
player.EntityProjectile.SetDamageMultiplier(levelData.damageMultiplier);とはできない。PlayerはEntityProjectileを、componentとしてはもっていないから(Spawnerしか持ってない)。これも構造体で解決する
手順
MagicBoltStateでは、スキル情報は取得ができるので、それを構造体とする
スポナーに構造体を渡す
スポナーが弾インスタンスにダメージ、KBをセット
⭐Spawnerへの渡し方
MagicBoltStateで作った構造体を、どうAnimationTriggerで使うか(トリガーでSpawnしてるので)
Playerが弾のコンテキストを保持し、仲介してうけわたしてあげる
⭐Enemy側の通常攻撃ができなくなったので、直そう
Spawn周りを調整する
PlayerProjectileSpawner(Prefab複数所持)と、EnemyProjectileSpawner(まずは1こ)をわけてもいいかもしれない
DemonGunnerにProjectile ctxを持たせて、Awake時に設定するように
DemonGunnerTriggerをつくって、その情報を参照するようにした
弾の不具合調整
倒した敵に弾を当てたらエラー?
code:txt
MissingReferenceException: The object of type 'DemonGunner' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
UnityEngine.Object+MarshalledUnityObject.TryThrowEditorNullExceptionObject (UnityEngine.Object unityObj, System.String parameterName) (at <d35f3fd492a14898b9e7cb0372b7ba3d>:0)
# このへん
var ownerStatus = owner.GetComponent<EntityStatus>();
var targetStatus = target.GetComponent<EntityStatus>();
コード直してるうちに解消した。
攻撃するか、groundに当たっても消えるようにしとく
whatIsGroundで対応。
テレポートの実装と、重複するZキーの処理をなんとかする
テレポートのスキル化
UIをわける
スキル取得ボタンと見せるためだけのボタンを同じものを使っているので、分割しよう。
スキルの動きをちゃんと理解しておく
まず、スキルSOにZ,D,Vスキルスロットを用意した
今回、同じスロットの技は一度取得したら、他は二度と取れないという設計にしたい。
クールタイムが回復したら、光る演出を入れた
物理ビルド Vスキル Ground Slam
anim
ジャンプ、落下、攻撃の3種類
ジャンプと落下はブレンドツリー。boolを持たせる。yVelocity を -1 or +1で切り替え。
state
3つ作って割り当て
攻撃判定の拡大
魔法ビルド Dスキル
近距離、地形, 敵貫通弾。SwordBeam
Spawnerが1種のPrefabしか持てないので、Spawn()の引数にPrefabを渡す形とすれば、Spawnerは弾を生成するだけという責務になり、すっきりする。State -> Playerが弾を決める -> Trigger -> Spawner という受け渡しが必要。
できた!
多段ヒットの考慮
OnTriggerEnter2Dが、侵入した瞬間にしか呼ばれないので、重なり続けても発火しない。なのでこれでOK
スキルポイントの流れ
まず全体のスキルポイントは、EXPやレベルを持たせているPlayerLevelで持つことにする。このPlayerLevelで、LevelUp()時にスキルポイントを増やす処理と、スキルポイントを減らす処理を書いておく(SkillButtonをclickしたときに持ってくる)。この2つの処理にはOnSkillPointsChanged?.Invoke()を設定して、増減したときに走らせるイベントを購読させる。
SkillButton側でplayerLevel.OnSkillPointsChanged += HandleSpChanged;で、SP増減イベントの購読。。click時にSPを減らす。このとき、同時にHandleSpChangedがイベントで走る。(SPが足りなくなったり、同じスロットならグレーImageを被せる。)
SkillPanel側は、まずPlayerLevelが親のUIStatusMenuからもらえるので、
レベルアップVFX
ダメージを出す
パッシブスキル
スキルLvUp時に、Statusを調整する考え方
スキルクリック時に、ステータス更新
DemonBatの作成
弾丸の不具合
その1
https://scrapbox.io/files/693d314c0cd5e3d78f78c332.png
敵の弾に敵を倒してからあたるとエラーになるっぽい
code:txt
var ownerStatus = owner.GetComponent<EntityStatus>();
var targetStatus = target.GetComponent<EntityStatus>();
Fire時に弾の変数として持たせるようにした。
その2
ownerVfxを取っているが、これはAwake時点でどこかから取る1ほうがいい気もする。spawnerから渡すとか。
これも直した。
その3
弾で攻撃したとき、こっちを振り向かない。EnemyBattleState
攻撃対象がMagicBoltProjectileとなっていたので、弾は消えるので向き先が不明だった。弾のTransformではなく、ownerのtransformを持たせることで、弾で殴ったときにPlayerを向かせるようにした。
レベルが上がったと同時にボスウェーブになったら、warningが出ないときがある
レベルアップでTimeScaleを止めていたから起こったことなので、多分発生しない
タイトル画面を作る!
常駐するファイルを決める。今回はGameManagerだけ。
レイアウト: クリックすると、詳細なUIが出る。モーダルみたいにしよう。Dimmerにはターゲットを吸わせたいので、Raycast TargetをONにしておくとよい。
キーボード操作!
ボタンのNavigationをExplicitにする
上を押したら、下に押したらどのobjectのボタンに飛ぶのかを指定できる。
InputActionsに、Playerと、新しいアクションマップUIを用意しよう
操作の命名には慣例があるので、それに従って書く
EventSystemのActions Assetを割り当て。PlayerInputSet.
? BattleEasy側では特に何もやってないので、メニュー画面を操作させたいときはこれを忘れないこと。
EventSystemに割り当てる。(慣例通りに書いておくと、ここで役立つ)
タイトル表示時に、最初の選択肢をセットしておく
ボタンを見やすく
Highlightedはマウスホバー、Selected Colorはキーボード選択で指定できる。
選択音を鳴らす
AudioManagerの用意。シーンをまたぐ想定で。
AudioManagerに、bgm, se, sfx用の3つのAudio Source componentsを持たせる。
ボタンに付与したscriptの、セレクトされたときのメソッドにSEを挟めばよい
セレクト音はセレクト用スクリプトが出す責務
決定音は、そのボタンに割り当てているメソッド側がならしてやればよい
鳴らす
UITitleMenuのSerializeFieldに鳴らしたい音を持たせる。
AudioManagerがシーンにいることの確認
AudioManagerにもたせた3つのcomponentをちゃんと割り当てる。
初期選択について
ゲーム開始時はPlay, 難易度選択時はTutorial, quit時は「やっぱりつづける」を選択させる。これらはUITitleMenuのSerializeFieldで設定できる。
ボタンのNavigationをExplicitとして、上と下に押したらどうするかを指定。ボタン自体のスクリプトにもフレームと文字への割当が必要。
ESCキーを有効化する
タイトル画面全体で受け取るようにする。
EventSystemとNewInputSystemについて
タイトル画面のブラッシュアップ
PRESS ENTER KEYをだす
ロード画面
そもそも、ロード画面とはどういうものなのだろう?
画面呼び出し中はフレーム更新がとまって、何も起きないように見える時間がある。それを見た目として隠すもの。ロードシーンを作っても良いし、簡易的なオーバーレイとして作っても良い。
今回はオーバーレイでつくってみる
todo: 画面の流れ見ておく
? どうやって同じスロットの技は複数覚えられない、とユーザーに伝えられるだろう?
チュートリアルステージとかを作るべきだろう。
easy, normal, hardをつくるならついでに作れるはず。
チュートリアル
文字で操作説明をいれていく。これはオブジェクトとして配置しよう。TextMeshPro3Dを使うのが良い。
スキル振りの説明は...
レベルが上がるとスキルポイントがもらえます。ESCでひらきましょう
ESCとかの文字を浮かべて、点滅させる
→パネルに説明が出るのが理想だが。
Descriptionパネルが空なので、ここに入れたい。
ふきだしで、パッシブスキルとアクティブスキル(スロット)の説明を出す
このシーンだけ特別扱いする
GameManager以外で管理する。そもそも使わないようにする。
PlayerとCanvasをPrefabで共通化して、使いまわせるようにした
Cinemachineのkeep警告
シーンをまたぐと、その次のシーンのステータスに更新されてしまうので警告が出る。これを防ぐには、シーンにはシーンごとのカメラを持たせておけばよい。もしくは面倒なら、Save During Playを切っておけばよい。
UIをキーボード操作する
タイトルで用意したAction Mapをそのまま使う。ESCが押されたら、ActionMapをUIに変更。そこでESCが押されたら、またActionMapを変更してPlayerの操作に戻す必要がある。
BattleシーンのEventSystemのInput System UI Input Moduleを確認。そこに今回作っているAction MapのPlayerInputSetを割り当て。Move:UI/Navigate, Submit:UI/Submit, Cancel:UI/Cancel としておくと、何かしらSelectableとなっているケースの時、この入力が適用されるようになる。
改修。メニューを開く部分に改修。メニュー遷移させたとき、最初にselectableとしたい要素を持たせる。selectされたボタンに対して処理できるインターフェースがあるのでそれを付与して、マウスオーバーと同じ処理を書いて、関数としてまとめて与えてやればよい
ツールチップが有効なときと、そうでないときで挙動を変える必要がある
ふわっと消えた時
フラグを消すのと、選択もどっか行っちゃう
不具合修正
Tutorial -> Easyに遷移してから、レベルを上げるとエラーになることがある
GameManagerの情報キャッシュタイミングを調整
クリア / やられたときの画面も、キー操作に対応させる
Action Mapを変更して
最初に選択されるボタンを指定して
ボタンがセレクトされていたら黄色くする
ゲーム性を上げる
敵を倒す、時間内生き残る、ボスを倒す。これ以外にどういった実現ができるだろう?
防衛はやっぱり良さそうだ。実装することにする。
RigidBody 2D, Colliderの割当。
体力設定。IDamagableの実装。
まず、こわれたらおわりというところまで作る
IDamagableをつけて、whatIsTargetにこのObjectiveレイヤを対象として追加。
壊れた時にスローモーションイベントを実行。
敵のAIを刷新する
折角今あるんだから、タイプ別に分けたいな。
うろうろタイプ。
Objectに向かって走るタイプ。
Prefabの管理。敵のRoleが増えたので、これまでのPrefabをどう管理していくのが良いだろうか。Prefab Variantで分けるべきか?
→これは、Wave SOで設定して、Spawnするときに割り当てるのが良いだろう。Prefab Variantは、見た目やアニメ、当たり判定が別物になってから使い分けるのがよい。
Waveで管理できるようにした。
Objectiveのブラッシュアップ
アニメ。壊れた後画面を揺らす。
HPバーの実装
画面に固定しよう。Canvasで作る。
バグ修正
経験値取得時、ログが出る
ログ部分コメントアウトしてからもう1回走らせると出なくなるが...
raiderがplayer検知→ 一定時間たつとidleに戻るので、raiderはbattleStateになるように戻したほうが自然かも。(一度見ただけなので、違う挙動かもしれない)
治ってた
AIを調整したので、ダークナイトのoverride挙動が問題ないか確認する
大丈夫そうだけど、壁に引っかかることがある。frictionNoMaterialをつければいい感じだけど、すべってしまうのでcs側で修正が必要だと思う
アイテムかな。
Chestをスポーンさせて、定期的にアイテムがとれるようにするのがよさげ
code:設計
Chest.cs
・ChestがTakeDamage()
・アニメが走る。
・アニメの開くタイミングに合わせて、ItemをSpawn(トリガーなど?)
・色々なChestを用意して、種類に合わせて出るアイテムを変えるとかでもいいけど後で良さそう。
アイテム
・Itemという基底クラスを作るのが良さそう
・Playerレイヤが触れると、体力回復とか、攻撃UPとか
・チェストからスポーンしたとき、ぽよんと跳ねる。その後、ゆらゆら上下に揺れて存在感を出すのが良さそう
まず、Playerのバフを管理するComponentを用意
Itemの管理
基底クラスを作って管理?
簡易的なアイテムなら、SOで管理したほうがいいらしい。そうする。
アイテムを用意する
チェストからアイテムをスポーンさせる
ぴょんとだして、地面に落ちる感じが楽かも。
アイテムがisTriggerなので地面を貫通するのが大変なので、浮遊させよう。
チェストをスポーンさせる
と思ったけど、そもそもこの設計はどうだろう。元から置くというのはありだろうか?
固定配置 + Waveが終わったとき、チェストが落ちてくるようにするのはよさそう。両方のいいとこどり。リスクを割いてステージ探索でアイテム, Wave終わりに救済でアイテム。
色々Fix
壊されたらExhaustedじゃなくて、別の記載にしよう ✅
Objectiveの体力がマイナスに✅
Objectiveが消えた後も殴られ続けてしまう
co.enableをfalseとするのがいいが、地面から落ちてしまう
地面設置用のColliderを作って、そっちだけずっと有効にしていればよい。
ゲームオーバー画面でぼこぼこにされるのが可愛そうなので、とめてやりたい✅
似た感じで実装できそう。Enemyの検知判定を、GameManagerのステータスに依存してやる。
弾が当たったときのバグ
https://scrapbox.io/files/694698c9aa397c2a71ae7995.png
ownerがdestroyedされた弾に当たったとき。Playerが当たってもバグる。
アイテムのレイヤーについて調べる
Defaultにして、ItemにしたらPlayerがさわれなくなった。レイヤーのソートはItemが一番上だが、それの影響があるんだろうか?
違った。Layer Colision Matrixで、レイヤー同士の衝突が無効になっているからだった。
https://scrapbox.io/files/6947938abd48e5799e0d3c6c.png
code:txt
# OnTriggerEnter2Dの条件
Collider2D 同士が重なる
Layer Collision Matrix で、そのレイヤー同士の衝突が有効
どちらか片方(または両方)に Rigidbody2D が付いている
片方の Collider2D が Is Trigger = ON
レイヤーに振られた番号は、単純なIDなので関係なし。
parallaxな背景を作る
Sorting Layerで区分けをする。大きさは、Sprite Rendererの要素だけで調整するのが良い。(Rootでは弄らない。)
横512, x軸が+-40とすればきれいになる。 camera
カメラ
Fixed Updateにして、Chinemachine Bran側のUpdate Methodもそれに合わせる。
Player側のRigidbody2D. InterpolateをNoneにする。
Objectiveが急に真っ暗になっちゃった
Sprite Rendererの Materialを、Sprite-Unlit-Defaultに変更して、治ったらライト関連の原因。
元からHierarchyに存在する、Global Light 2Dを調整する
ここにチェックがないと、暗くなる。
テレポートの不具合を直す
Parallaxな背景と相性が悪い...と思ったが、違うっぽい
早すぎると、カメラと背景ががくつく
カメラとBackgroundの動きを、LateUpdateであわせた
Playerのrigidbody InterpolateをInterpolateにする
さらにParallaxBackgroundの実行順序を、遅めにすることで
https://scrapbox.io/files/6947e399972bc34c40e1e537.png
カメラがうごく
うごいたあと、背景も動く
となるようにできた。
ステージの作りこみ、バランス調整(時間が余りそうなら敵も作る)、BGM / SE
平均時間は8-10分を目安にしたい
レベル感覚
スキルが8こ * 5Lvずつ分ける。つまり、MAX 41Lv
イージーがLv15,, ノーマルLv20, ハードが35とか目安。
ステージ乗り継ぎのほうがいい気も..............
するけどまあ....
いや、今回はeasy, normal hardで分けよう。
敵に、Waveごとの強化を適用する
体力と、EXPも補正をかけたい(MaxHPは増えているが)
回復アイテム
多分回復用グリーンチェストを用意して、wave明けにドロップさせるのがいい
ほかは固定チェスト, 時々他もスポーンという感じ。
定期的に落ちてくるのは回復アイテム。不定期なのは、ステータスアイテムとかにできるといい
WaveManagerを調整して、回復アイテムのスポーンと不定期アイテムのスポーンをわけれると良さそう
EnemySpawnPoint
WandererとRaiderポイントをわけてもいいな。経験値稼ぎにいったりできるようになる。
? enemyCountをチェックするなら、debug.logで流してみるとよい。
Easyを実装してみよう
経験値テーブルは増分式でやるのが楽そう。とりあえず1.2倍。
れべるがサクサク上がるので、hp max回復はやめよう。
安心して出歩けないな!
ミニマップがあればうれしい
単純なものでもいいので、あるとうれしい
ステージ全体をつつむ判定、StageBounds
SurviveTime
沸き終わってから、この時間だけ経てば大丈夫になる
スキルの解放条件
PlayerLevelを付けるとぐっといい感じになりそうな気がするのだが...Lv4以上はLv20以上とか。
体力が少数になる。切り捨てした
経験値が変
expが20だが
Unity再起動で治った。謎。
流れ
wave 1-3 のんびり。
wave 4-5 たくさんraiderが。
wave 6-8 のんびり。
wave9 大詰め?
wave10 ボス
ためした
Lv11時点でexp626
10-15分くらい
ひまだった
ボスアラートなかった(忘れてた。)
画面にWaveを出す
再度試した
10分くらい。EASYはもっとゆるくていいかも。
そもそも、EASYは必要なのかという疑問
NORMALとHARDでよくないかという(敵も少ないし)
そもそも、Playerのレベルアップで能力向上は必要か?
パッシブで頑張ればよくないか
skillのUIに、Required Player Levelはいると思うのでつけた
チェストが動かせたほうが面白いだろうな、と思う
問題があるのはground slamだけ。直した
ついでに、飛びすぎてるので直すか。
直した。改めてNormalとして、仕組みを決める。
5-7分。8 waveにしてみる。1wave の時間を決めるのが楽かな。
30 * 8 = 240 = 4分なので、30は短い。 40 * 8 = 320 = 6分強 。45秒くらいにする
30 46 30
7分20秒。結構楽しいかも
レベル12: 63EXPだった(画面にはまだ結構残ってる)
ボスが結構急に出たので、ちょっと待たせていい
もっと直す
魔法のクリティカルVFXがダメなので直した
敵にKB耐性を持たせたい
いい感じ
ウェーブ終わりの回復
やっぱりボスと戦っているときついと思うので、リジェネがあるといい。
Playerのレベルテーブルを、ステータスが上がるのはほんとちょっとだけにする
序盤は初心者に優しくするために、これくらいあげてもいい。Lv15-くらいからは、控えめにする
雑魚をもうちょっと増やしたい(全然終わらない)
スライム
耐久が高い。ひじょうにふっとびづらい
なかなかの攻撃力
iron sentinel
めっちゃふっとばしてくる(つよい)
ダッシュ
出だしにxVeloと、StartContiのトリガー
おわりぎわにendConti と、xVeloZero
さいごにcurrentState
melee
おなじ。
てき3
超素早い, 硬い, 低HP
スキル
パッシブも取得できないとき暗くする。
UI側のクールタイム描写いらない。
バランス調整しよう
skill
クールタイム長いとつまんない
弱くてもいっぱい使えるほうがいい
パッシブ
HPは+20くらいしても全然いい
防御は+3とかでもいいと思う
ATKはいい感じ
MenuUIのLevelを黄色く書いてもいいと思う
PROTECTIONの強化
攻撃+1, 防御+1とか複数持たせる
force
lv3: 10
lv4: 15
lv5: 25
背景を装飾。
GameManagerのPlayerがnullになる挙動を改善したい
いつなっているんだろう?
とりあえず1面終わったらSEもつけてみよう
結構いい感じかも。
必要なse
斬撃、被弾、やられ音、アイテム音、レベルアップ、警告、クリア音
skill音
dash, teleport, 弾発射音, physic用の鈍い音
必要なBGM
3つ(チュートリアルはタイトルでもいいか)
BGMをどう管理するか。
各種シーンに、SceneBgmというオブジェクトを置く。ここにBGMを持たせて、Start()で流せばいいだろう。
SE
イベントハンドラで鳴らすのがいいだろうか?
色々
SEがなるのが遅い
wavファイルがいいらしい...
単純にmp3だったり、遅いファイルのケースがあるので自前で修正が必要
アイテム
ItemPickupにstaticイベントを作って購読する
todo
ボスウェーブ時の警告音
uibossalert
ボス討伐時
倒したときもスローモーション
倒したときは、Wavemanager.OnStageClearedが責務。
フラッシュと、やっつけ音(しゅぴーんみたいな、フラッシュもいれたい)
skill音
dash, teleport, physic用の鈍い音
スキルレベルを上げるときの音
ハード実装
チュートリアル改修
ハード作る
最初のレベリングはとにかくサクサクにして、リピート性をあげられるようにがんばろう
expをたくさんもらっても、その分のレベルになるようなシステムを作る
もうできてそう。要確認だけど。
ハードだったらツールチップを最初から消しておいてよい。
Livelyを振り分けた時にHPUIを更新する
wave: 45 - 50 秒 * 10セット。10分弱で。
単体でいくつか出すから、Waveは多少分かれても構わない。ただし時間は長くならないように。
wandererスポナーポイントをたくさん用意する
似た場所への配置でも、運よくプレイヤーの近くにexpスライムが沸く可能性を調整できる
持続攻撃のバグ?
それでやられた後、攻撃が持続することがある(endがよばれてない)
持続攻撃の仕組み
isContinuousAttackingを、OnDiedでfalseにしたらいい
DarkKnight
格落ちさせる(よわい)。持続攻撃がバグってたので直した。複数出現すると結構厄介かも
持続攻撃がバグがある
melee, idle, battleのEnter時に持続攻撃フラグを切るようにすれば良さそう
Objective
こわれたときに、ずがんと鳴らす
ハード調整
wave7, 8間の経験値上昇が多いので減らす(あげわすれる)
6,7,8がいきつくひまがないので、すこしひまなwave(かいふくあり)を挟む
今度は少し暇になって、回復がふえてしまったのでへらす
小さいスライムの出る感覚を徐々に増えてく感じにする
critical直す
evasionもちょっと強いかも(defより強いかも)
debugStage: wave 7,8がひま
wave10, 11が暇。
demonbatが左上に引っかかる(wandererかな)
ジャンプせずにとまってる(raiderも)
右でスポーンしたdemonBatも、スポーンしたとき右土台に引っかかる。
レベルバランスは結構いい感じかも。
調整
左上の段差はwallCheckが反応しない
右の段差は、wallcheckさせないと下の通路をずっと通り続けることになる
SecondWallCheckを用意した。
6.7がちょっと時間空きすぎ
最後ちょっと変にきついかな
13,14あたりがしずかなのに、15が急にやばい感じ
物理スキルがちょっとよわめかも
吹っ飛ばすのを売りにする
もうちょっとだけリジェネしてあげよう
最大体力の3%とか
ハード結構いい感じ
ノーマル
長いだけであんまりおもしろくないかも
サクサク倒せる雑魚たくさん
ほとんどのユーザーはノーマルしかやらないんだから、こっちを楽しませるべきである
強敵が出る
wave9時点でlv11, 5分くらい。darkKnightを出すか。
ステージ縮める
とかする
修正
合計8分くらい。良い感じ
まあボスはあれくらいの強さでもいい気はする。適当にやったら負けるくらい。
チュートリアル
敵が、GameManagerの管理外なので感知しない
別にこれでも良い感じ
と思ったけど、LevelUpとかも感知できず、VFXが出ない
修正
チュートリアルのパネルUIだけZの決定がきかない!
動いてると思ってたが、UnityデフォルトのActions Assetを割り当てていた
https://scrapbox.io/files/694f9c0078e1e2bbccc79b5e.png
DefaultInputActionsではなく、ちゃんと自前で実装したものを割り当てる。
通常攻撃を、単体対象とかにしたほうがスキルが活きそう。
⭐これは威力を下げて、KBも下げれば差別化できる。
何もしなくてOK
BattleHardのエラー
code:txt
Scene 'BattleHard' 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.
To add a scene to the active build profile or shared scene list use the menu File->Build Profiles
UnityEngine.SceneManagement.SceneManager:LoadSceneAsync (string)
UITitleMenu/<LoadBattleAsync>d__51:MoveNext () (at Assets/Scripts/UI/TitleMenu/UITitleMenu.cs:350)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
ここに入れないと駄目
https://scrapbox.io/files/694fbbf4c513456ec4df9814.png
タイトルの選択肢の時、1度目SE出ない
修正
敵の弾を攻撃で消せるようにする?
やめておいてもいいか。
魔法でチェストが開かないのは、さすがに不自然
whatIsTargetに追加するだけでできた。
ビルドしてみてもいい(試しに。)
クオリティが低い
フレームが少なかったり、効果音がしょっぱい
これでデフォルトをウルトラにしてみたが、変わらない
Compression FormatをGZip
Decompression Fallbackをオフにしてみた。(ちょっとぬるぬるになった。)
と思ったけどそうでもなかった。UltraでもHighでもそこまで変わらない。
LevelUpのVFXがチュートリアルで出ない
フレームが落ちて見えない?
そうでもないように見える...他のシーンではWebGLでも出る。
チュートリアルだけ変な構成にしたのが悪さをしていそうだ
GameManagerのAddExp()の不具合
ちゃんとロードしてやれば大丈夫そうなのだが。後回す。
Awake時点でPlayerをキャッシュしてないから出てるっぽい。なので、ちゃんとシーンがあれば大丈夫そう。
と思っているのだが...
たまにスキル出ない
被弾が悪さしていそうだが。後回す...
残タスク
WebGLでテストプレイ
skillは出るか。exp大丈夫か。大丈夫なら仕上げても良いかも。
操作自体は問題ないな。
GameManager
webgl側のlevelup表示を、missVfxにかえてみる
変わらずでない
カメラを変えてみた
変わらない...
画面にログを出す
https://scrapbox.io/files/6950dc1a861d1c2b6c0d00dd.png
WebGLでは出なかったので、参照できていないということ
code:c#
player = FindFirstObjectByType<Player>(FindObjectsInactive.Include);
// ...
player.Level.OnLevelUp -= HandleLevelUp;
いろいろ言ってもらったので、直そう
フォント
code:c#
// ★ player.Level に依存しない
level = player.GetComponent<PlayerLevel>();
vfx = player.GetComponent<PlayerVFX>();
このような形でちゃんととって、それでイベント購読してしまおう。
スキルが出ない不具合について
移動中、停止 + スキルが同フレーム中に行われると再現する。
挙動について
Update()で毎フレーム、MoveState中にx軸の移動がチェックされている
SkillDを押すと、ChangeStateが呼ばれて、ステートが変わる
ただ、Move側のUpdateは中断されないので、処理が続行し、停止したとき、こちらでもChangeStateが呼ばれる
KBStateに入った途端、Move側のIdleが呼ばれる
対策
code:c#
if (stateMachine.currentState != this)
return;
おまけ:再現
code:c#
# フレームを落として、同時発生を起こしやすくしたり
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 15;
# 意図的に起こす関数をつくって、それをキーにわりあてたりするとよい
public void Debug_Repro_MoveStopAndSkillSameFrame()
{
// 1) moveInput を0相当にする(あなたの実装に合わせて)
moveInput = Vector2.zero;
// 2) そのフレームでスキル遷移を直接呼ぶ(入力経由を避ける)
var id = Skill.GetEquipped(SkillSlot.D);
if (id == SkillId.HeavyKB && Skill.CanUse(id))
{
Skill.OnUse(id);
stateMachine.ChangeState(knockbackAttackState, "DEBUG repro same-frame stop+skill");
}
}
色々指摘してもらったので、直してしまおう
フォント
多分OK
Player, Objectiveをミニマップへの割当が必要。
itemSFX
斬撃音
SFXを全体的に下げる
音量下げたけど、同フレームで鳴らしたら少し音が変わるのかな。
敵がやられたときの音がでかすぎ
EnemyHealthに購読しているので、それが影響していた。敵が3体いたら3体からなってる。
staticイベントに音を紐づけるのはやめよう!
タイトルに戻る画面を作ろう
========================================
反省
ステ振りがつかれる
シューティングはよかった。受け身でステ振りできたから。
チュートリアルを先に作らない
GameManagerを、チュートリアルを考慮した設計にすべきだった
タイトル、チュートリアルを跨いで運用するような想定で作るべき。
最初の段階で、objectiveがないと単調なゲームになることは気づいたほうがよかった
スコアを稼ぐという方針だったが、スコアというのは今どき流行らなさそう。
動画
考慮した設計
StateMachine
ScriptableObjectによるステータス、スキル、敵スポーン管理