【Unity】最強のゲームクリエイターに俺はなる | Interactiveな水の実装(2D)
はいーーーー「ゴジラ-1.0」をもう一回観てきたFORM_07でーーーす
っぱIMAXレーザーなんよ。
前回↓は近況報告でしたが、、、
【Unity】最強のゲームクリエイターに俺はなる | 近況報告 - FORM_07’s blog
予告通り今回は2Dゲームにおける水面(波打ち)の実装についてやっていきましょう!
今回実装していくのはこんなかんじの水です。

有料アセットと比べても遜色ない挙動だと思います。
それではやっていきましょう!
準備(オブジェクト設定)
Sprite Shape生成
まず、水の実装をSprite Shapeという機能を使って行っていくので軽く説明します。
Sprite Shapeというのは簡単に言うと変形可能なSpriteのことです。
水面が波打つ際の形状変化を表現するためにSprite Shapeを用いていきます。
とりあえず「Hierarchy上で右クリック」→「2D object」→「Sprite Shape」→「Closed Shape」で、オブジェクトを生成しましょう。

すると、、、

↑こんな見た目のオブジェクトが生成されて、Inspectorを確認するとSprite Shape RendererとSprite Shape Controllerというコンポーネントがアタッチされているのがわかると思います。
Element設定
次にSprite Shape ControllerコンポーネントのEdit Splineを選択します(SceneビューのToolsからも選択できます)。
そうするとSprite内にElement(丸いやつ)が4つ表示されるので、どれか1つ選択してElement Inspectorを表示させてください。
0~3番目のElementのPositionをそれぞれ(x, y) = (-1, -1), (-1, 0), (1, 0), (1, -1)に設定します。
(Sprite Shapeの仕様で、左下のElementから時計回りに0からIndex(番号)が振られています)
Tangent Mode、HeightはすべてのElement共通でそれぞれLinear、0.1に調整します。

