💨

【Roblox】ダッシュとエアダッシュを作ってみよう(3)~風エフェクト~

に公開

1. はじめに

前回までの記事で、「ボタンを押すと一定距離を素早く一直線に進む」というダッシュ機能を、LinearVelocityを利用して実装しました。
今回の記事から読んでもあまり問題はありませんが、まだ前回、前々回の記事を読んでいない方は、ぜひご覧いただければと思います。
https://zenn.dev/landho_roblox/articles/e868868889e121
https://zenn.dev/landho_roblox/articles/a5fc15a90750b5

今回は、このダッシュ機能に、スピード感を強調する風の画面エフェクトを追加しようと思います。
雑な絵で申し訳ないですが、イメージとしては以下のような集中線っぽいエフェクトが流れる感じです。

ただし、横向きにダッシュした時はこうなってほしいです。

こんな感じで、どんな角度でも成立するエフェクトを作ってみましょう。

バージョン:0.669.1.6690663

2. エフェクト

今回のエフェクトは、板状のPartからParticleを出し、Characterの移動速度ベクトルの向きによって位置や角度を調整すればできそうです。

赤い四角のところに見えない板を置き、そこからParticleを出す
というわけで、まずはParticleEmitterでエフェクト部分を作ります。
Workspace下にPartを追加し、その下にParticleEmitterを追加してください。
名前は「WindParticlePart」と「WindParticleEmitter」としておきます。

Part

まずはPartを調整します。
当たり判定は不要なので、CanCollideCanQueryCanTouchは全てfalseにし、動かないようにAnchoredをtrueにしておきます。
Sizeはとりあえず(8, 8, 1)くらいにしておきましょう。
Transparencyは1にして見えなくするのですが、制作中はわかりやすいように0.5くらいにしておきます。

デフォルトのParticleが出ていますが、これでひとまずPartはOKです。

ParticleEmitter

次にParticleEmitterを作っていきます。
設定するプロパティが多いので、プロパティの項目分けごとに区切って説明していきます。

外観

最初は外観タブのプロパティです。
今回は白くて細い線が出せればいいので、TextureはRobloxに元から用意されている画像をプロパティで調整すれば問題無さそうです。
Robloxに元から用意されている画像とその参照方法については以下の記事で解説していますので、詳しく知りたい方はこちらをご覧いただければと思います!
https://zenn.dev/landho_roblox/articles/57a98e9a8a8af8
今回は、「rbxasset://textures/particles/explosion01_implosion_main.dds」に設定しましょう。
白い円形のParticleが出るようになったと思います。

次に、Squashを-10にします。
SquashはParticleの縦横比率を調整するもので、負の値にすると縦が潰れ、横に長くなります。
Sizeはとりあえず0.5くらいでしょうか。
こんな感じの細長い線のParticleが出るようになったかと思います。

ただ、このままでは風にはならないので、OrientationVelocityParallelに設定します。
OrientationはParticleの向きを決定する方式を選択できるプロパティで、VelocityParallelにした場合、進行方向に対して平行に向くようになります。
そしてTransparencyを以下の画像のように、最初と最後が透明になるようにNumberSequenceを設定しましょう。


だいぶ風に使えそうな感じになってきました!
これで外観のプロパティの設定はOKです。

パーティクル

次に、パーティクル系のLockedToPartをTrueにします。
これは、生成元のPartなどの座標や向きが変わった際、既に生成されているParticleが連動して移動するかどうかを決定するプロパティで、trueにすると生成元に付いてくるようになります。
一見風のエフェクトは生成元から独立していた方が良さそうに見えますが、今回は画面エフェクトなので、カメラの座標を基準に生成する予定です。
その場合、カメラが高速で移動した時にParticleが置いて行かれると、カメラの速度と相対的に速度を計算して設定しなければならなくなってしまいます。
一緒に付いてきてくれた方が、カメラの移動速度とは関係なくParticleの速度を調整できて演出に使いやすくなります。

放射