ここまでの操作で↑こんなかんじになったらOKです。
C#でInteractiveな水面を実装する
さて、ここからはプログラミングの力でSprite ShapeをInteractiveにしていきましょう!
コード
using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.U2D; public class Water : MonoBehaviour { private SpriteShapeController sc; private Spline spline; private float scale; private Vector2 basePointPos; private int n; private float[] vec; [SerializeField] private float interval = 0.45f; [SerializeField] private float maxWave = 0.5; [SerializeField] private float minWave = 0.05f; [SerializeField] private float k = 0.09f; [SerializeField] private float waterPow = 0.05f; [SerializeField] private float waveRate = 0.65f; [SerializeField] private float waveTime = 0.08f; private void Start() { sc = GetComponent<SpriteShapeController>(); spline = sc.spline; basePointPos = spline.GetPosition(1); scale = transform.localScale.x; n = (int)(-basePointPos.x * scale * 2 / interval) + 1; vec = new float[n-2]; for (int i = 0; i < n-2; i++) vec[i] = 0; Vector3 p = basePointPos; for (int i = 2; i < n; i++) { p += interval * Vector3.right / scale; spline.InsertPointAt(i, p); spline.SetTangentMode(i, ShapeTangentMode.Continuous); spline.SetLeftTangent(i, 0.1f * interval * Vector3.left / scale); spline.SetRightTangent(i, 0.1f * interval * Vector3.right / scale); spline.SetHeight(i, 0.1f); } } private void FixedUpdate() { for (int i = 2; i < n; i++) { Vector3 pos = spline.GetPosition(i); vec[i-2] -= k * pos.y; vec[i-2] *= 0.9f; pos += vec[i-2] * Vector3.up; spline.SetPosition(i, pos); } } private async void WaveCreate(Collider2D collision) { int i = (int)((collision.transform.position.x - transform.position.x - basePointPos.x * scale) / interval) + 1; if (i < 2) i = 2; else if (i > n - 1) i = n - 1; float y_collisionVelocity = collision.GetComponent<Rigidbody2D>().velocity.y; Vector3 pos = spline.GetPosition(i); Vector3 dv = pos + waterPow * y_collisionVelocity * Vector3.up; if (dv.y > maxWave) dv = new(pos.x, maxWave, 0); else if (dv.y < -maxWave) dv = new(pos.x, -maxWave, 0); spline.SetPosition(i, dv); int l = i; int r = i; float y = dv.y; if (y * y != maxWave * maxWave) y *= waveRate * Random.Range(0.8f, 1.2f); while (true) { if (y * y < minWave * minWave) return; l--; if (l > 1) { pos = spline.GetPosition(l); spline.SetPosition(l, new(pos.x, y, 0)); } r++; if (r < n) { pos = spline.GetPosition(r); spline.SetPosition(r, new(pos.x, y, 0)); } if (l <= 1 && r >= n) return; await UniTask.Delay((int)(waveTime * 1000)); y *= waveRate * Random.Range(0.8f, 1.2f); } } private void OnTriggerEnter2D(Collider2D collision) { if (!collision.GetComponent<Rigidbody2D>()) return; WaveCreate(collision); } private void OnTriggerExit2D(Collider2D collision) { if (!collision.GetComponent<Rigidbody2D>()) return; WaveCreate(collision); } }
見ての通りとても短いです。
このコードをアタッチしたら完成!
はい!お疲れ様でした!
なにかと敬遠しがちな波打つ水面の実装ですが、こんなに簡単にアセットなしでできちゃいました。
これでゲームのクオリティを一段階アップできるね!やった!
次回からはゲーム開発の続きをしていきたいと思います!
【Unity】最強のゲームクリエイターに俺はなる | 近況報告
どもー。「君たちはどう生きるか」を劇場で観れてうれしいFROM_07ですよ~。
実に半年ぶりの更新ですか。。。(._.)
ブログの更新サボってただけでゲーム制作は続けていたのでご安心ください(🙄)
前回↓まで一生懸命ゲームを作ってきたわけですが、、、
【Unity】最強のゲームクリエイターに俺はなる | 回避実装!! - FORM_07’s blog
えー、、、全部作り直してます
はい、1から。
というのも、Unityを学んでいく内に自分のやり方が間違いだらけで拡張性のないものだったことに気づいたので、、、
ぶっ壊したくなった!😁
だからぶっ壊してみた!!😁😁😁
コードもぐちゃぐちゃだったしね、、、(=_=)
といことで今回は進捗報告。

見ての通りなにもかも変わりました。(サフィアちゃんの見た目以外)
地面のデザインは変更します(つるつるしてるのと、1ブロックが大きすぎた)
変更点を挙げていくと本当にきりが無いのですが大きく変わったところとしては、ゲームの不変データの管理をScriptableObjectを使って行っている点があります。