次は放射タブです。
まずはEmissionDirectionBackに設定しましょう。
Partのどの方向にParticleを生成するのかを設定するプロパティで、BackならPartの背面方向にParticleが生成されるようになります。
そして、Lifetimeは0.25、RateSpeedは50に設定しましょう。
Enabledについては後でfalseにしますが、確認用にまだtrueにしておきます。

静止画だとちょっとわかりづらいですが、一気に風らしくなってきました!

放射器の形状

最後に放射器の形状タブです。
ここのプロパティで、生成範囲の形状を調整できます。
まずは、ShapeDiscに設定しましょう。
生成範囲が円盤状になります。
次に、ShapePartialを0.5にします。
これは、生成範囲を部分的に変化させるプロパティで、Shapeプロパティの設定によって効果が変わるのですが、Discにした場合は円盤の中心にParticleの出ない範囲を指定できます。
0にした時は円盤の外周のみから生成され、1にした時は円盤全体が生成範囲になります。
0.5にした場合は半分の半径まで穴の開いたドーナツ状になるイメージです。
こうすることで、真正面から風が生成されている時に、カメラのすぐ近くには生成されづらくすることができます。
ちなみに、公式のリファレンスだとDiscの時の0と1の説明が逆になっているようなのですが、「0で外周のみ、1で全体」が実際の挙動のようです。(記事作成時点)

さて、これでParticleEmitterの設定が完了したので動作を見てみましょう。

かなりイメージ通りになっているんじゃないでしょうか!

最後に、確認用にtrueにしていたEnabledはfalseにして、場所をReplicatedStorageに移しましょう。

これで、エフェクト部分の作成が完了しました!

ちなみに、ParticleEmitterについては以下の記事でも作例を紹介しています。
エフェクト作成の際にはぜひ参考にしてみてください!
https://zenn.dev/landho_roblox/articles/dddd928e2dc6dd
https://zenn.dev/landho_roblox/articles/ae0139cf092d15

3. Script

ここからは、Scriptで動作を作っていきます。

前準備

まずは、WindParticlePartにScriptを一つ追加してください。
名前は「WindParticleScript」としておきます。
そして、RunContextのプロパティをClientに設定してください。
RunContextについてはこちらの記事で解説しているので詳しくは省略しますが、Client設定でサーバー上のReplicatedStorageに置いておけば、クライアント上に複製された時に自動的に各クライアントで動作するようになります。

自分でSetup

そして、以下のコードを追加してください。

WindParticleScript
--!strict

-- Constant
local SPEED_THRESHOLD = 60
local RADIUS = 4

-- Veriable
local player_ = game:GetService("Players").LocalPlayer
local particlePart_ = script.Parent
local particleEmitter_ = particlePart_:WaitForChild("WindParticleEmitter")
local velocityUnit_ = Vector3.new() -- 最後にエフェクトがONになっていた時の正規化された速度ベクトル.
local camera_ = workspace.CurrentCamera
while not camera_ do
	task.wait()
	camera_ = workspace.CurrentCamera
end

-- 自分でカメラの子オブジェクトになる.
particlePart_.Parent = camera_

諸々の変数宣言の後、workspace.CurrentCameraの取得を待ち、取得できたらWindParticlePartをCurrentCameraの子にしています。
こうすることで、Playerが接続した時に別のScriptから設定する処理を省略し、Setupと動作を一つのScriptに纏めています。
どちらの方法がよいかは場合によると思いますが、今回は説明を簡略化できるためこのようにしています。

カメラを基準に座標設定

さて、エフェクトの作成中にも少し触れましたが、今回作ろうとしているエフェクトで強調したいのはプレイヤーから見たスピード感なので、エフェクトはPlayerCharacterではなく、カメラに対して付けるのが良さそうです。
というわけでとりあえず、カメラを基準に、ダッシュ移動している方向にズラした位置にWindParticlePartを置いてみましょう。

先ほどのコードに続けて、以下のコードを追加してください。

WindParticleScript
-- カメラの座標を基準にするので、RenderSteppedのコールバック関数にする.
local function onRenderStepped(deltaTime: number)
	local character = player_.Character
	local rootPart = character and character:FindFirstChild("HumanoidRootPart")::Part
	local linearVelocity = rootPart and rootPart:FindFirstChild("DashLinearVelocity")::LinearVelocity
	if not linearVelocity then
		particleEmitter_.Enabled = false
		return
	end
	
	-- 状態と速度を見てエフェクトのONOFFを切り替える.
	local velocity = rootPart.AssemblyLinearVelocity
	if not linearVelocity.Enabled or velocity.Magnitude < SPEED_THRESHOLD then
		particleEmitter_.Enabled = false

	else
		particleEmitter_.Enabled = true

		-- ONの時のみ正規化された速度のベクトルを更新.
		velocityUnit_ = velocity.Unit
	end

	-- エフェクトパーツのCFrameを更新.
	local cameraCF = camera_.CFrame
	local pos = cameraCF.Position + (velocityUnit_ * RADIUS)
	particlePart_.CFrame = CFrame.lookAt(pos, pos + velocityUnit_)
end

-- イベントを接続.
game:GetService("RunService").RenderStepped:Connect(onRenderStepped)

RobloxのカメラはRenderSteppedで動作しているので、それに合わせてRenderSteppedに接続すると動作が安定します。
nilチェックの後に、簡易的にダッシュ用のLinearVelocityのONOFFと現在の速度を見てエフェクトを出すかを決めています。
そして、エフェクトを出す場合にのみ、最後の計算で用いる「正規化された速度ベクトル」を更新しています。
これは、エフェクトが生成されている時のみWindParticlePartの角度を変更するようにしないと、ダッシュ後にHumanoidRootPart.AssemblyLinearVelocityの方向が変化してしまった際、LockedToPartの影響で、まだ残っているParticleの位置や角度も変わってしまうためです。
ただし、カメラとWindParticlePartの相対的な位置関係は維持する必要があり、座標の更新は行わなければならないため、角度の算出に使っているベクトルは最後にエフェクトがONだった時のものを使用しています。
最後に、CFrame.LookAtの第二引数に速度の単位ベクトル分ズラした位置を指定しているので、WindParticlePartが進行方向側を向きます。
ParticleEmitterのEmissionDirectionをBackに指定しているので、背後側、つまりカメラの方に向かってエフェクトが出るようになるわけです。
さて、この計算式だと、WindParticlePartが座標に取りうる軌道の円は、これまたざっくりの図ですが以下のようになります。

上から見た図で上部がカメラ前方 赤い円がPartが座標に取りうる軌道円
一見これで良さそうな気がしますが、実はまだ問題があります。
真横付近にWindParticlePartが来た時、カメラより後ろに生成されたParticleは視界に入らないため、正面に来ている時に比べて半分程度のParticleの量になってしまいます。

青い領域を通るParticleは見えるが、灰色の領域を通るParticleは見えない
というわけで、どんな角度の時にも適切に見えるように、WindParticlePartの回転の中心点をズラして対応します。

回転の中心点をズラす

コードに以下の修正を行ってください。

WindParticleScript
 --!strict

 -- Constant
local SPEED_THRESHOLD = 60
local RADIUS = 4
+local FRONT_OFFSET = 3

 ----------- 省略 -----------

 -- カメラの座標を基準にするので、RenderSteppedのコールバック関数にする.
local function onRenderStepped(deltaTime: number)

 	----------- 省略 -----------

 	-- エフェクトパーツのCFrameを更新.
	local cameraCF = camera_.CFrame
+	local pos = cameraCF.Position + (cameraCF.LookVector * FRONT_OFFSET) + (velocityUnit_ * RADIUS)
-	local pos = cameraCF.Position + (velocityUnit_ * RADIUS)
	particlePart_.CFrame = CFrame.lookAt(pos, pos + velocityUnit_)
end

 -- イベントを接続.
game:GetService("RunService").RenderStepped:Connect(onRenderStepped)

FRONT_OFFSETの分だけ、カメラから前方にPartの座標がズレるようになりました。
図にすると以下のようになります。