↑こんなかんじ。(これは攻撃のパラメータ)
ScriptableObjectを使って攻撃や敵、アイテム、会話などを管理してます。
ScriptableObjectについては今回は割愛。後々説明します。
あとはステート駆動をStateMachineBehaviourを使って実装してる点とか。
この実装方法はオブジェクト指向には反していて、扱いにくい側面もあるんだけど、、、個人開発だし他のしっくりくる方法も思いつかないしいっかってことで。
StateMachineBehaviourについても今回は割愛させてくんさい。
Cinemachineなんかを導入してみたり。
Cinemachineについては割(ry

↑わかりにくいけど攻撃があたったときに画面を振動させてます。
そして個人的に見てほしいのはこれ!

めっちゃ水!
アセットなしかつまぁまぁ実用的な方法で実装できたので次回あたり解説したいですね。
コードの量も少ないのでお楽しみに。

↑NPCの近くで会話できるようになったし、、、

↑TimeLineでムービーみたいなこともできるようになった。
あとはやっぱり、コードの可読性とか拡張性とか諸々が改善されたのがでかいかな。
見えない変化だけど超大事。
はい!
今回はここまで!
まだまだ伝えきれてないですが、、、、、まぁいいでしょう!😁
次回は早速水の実装について話そうかなぁ~。
【Unity】最強のゲームクリエイターに俺はなる | 回避実装!!
どもです!「崩壊スターレイル」で飲月と白露を2枚抜きしてうれしいFORM_07っすうぅぅぅい!!
白露持ってなかったからうれしい。
前回↓は敵をちょっと動かして倒せるようにしましたね!
【Unity】最強のゲームクリエイターに俺はなる | Root Motionで敵を動かそう - FORM_07’s blog
今回は敵に攻撃させて、さらにサフィアちゃんの回避、そしてできたらパリィまで実装しちゃいたいと思います!
まずは敵の攻撃から。
とりあえずPlayerの位置を取得させてみるか。
Playerと敵がある程度近くなったときに敵を反応させたいから、、、、
距離を出さないとナ。。。
距離の比較にはSqrMagnitude()を使います。
こいつはベクトルの長さを2乗した値を返してくれます。
これを使ってPlayerと敵の距離を判定していくぅう!

これでよし。
・・・・で、Playerとの距離が近いときに何秒かおきに攻撃してほしいから、、、

「何秒かおきに」の実装はこれでよさそうだな。
あとは攻撃させればいいわけだけど・・・

これがぴゅーーってなってグサッってなればいいか。


これでPlayerめがけて体当たりしてくれるはずだ。

あれぇーーーー・・・・・・(゚◇゚)
・・・・・おかしいな。
・・・・・・ッスゥーーーーー・・・・・・(-.-;)
ん!?
・・・なんかAnimatorコンポーネントをオフにして実行したらちゃんとPlayerに向かって体当たりしてきた。
なにこれ・・・( ・o・)
しかも試しにanimator.speedを0にしてアニメーションを停止させてみても駄目だったし、、、
理由がわかんないからコンポーネントを無効にするしかないか。
なんかモヤモヤするなぁ・・・(=_=)
まぁいいや。
・・・ってまてよ。Animatorコンポーネントオフにしたらアニメーションしないじゃん。(当たり前だろ)
やーーめた。
敵の挙動はスクリプトでどうにかするか。。。


結局ステート駆動システムで実装することにした。
特に特殊なことはしてないね。

やった~~~~!!
動いたし安定してる。
挙動の調整は必要だな。。。(._.)
かなり手こずったがここからさらにサフィアちゃんの回避を実装する。
回避の概念を実装するにはまずダメージを受けるようにしないとな。。。
ってなわけで攻撃用の当たり判定をつくって、、、

攻撃判定のコードを変えて、、、

PlayerとEnemyそれぞれの被攻撃時の処理を用意して、、、


攻撃時にスプライトの色を赤くする処理を付けてあげたら、、、


っしゃぁぁい!ダメージが実装できたぞ~~~~~!!
間髪入れずに回避を実装していく。
まずはInputSystemをいじくりまわす。

で、コード。


これでよし。

あとは当たり判定をいじくって回避できるようにしてやればいい。
要は無敵時間を作る。

ちょっと実装がスマートじゃないしコードが汚いな。。。
まぁひとまずよしとしよう。

こんなかんじで動いてくれました。
若干ゲームっぽくなってきたな。
よ~~~~~~し今回はここまでぇぇぇぇぇい!
パリィはまたの機会に断念します。
かなり手こずったな。。。
この前立てた予定も一週間遅れる羽目に、、、
(スケジュール通りにいくとは端から思ってなかったけども・・・・(^x^))
次回からはマップに本格的に取りかかっていきたいと思います!!
【Unity】最強のゲームクリエイターに俺はなる | Root Motionで敵を動かそう
どもーーー最近ゲーム制作の進みが悪いFORM_07ですよ~~~。
スプライトを描くのがまだ慣れない。。。
だがやれるこおはただ1つ、そう、ウダウダ考える前に手を動かすことだ。
さて前回↓はサフィアちゃんがコンボを打てるようになったんでしたね!
【Unity】最強のゲームクリエイターに俺はなる | コンボ実装! - FORM_07’s blog
今回は敵を作っていきます!!
というわけでスプライトを1枚だけ用意しました。

(スプライトの単位って枚であってるのか??)
トマトみたい(小並感)
早速Unityに取り込んでいこう。

でか。
いや。見やすいようにでかくしようとは思ってたし、想定通りの大きさなんだけど・・・
いざサフィアちゃんの隣に配置してみるとサイズ感が怖いな。
まぁ、いいことか。
なんか、、こいつ「ぷよぷよ」みたいにぷにぷにさせるアニメーション付けたら面白そうだな。
・・・・・・(^_^)

とりあえず動かしてみた。
で、初めて使うんだけど、Root Motionなるものがあるそうでして。。。
簡単にいうと、Root Motionをオンにすると、今いる位置からアニメーションを始めてくれる。みたいな。

ほんとUnityは便利ねぇ。。。

アニメーションをちょっと修正した結果がこれ↑。
若干動きすぎなきもするがその辺の調整は追々しよう。

以前作ったEnemyスクリプトをアタッチして実行したら倒せるようになった!
やったぁぁぁっぁぁい
今回はここまで!
今のところなんとかスケジュール通りには進めてるな。。。
次回はついにサフィアちゃんに回避、パリィを実装していきたいと思います!
【Unity】最強のゲームクリエイターに俺はなる | コンボ実装!
どーもーーーー「MEG2」を観てたらめっちゃ笑えて楽しかったFORM_07ですよぉぉい
もう題名「ステイサム2」でいいやん( ^o^)
前回↓はアニメーションとステートを連携させて攻撃の土台を作りましたね!
【Unity】最強のゲームクリエイターに俺はなる | アニメーション終了を検知して攻撃させよう - FORM_07’s blog
今回はそれを使ってあれしていきます!
とその前に、、、、
スケジュールを組んでおこう。
このままのペースだといつになっても完成しない気がしてきたゾ。。。
できた。

とりあえずここまででいいや。
いつ完成するか目処を立てるというよりは、その日することをなんとなく決めておくことが目的だからね。
あんまりがちがちに予定立てて行動するの苦手だからこんなもんでいいでしょ。
適当じゃん・・・(゚▽゚*)
さーーーーーーてスプライト作るぞーーーーーーい
モーションをドットで書くとか慣れなさすぎて時間対成果がおわってる。
・・・・・・・・・・・・・
はい、結局何も進まず一日がたちました
・・・・・・・・(゚◇゚)
スケジュールたてた意味。。。
い、一応日曜日に調整日を入れてるから多少のずれはいいってことでね。


がんばって2つ作った。
1個目が残心(フォロースルー)状態、2個目が2撃目の攻撃です。
最低限これだけ用意できればコンボの実装ができるので、早速やっていきましょう。
(先に全部作ったりしちゃうと後々変更とか作り直しが大変だからね)

まずはAnimator Controlerをいいかんじに組む。
Attack(攻撃)アニメーションが終了したらFollowTrough(残心)アニメーションへ遷移させて、さらに残心中に攻撃入力があれば次の攻撃アニメーションに遷移するようにしてます。
攻撃入力があったかどうかは、以前↓作ったパラメータをそのまま使ってます。
【Unity】最強のゲームクリエイターに俺はなる | アニメーション終了を検知して攻撃させよう - FORM_07’s blog
残心中に攻撃入力がなければ、Attack_end(攻撃終了)アニメーションへ遷移させて、それが終わったらDefault(標準)アニメーションに戻してます。
こんな流れでどんどん繋げてけばコンボが実装できるってわけだ!
そしてここで間違いに気づく。。。
前回↓アニメーション終了を検知したつもりでいたけど、この方法じゃ無理だった。
【Unity】最強のゲームクリエイターに俺はなる | アニメーション終了を検知して攻撃させよう - FORM_07’s blog
いや!前回は正常に動作してたんだよ!?でも!勘違いしててさ!!
勘違いというか、浅はかだった。
アニメーションが遷移する中で、GetCurrentAnimatorStateInfoで取得したアニメーションのnormalizedTimeが1以上になるわけなかった。。。
だってアニメーションが終わった(normalizedTimeが1になった)瞬間、次のアニメーションが始まる(normalizedTimeが0に戻る)んだもん。
迂闊だった。
前回正しく動いたように見えた理由は、Defaultアニメーションがループしてるから。
ってことで修正。

やってることは簡単で、現在再生中のアニメーションの名前を取得してそれに応じてステートを遷移させてるだけです。
修正できたけど、、、なーーーーーんか美しくないなぁ。。。
・・・・・・まぁ動けばいいか
ってことで実行!

わかりづらいですが、攻撃モーションの後に残心モーションが入って、その間に攻撃入力をしたので2撃目が発動してます。
ちなみにだけど、わざわざ1回の攻撃に、攻撃、残心、攻撃終了の3つのステートを用意してるかというと、だいたいの人が攻撃ボタンを連打するからです。
つまり攻撃ステート中は攻撃入力を受け付けず、残心ステート中に受け付けるってかんじで攻撃が連発するのを防いでいます。
攻撃終了ステート中に攻撃が入力されたら、コンボは1から始まるかんじ。
よーーーーし今回はここまで!
本当は全攻撃のスプライトをつくってサフィアちゃんの攻撃を一通り完成させるつもりだったけど、時間足んねぇや!
一日短すぎ
でもいつでもコンボの続きを実装できる状態にできたのはでかいですね!
次回からは敵に手をつけていきたい。。。
【Unity】最強のゲームクリエイターに俺はなる | アニメーション終了を検知して攻撃させよう
ども~。「キングダム 運命の炎」がおもろくてうれしいFORM_07ですよい!
早く続きが観たいぎぎぎぎぎ。。。
前回↓はState駆動システムを実装し直しましたね!
【Unity】最強のゲームクリエイターに俺はなる | ステート駆動システムをUpdate関数で実装しよう! - FORM_07’s blog
今回はそれを使ってサフィアちゃんの攻撃の種類を増やしたい。
さぁぁてまずはスプライトを用意しないとな。。。

まず1つ。
エフェクトとかは後回し。
前と比べると早く完成できるようにはなったな。。。
これをUnityに取り込んで、、、Animator Controllerにいい感じに追加して、、、

(Animator Controllreの設定について詳しくはこちら↓から)
今回はアニメーション遷移にTrigger型のパラメータを使ってます。
boolとtriggerの違いだけど、クソ雑魚語彙力で説明すると、boolはレバー、triggerはボタンってかんじ。
boolはtrue (on) とfalse (of) の切り替えができて、triggerはできない。
逆に言うとtriggerは一度onにしたらすぐにoffになるから、boolみたいにわざわざ切り替える必要がない。
今やってる「攻撃」みたいな、1回きりのアニメーションにはtriggerが向いてて、しゃがみとかバッグを開く、みたいな状態を切り替える系のアニメーションにはboolが向いてるってこと。
さて説明も終わったところで、
攻撃関数でtriggerをonにする。

あとは、、、ステート駆動システムを採用してるからそっちにも対応させないとな。
これ知らなかったんだけど、GetCurrentAnimatorStateInfo(レイヤー番号).normalizedTimeで今再生中のアニメーションの進捗を参照できるみたい。
normalizedTimeが0で再生前、0.5で半分再生済み、1で再生後。
すげぇこれ使えばアニメーションが終わった時点でステートを遷移させれるじゃん。

ここで注意なのが、normalizedtime == 1じゃなくてnormalizedTime >= 1にすること。
Update関数で1フレームが実行された瞬間に一寸の狂いも無くちょうどアニメーションが終わることなんてないからね。

よぉぉぉ~~し、いいかんじ!
今回はここまで!
学びのある日々はいいな、やはり。
次回からはコンボを実装していこうと思います!