カメラ前方に対して、FRONT_OFFSETの分、中心点をズラしている
これならどんな角度になってもそれっぽい見え方になりそうです。
最後に、確認用に0.5にしていたWindParticlePartのTransparencyは1にしましょう。
それではさっそく実行して確認してみます。

これで、ダッシュのスピード感を強調する風エフェクトができました!

サンプルコード

今回作成した最終的なサンプルコードを掲載しておきます。

サンプルコード
WindParticleScript
--!strict

-- Constant
local SPEED_THRESHOLD = 60
local RADIUS = 4
local FRONT_OFFSET = 3

-- Veriable
local player_ = game:GetService("Players").LocalPlayer
local particlePart_ = script.Parent
local particleEmitter_ = particlePart_:WaitForChild("WindParticleEmitter")
local velocityUnit_ = Vector3.new() -- 最後にエフェクトがONになっていた時の正規化された速度ベクトル.
local camera_ = workspace.CurrentCamera
while not camera_ do
	task.wait()
	camera_ = workspace.CurrentCamera
end

-- 自分でカメラの子オブジェクトになる.
particlePart_.Parent = camera_

-- カメラの座標を基準にするので、RenderSteppedのコールバック関数にする.
local function onRenderStepped(deltaTime: number)
	local character = player_.Character
	local rootPart = character and character:FindFirstChild("HumanoidRootPart")::Part
	local linearVelocity = rootPart and rootPart:FindFirstChild("DashLinearVelocity")::LinearVelocity
	if not linearVelocity then
		particleEmitter_.Enabled = false
		return
	end

	-- 状態と速度を見てエフェクトのONOFFを切り替える.
	local velocity = rootPart.AssemblyLinearVelocity
	if not linearVelocity.Enabled or velocity.Magnitude < SPEED_THRESHOLD then
		particleEmitter_.Enabled = false

	else
		particleEmitter_.Enabled = true

		-- ONの時のみ正規化された速度ベクトルを更新.
		velocityUnit_ = velocity.Unit
	end

	-- エフェクトパーツのCFrameを更新.
	local cameraCF = camera_.CFrame
	local pos = cameraCF.Position + (cameraCF.LookVector * FRONT_OFFSET) + (velocityUnit_ * RADIUS)
	particlePart_.CFrame = CFrame.lookAt(pos, pos + velocityUnit_)
end

-- イベントを接続.
game:GetService("RunService").RenderStepped:Connect(onRenderStepped)

4. まとめ

  • 透明なPartからParticleを出すことで、画面エフェクトを作る
  • ParticleEmitter.Squashで、Particleの縦横比率を変更できる
  • ParticleEmitter.LockedToPartをtrueにすると、Particleが生成元に連動して付いてくるようになる
  • ParticleEmitter.Shapeで放射器の形状を選択でき、Discの場合円盤状にできる
  • ParticleEmitter.ShapePartialで放射器の形状を調整でき、ShapeがDiscの場合、ドーナツ状の穴の大きさを調整できる
  • Script.RunContextのプロパティをClientにすることで、Scriptが自分でクライアント上の最初の設定を行える
  • エフェクトがONの時のみ生成元のPartの角度を変更することで、生成後のParticleの位置や角度が不自然に変わってしまうことを防ぐ
  • 生成元が真横に来てしまうとカメラの背後を通るParticleが見えなくなってしまうので、生成元Partの軌道の中心点をカメラの前方に少しズラす

ちなみに、前回までに作ったダッシュは基本的に水平にしか移動できませんが、このエフェクト自体は上下方向のベクトルにも対応しているので、他の色んなアクションにも流用することができます。
ぜひ自分なりに改良して活用してみてください!
最後までお読みいただき、ありがとうございました!

5. 参考

https://create.roblox.com/docs/reference/engine/classes/Part
https://create.roblox.com/docs/effects/particle-emitters
https://create.roblox.com/docs/reference/engine/classes/ParticleEmitter
https://create.roblox.com/docs/en-us/reference/engine/enums/RunContext

ランド・ホー Roblox開発チーム

Discussion