├── .gitignore
├── LICENSE
├── README.md
├── aftman.toml
├── default.project.json
├── development.project.json
├── src
├── Assets.rbxm
├── Functions.lua
├── Operator.lua
├── Settings.lua
└── init.lua
└── wally.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rbxl
2 | *.lock
3 |
4 | **/Packages/**
5 | **/ServerPackages/**
6 | **/DevPackages/**
7 |
8 | sourcemap.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 rotn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | v1.1.2 • [Model](https://create.roblox.com/marketplace/asset/15420466379/) • [Devforum](https://devforum.roblox.com/t/blood-engine-a-droplet-emitter-system/2545682)
13 |
14 |
15 |
16 | ## What is Blood Engine?
17 | Blood Engine is a versatile resource that can be utilized for various applications, including creating effects like paint, water, blood, and more. It offers numerous methods tailored to meet your specific needs.
18 |
19 | One of its key features is the ability to emit "droplets" - these are meshes that can take on the appearance of "Decals" or "Spheres". These droplets can be emitted from any given origin point with a given velocity. Upon landing on a surface, such as a wall or floor, they transform into a pool.
20 |
21 | This entire process is highly customizable, with 24 options at your disposal to tweak and adjust according to your requirements. This ensures that Blood Engine can adapt to a wide range of scenarios and use-cases, providing you with the flexibility to create the exact effect you're aiming for.
22 |
23 | ## Installation
24 | You can install Blood Engine through the latest release of the repository, the [Model](https://create.roblox.com/marketplace/asset/15420466379/) published on Roblox, or by using Wally:
25 | ```toml
26 | [dependencies]
27 | BloodEngine = "rotntake/blood-engine@1.1.2"
28 | ```
29 |
30 | ## Usage
31 | #### Initialization
32 | Firstly, you'll need to initialize BloodEngine with your preferred settings. This can be done in either a client or server script. However, it's generally more advisable to do this on the client side, so we'll proceed with that approach.
33 |
34 | The settings provide you with control over various aspects of droplets and pools. These include the maximum number of droplets that can be created, the type of droplets to use, the velocity of droplets upon emission, and much more.
35 | ```lua
36 | -- Import the BloodEngine module
37 | local BloodEngine = require(PathToModule)
38 |
39 | -- Initialize BloodEngine with desired settings
40 | local Engine = BloodEngine.new({
41 | Limit = 100, -- Sets the maximum number of droplets that can be created.
42 | Type = "Default", -- Defines the droplet type. It can be either "Default" (Sphere) or "Decal",
43 | RandomOffset = false, -- Determines whether a droplet should spawn at a random offset from a given position.
44 | OffsetRange = {-20, 10}, -- Specifies the offset range for the position vectors.
45 | DropletVelocity = {1, 2}, -- Controls the velocity of the emitted droplet.
46 | DropletDelay = {0.05, 0.1}, -- Sets the delay between emitting droplets in a loop (for the EmitAmount method).
47 | StartingSize = Vector3.new(0.01, 0.7, 0.01), -- Sets the initial size of the droplets upon landing.
48 | Expansion = true, -- Determines whether a pool can expand when a droplet lands on it.
49 | MaximumSize = 1, -- Sets the maximum size a pool can reach.
50 | })
51 | ```
52 | #### Emitting Droplets
53 | After initializing the module, you're all set to emit droplets. There are two key methods available for droplet emission: `EmitAmount` and `Emit`.
54 | ```lua
55 | -- Emit a specific amount of droplets from a given origin in specific or nil direction
56 | -- (Setting the Direction to nil will make droplets go in random directions)
57 | Engine:EmitAmount(Origin, Direction, Amount)
58 |
59 | -- Emit a single droplet from a given origin in a specific or nil direction
60 | Engine:Emit(Origin, Direction)
61 | ```
62 | In this instance, we’ll be utilizing the `EmitAmount` method. Typically, you’d use the `Emit` method when you want to create your own loop instead of relying on the built-in loop of `EmitAmount` . This gives you more control over the emission process.
63 |
--------------------------------------------------------------------------------
/aftman.toml:
--------------------------------------------------------------------------------
1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager.
2 | # For more information, see https://github.com/LPGhatguy/aftman
3 |
4 | # To add a new tool, add an entry to this table.
5 |
6 | [tools]
7 | wally = "UpliftGames/wally@0.3.2"
8 | rojo = "rojo-rbx/rojo@7.3.0"
--------------------------------------------------------------------------------
/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Blood Engine",
3 | "tree": {
4 | "$path": "src"
5 | }
6 | }
--------------------------------------------------------------------------------
/development.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Blood Engine",
3 | "tree": {
4 | "$className": "DataModel",
5 |
6 | "ReplicatedStorage": {
7 | "$className": "ReplicatedStorage",
8 | "$ignoreUnknownInstances": true,
9 |
10 | "Packages": {
11 | "$className": "Folder",
12 | "$path": "Packages",
13 |
14 | "BloodEngine": {
15 | "$path": "src"
16 | }
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Assets.rbxm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rotntake/BloodEngine/93aab15828a17409f44536cdb56a4a4bfce99061/src/Assets.rbxm
--------------------------------------------------------------------------------
/src/Functions.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | @Description: Contains a list of useful functions.
3 | ]]
4 |
5 | local TweenService = game:GetService("TweenService")
6 | local Workspace = game:GetService("Workspace")
7 |
8 | -- Variable definitions
9 | local ParentClass = script.Parent
10 | local Assets = ParentClass.Assets
11 |
12 | -- Asset definitions
13 | local Images = Assets.Images
14 | local Essentials = Assets.Essentials
15 | local Effects = Assets.Effects
16 |
17 | -- Effect definitions
18 | local TrailEffects = Effects.Trail
19 | local ImpactEffects = Effects.Impact
20 |
21 | -- Essential definitions
22 | local FastCast = require(Essentials.FastCast)
23 | local Random = Random.new()
24 |
25 | -- Globals
26 | local Unpack = table.unpack
27 | local Decals = Images:GetChildren()
28 |
29 | -- Module definition
30 | local Functions = {}
31 |
32 | -- Variable definitions
33 | local Properties = {
34 | "Size",
35 | "Transparency",
36 | "Anchored",
37 | }
38 |
39 | --[[
40 | A shorter way of doing:
41 | ```lua
42 | typeof(Variable) == "Type"
43 | ```
44 | ]]
45 | function Functions.IsOfType(Any, Type: string)
46 | return typeof(Any) == Type
47 | end
48 |
49 | --[[
50 | Allows the ability to insert an array of
51 | variables efficently onto a table.
52 | ]]
53 | function Functions.MultiInsert(List: {}, Variables: {})
54 | for Key, Variable in Variables do
55 | --[[
56 | Executes the variable if it's a function,
57 | It is expected to return a variable to later assign.
58 | ]]
59 | if Functions.IsOfType(Variable, "function") then
60 | Variable = Variable()
61 | end
62 |
63 | -- Adds in the variable with a key
64 | if Functions.IsOfType(Key, "string") then
65 | List[Key] = Variable
66 | end
67 |
68 | -- Adds in the variable without a key
69 | table.insert(List, Variable)
70 | end
71 | end
72 |
73 | --[[
74 | Returns the name of the specified function
75 | within the class’s metatable.
76 | ]]
77 | function Functions.GetFunctionName(Function, Table)
78 | for Name, AltFunction in Table do
79 | return AltFunction == Function and Name
80 | end
81 |
82 | return nil
83 | end
84 |
85 | --[[
86 | Sets up a `CastBehavior` for later use,
87 | then returns it.
88 | ]]
89 | function Functions.SetupBehavior(Cache, CastParams): FastCast.Behavior
90 | -- Define Variables
91 | local Behavior = FastCast.newBehavior()
92 | local Gravity = Workspace.Gravity
93 |
94 | -- Update Behavior properties
95 | Behavior.Acceleration = Vector3.new(0, -Gravity, 0)
96 | Behavior.MaxDistance = 500
97 | Behavior.RaycastParams = CastParams
98 | Behavior.CosmeticBulletProvider = Cache
99 |
100 | -- Export behavior
101 | return Behavior
102 | end
103 |
104 | --[[
105 | Clones and parents Droplet effects from a template part.
106 | ]]
107 | function Functions.CreateEffects(Parent: MeshPart, ImpactName: string)
108 | -- Variable definitions
109 | local Trail = TrailEffects:Clone()
110 |
111 | local Attachment0 = Instance.new("Attachment")
112 | local Attachment1 = Instance.new("Attachment")
113 | local ImpactAttachment = Instance.new("Attachment")
114 |
115 | -- Update Trail-related properties
116 | Trail.Attachment0 = Attachment0
117 | Trail.Attachment1 = Attachment1
118 |
119 | Attachment1.Position = Vector3.new(0.037, 0, 0)
120 | Attachment0.Name = "Attachment0"
121 | Attachment1.Name = "Attachment1"
122 |
123 | Attachment0.Parent = Parent
124 | Attachment1.Parent = Parent
125 | Trail.Parent = Parent
126 |
127 | -- Update Impact-related properties
128 | for _, Effect in ipairs(ImpactEffects:GetChildren()) do
129 | local Clone = Effect:Clone()
130 | Clone.Parent = ImpactAttachment
131 | end
132 |
133 | ImpactAttachment.Name = ImpactName
134 | ImpactAttachment.Parent = Parent
135 | ImpactAttachment.Orientation = Vector3.new(0, 0, 0)
136 | end
137 |
138 | --[[
139 | Returns an empty object template that's going to be used as a droplet.
140 | ]]
141 | function Functions.GetDroplet(ImpactName: string, IsDecal: boolean): {}
142 | -- Variable definitions
143 | local Droplet = Instance.new("MeshPart")
144 |
145 | -- Update properties
146 | Droplet.Size = Vector3.new(0.1, 0.1, 0.1)
147 | Droplet.Transparency = 0.25
148 | Droplet.Material = Enum.Material.Glass
149 |
150 | Droplet.Anchored = false
151 | Droplet.CanCollide = false
152 | Droplet.CanQuery = false
153 | Droplet.CanTouch = false
154 |
155 | -- Export droplet
156 | Functions.CreateEffects(Droplet, ImpactName)
157 | return Droplet
158 | end
159 |
160 | --[[
161 | Returns a folder that handles droplets; If it doesn't exist,
162 | make a new one in Workspace.Terrain.
163 | ]]
164 | function Functions.GetFolder(Name: string): Folder
165 | -- Variable definitons
166 | local Terrain = Workspace.Terrain
167 | local DropletsFolder = (Terrain:FindFirstChild(Name) or Instance.new("Folder"))
168 |
169 | -- Update properties
170 | DropletsFolder.Name = Name
171 | DropletsFolder.Parent = Terrain
172 |
173 | -- Export folder
174 | return DropletsFolder
175 | end
176 |
177 | --[[
178 | Returns a Vector3, given the array range.
179 | ]]
180 | function Functions.GetVector(Range: {})
181 | -- Vector definition
182 | local Vector = Vector3.new(
183 | Random:NextNumber(Unpack(Range)),
184 | Random:NextNumber(Unpack(Range)),
185 | Random:NextNumber(Unpack(Range))
186 | )
187 |
188 | -- Export position with applied offset
189 | return Vector
190 | end
191 |
192 | --[[
193 | NextNumber; Uses a global Random class,
194 | this is done for efficency.
195 | ]]
196 | function Functions.NextNumber(Minimum, Maximum): number
197 | return Random:NextNumber(Minimum, Maximum)
198 | end
199 |
200 | --[[
201 | An efficent way of doing TweenService:Create(...)
202 | ]]
203 | function Functions.CreateTween(Object: Instance, Info: TweenInfo, Goal: {}): Tween
204 | -- Export tween
205 | return TweenService:Create(Object, Info, Goal)
206 | end
207 |
208 | --[[
209 | Plays a sound in the given parent,
210 | used to play `End` & `Start` sounds.
211 | ]]
212 | function Functions.PlaySound(Sound: Sound, Parent: Instance)
213 | if not Sound then
214 | return
215 | end
216 |
217 | local SoundClone = Sound:Clone()
218 | SoundClone.Parent = Parent
219 |
220 | SoundClone.Ended:Connect(function()
221 | SoundClone:Destroy()
222 | end)
223 |
224 | SoundClone:Play()
225 | end
226 |
227 | --[[
228 | Returns a random value/object from the
229 | given table.
230 | ]]
231 | function Functions.GetRandom(Table: {})
232 | return #Table > 0 and Table[math.random(1, #Table)]
233 | end
234 |
235 | --[[
236 | Resets the properties of the given droplet,
237 | used to return pools to be recycled.
238 | ]]
239 | function Functions.ResetDroplet(Object: Instance, Original: Instance)
240 | -- Variable definitions
241 | local Decal = Object:FindFirstChildOfClass("SurfaceAppearance")
242 | local Weld = Object:FindFirstChildOfClass("WeldConstraint")
243 | local Trail = Object:FindFirstChildOfClass("Trail")
244 |
245 | -- Reset all properties
246 | for _, Property: string in Properties do
247 | Object[Property] = Original[Property]
248 | end
249 |
250 | -- Update outsider properties
251 | if Trail then
252 | Trail.Enabled = false
253 | end
254 |
255 | if Weld then
256 | Weld:Destroy()
257 | end
258 |
259 | if Decal then
260 | Decal:Destroy()
261 | end
262 |
263 | -- Export object
264 | return Object
265 | end
266 |
267 | --[[
268 | Manages the sequence of decals;
269 | initiates only when the Type is designated as Decals.
270 | ]]
271 | function Functions.ApplyDecal(Object: Instance, IsDecal: boolean)
272 | if not IsDecal then
273 | return
274 | end
275 |
276 | -- Variable definitions
277 | local Decal: SurfaceAppearance = Functions.GetRandom(Decals):Clone()
278 |
279 | -- Update Decal properties
280 | Decal.Parent = Object
281 | end
282 |
283 | --[[
284 | Emits particles by looping
285 | through an attachment's children; emitting a specific
286 | amount of them using the given amount.
287 | ]]
288 | function Functions.EmitParticles(Attachment: Attachment, Amount: number)
289 | -- Variable definitions
290 | local Particles = Attachment:GetChildren()
291 |
292 | -- Emits particles
293 | for _, Particle: ParticleEmitter in Particles do
294 | if not Particle:IsA("ParticleEmitter") then
295 | continue
296 | end
297 |
298 | Particle:Emit(Amount)
299 | end
300 | end
301 |
302 | --[[
303 | Returns the closest part within a given distance.
304 | ]]
305 | function Functions.GetClosest(Origin: BasePart, Magnitude: number, Ancestor): BasePart
306 | -- Variable definitions
307 | local Children = Ancestor:GetChildren()
308 | local ClosestPart = nil
309 | local MinimumDistance = math.huge
310 |
311 | for _, Part: BasePart in Children do
312 | local Distance = (Origin.Position - Part.Position).Magnitude
313 |
314 | local Logic = (not Part.Anchored and Origin ~= Part and Distance < Magnitude and Distance < MinimumDistance)
315 |
316 | if not Logic then
317 | continue
318 | end
319 |
320 | MinimumDistance = Distance
321 | ClosestPart = Part
322 | end
323 |
324 | -- Export closest part
325 | return ClosestPart
326 | end
327 |
328 | --[[
329 | Provides the target angles; utilized to
330 | assign the orientation to base position or CFrame.
331 | ]]
332 | function Functions.GetAngles(IsDecal: boolean, RandomAngles: boolean): CFrame
333 | -- Variable definitions
334 | local RandomAngle = Functions.NextNumber(0, 180)
335 | local AngleX = (IsDecal and -math.pi / 2 or math.pi / 2)
336 | local AngleY = (RandomAngles and RandomAngle or 0)
337 |
338 | -- Export angles
339 | return CFrame.Angles(AngleX, AngleY, 0)
340 | end
341 |
342 | --[[
343 | Delievers the target position; serves
344 | as a foundation that is subsequently
345 | applied with an orientation.
346 | ]]
347 | function Functions.GetCFrame(Position: Vector3, Normal: Vector3, IsDecal: boolean): CFrame
348 | -- Variable definitions
349 | local DecalOffset = (IsDecal and (Normal / 76) or Vector3.zero)
350 |
351 | local Base = (Position + DecalOffset)
352 |
353 | local Target = (Position + Normal)
354 |
355 | -- Export cframe
356 | return CFrame.new(Base, Target)
357 | end
358 |
359 | --[[
360 | Refines the components of the given
361 | Vector3; utilized to implement modifications
362 | based on factors.
363 | ]]
364 | function Functions.RefineVectors(IsDecal: boolean, VectorData: Vector3)
365 | local YVector = (IsDecal and 0 or VectorData.Y)
366 |
367 | return Vector3.new(VectorData.X, YVector, VectorData.Z)
368 | end
369 |
370 | --[[
371 | Weld, creates a WeldConstraint between two parts
372 | (Part0 and Part1).
373 | ]]
374 | function Functions.Weld(Part0: BasePart, Part1: BasePart): WeldConstraint
375 | -- Variable definitions
376 | local Weld = Instance.new("WeldConstraint")
377 |
378 | -- Update Part properties
379 | Part1.Anchored = false
380 |
381 | -- Update Weld properties
382 | Weld.Parent = Part1
383 | Weld.Part0 = Part0
384 | Weld.Part1 = Part1
385 |
386 | -- Export weld
387 | return Weld
388 | end
389 |
390 | --[[
391 | Adds a connection to a table that holds connections.
392 | ]]
393 | function Functions.Connect(Connection: RBXScriptConnection, Holder: { RBXScriptConnection })
394 | -- Update table
395 | table.insert(Holder, Connection)
396 | end
397 |
398 | --[[
399 | Destroys and disconnects all the connections
400 | in a table that holds connections.
401 | ]]
402 | function Functions.DisconnectAll(Holder: { RBXScriptConnection })
403 | -- Disconnect and destroy connections in Holder
404 | for Index, Connection: RBXScriptConnection in Holder do
405 | Connection:Disconnect()
406 | Holder[Index] = nil
407 | end
408 | end
409 |
410 | --[[
411 | Basic function used to replace the initial module methods,
412 | therefore avoiding errors after deletion of the module.
413 | ]]
414 | function Functions.Replacement()
415 | warn("BLOOD-ENGINE - Attempt to call a deleted function.")
416 | end
417 |
418 | return Functions
419 |
--------------------------------------------------------------------------------
/src/Operator.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | @ Description:
3 | This is the operator of the base system/class,
4 | it manages the functionality of the droplets,
5 | the events of the casts, the limit and such.
6 | ]]
7 |
8 | -- Variable definitions
9 | local ParentClass = script.Parent
10 | local Assets = ParentClass.Assets
11 |
12 | -- Asset definitions
13 | local Sounds = Assets.Sounds
14 | local Essentials = Assets.Essentials
15 | local Meshes = Assets.Meshes
16 |
17 | -- Sound definitions
18 | local EndFolder = Sounds.End:GetChildren()
19 | local StartFolder = Sounds.Start:GetChildren()
20 |
21 | -- Essential definitions
22 | local Functions = require(ParentClass.Functions)
23 | local PartCache = require(Essentials.PartCache)
24 | local Settings = require(ParentClass.Settings)
25 | local FastCast = require(Essentials.FastCast)
26 |
27 | -- Globals
28 | local Unpack = table.unpack
29 |
30 | -- Constants definition
31 | local TypeAttribute = "Type"
32 | local DecayAttribute = "Decaying"
33 | local ExpandAttribute = "Expanding"
34 | local MeshMap = {
35 | Default = Meshes.Droplet,
36 | Decal = Meshes.Decal,
37 | }
38 |
39 | -- Type definitions
40 | type Connections = { RBXScriptConnection }
41 |
42 | -- Class definition
43 | local Operator = {}
44 | Operator.__index = Operator
45 |
46 | --[[
47 | Class constructor, constructs the class
48 | including other properties/variables.
49 | ]]
50 | function Operator.new(Class)
51 | local self = setmetatable({
52 | Handler = Class.ActiveHandler,
53 | }, Operator)
54 |
55 | return self, self:Initialize(), self:InitializeCast()
56 | end
57 |
58 | --[[
59 | Immediately called after the construction of the class,
60 | defines properties/variables for after-construction
61 | ]]
62 | function Operator:Initialize()
63 | -- Variable definitions
64 | local Handler: Settings.Class = self.Handler
65 | local FolderName: string = Handler.FolderName
66 |
67 | -- Essential definitions
68 | local Type = Handler.Type
69 | local Limit = Handler.Limit
70 | local CastParams = Handler.RaycastParams
71 |
72 | local Folder = Functions.GetFolder(FolderName)
73 | local Object = Functions.GetDroplet(Handler.SplashName)
74 |
75 | -- Class definitions
76 | local Cache = PartCache.new(Object, Limit, Folder)
77 |
78 | -- Insert variables
79 | Functions.MultiInsert(self, {
80 | Registry = {},
81 | Connections = {},
82 |
83 | Droplet = Object,
84 | Cache = Cache,
85 | Container = Folder,
86 | Caster = FastCast.new(),
87 | Behavior = function()
88 | return Functions.SetupBehavior(Cache, CastParams)
89 | end,
90 | })
91 | end
92 |
93 | --[[
94 | The Cast-Setup, which is executed immediately
95 | following the Initialization of the class.
96 |
97 | It efficiently manages events
98 | associated with the Caster.
99 | ]]
100 | function Operator:InitializeCast()
101 | -- Self definitions
102 | local Connections: Connections = self.Connections
103 | local Caster: FastCast.Class = self.Caster
104 | local Handler: Settings.Class = self.Handler
105 | local Container: Folder = self.Container
106 |
107 | -- Event definitions
108 | local LengthChanged = Caster.LengthChanged
109 | local RayHit = Caster.RayHit
110 |
111 | -- Caster Listeners
112 | Functions.Connect(LengthChanged:Connect(function(_, Origin, Direction, Length, _, Object: BasePart)
113 | if not Object then
114 | return
115 | end
116 |
117 | -- 3D Definition
118 | local ObjectSize = Object.Size
119 | local ObjectLength = ObjectSize.Z / 2
120 |
121 | local Offset = CFrame.new(0, 0, -(Length - ObjectLength))
122 |
123 | local GoalCFrame = CFrame.new(Origin, Origin + Direction):ToWorldSpace(Offset)
124 |
125 | -- Update properties
126 | Object.CFrame = GoalCFrame
127 | end), Connections)
128 |
129 | Functions.Connect(RayHit:Connect(function(_, RaycastResult: RaycastResult, Velocity, Object: BasePart?)
130 | if not Object then
131 | return nil
132 | end
133 |
134 | -- Options definitions
135 | local RegistryData = self.Registry[Object] or Handler
136 | local Size = RegistryData.StartingSize
137 | local SizeRange = RegistryData.DefaultSize
138 | local YRange = RegistryData.YSize
139 | local Distance = RegistryData.Distance
140 | local Expansion = RegistryData.Expansion
141 | local IsDecal = RegistryData.Type == "Decal"
142 |
143 | -- Variable definitions
144 | local CastInstance = RaycastResult.Instance
145 | local Position = RaycastResult.Position
146 | local Normal = RaycastResult.Normal
147 |
148 | local VectorSize, SizeY = Functions.GetVector(SizeRange), Functions.NextNumber(table.unpack(YRange))
149 | local GoalSize = Functions.RefineVectors(IsDecal, Vector3.new(VectorSize.X, VectorSize.Y / 4, VectorSize.X))
150 |
151 | local GoalAngles = Functions.GetAngles(IsDecal, IsDecal)
152 | local GoalCFrame = Functions.GetCFrame(Position, Normal, IsDecal) * GoalAngles
153 |
154 | local ClosestPart = Functions.GetClosest(Object, Distance, Container)
155 |
156 | local ExpansionLogic = (
157 | Expansion
158 | and ClosestPart
159 | and not ClosestPart:GetAttribute(DecayAttribute)
160 | and not ClosestPart:GetAttribute(ExpandAttribute)
161 | and ClosestPart:GetAttribute(TypeAttribute) == RegistryData.Type
162 | )
163 |
164 | -- Clear the registry entry
165 | self.Registry[Object] = nil
166 |
167 | -- Evaluates if the droplet is close to another pool, if so, expand.
168 | if ExpansionLogic then
169 | self:Expanse(Object, ClosestPart, Velocity, GoalSize, RegistryData)
170 | return nil
171 | end
172 |
173 | -- Update properties
174 | Object.Anchored = true
175 | Object.Size = Size
176 | Object.CFrame = GoalCFrame
177 | Object.Transparency = Functions.NextNumber(Unpack(RegistryData.DefaultTransparency))
178 |
179 | --[[
180 | Transitions the droplet into a pool,
181 | then handles its later functionality.
182 | (Decay, Sounds, etc...)
183 | ]]
184 | Functions.CreateTween(Object, RegistryData.Tweens.Landed, { Size = GoalSize }):Play()
185 |
186 | self:HandleDroplet(Object, RegistryData)
187 | self:HitEffects(Object, Velocity, RegistryData)
188 | Functions.Weld(CastInstance, Object)
189 |
190 | return nil
191 | end), Connections)
192 | end
193 |
194 | --[[
195 | Destroys PartCache, FastCast,
196 | and all the droplets associated with this engine/operator.
197 | ]]
198 | function Operator:Destroy()
199 | -- Self definitions
200 | local Connections: Connections = self.Connections
201 | local Cache: PartCache.Class = self.Cache
202 | local Caster: FastCast.Class = self.Caster
203 | local Container: Folder = self.Container
204 |
205 | -- Destroy classes
206 | Cache:Dispose()
207 | table.clear(Caster)
208 |
209 | Functions.DisconnectAll(Connections)
210 | table.clone(Connections)
211 |
212 | -- Destroy main container
213 | if Container then
214 | Container:Destroy()
215 | end
216 |
217 | -- Null everything, making the operator unusable
218 | self.Connections = nil
219 | self.Container = nil
220 | self.Cache = nil
221 | self.Caster = nil
222 | end
223 |
224 | --[[
225 | Emitter, emits a certain amount of droplets,
226 | at a certain point of origin, with a certain given direction.
227 | ]]
228 | function Operator:Emit(Origin: Vector3, Direction: Vector3, Data: Settings.Class?)
229 | -- Class definitions
230 | local Caster: FastCast.Class = self.Caster
231 | local Behavior: FastCast.Behavior = self.Behavior
232 | local Cache: PartCache.Class = self.Cache
233 | local Handler: Settings.Class = self.Handler
234 |
235 | -- Create a clone of the default settings, and apply specific settings if provided
236 | local Clone = table.clone(Handler)
237 | Clone:UpdateSettings(Data or {})
238 | Data = Clone
239 |
240 | -- Variable definitions
241 | local IsDecal = Data.Type == "Decal"
242 | local DropletVelocity = Data.DropletVelocity
243 | local Velocity = Functions.NextNumber(Unpack(DropletVelocity)) * 10
244 |
245 | local RandomOffset = Data.RandomOffset
246 | local OffsetRange = Data.OffsetRange
247 | local Position = Functions.GetVector(OffsetRange) / 10
248 |
249 | -- Final definitions
250 | local FinalPosition = Origin + Vector3.new(Position.X, 0, Position.Z)
251 | local FinalStart = (RandomOffset and FinalPosition or Origin)
252 |
253 | if #Cache.Open <= 0 then
254 | return
255 | end
256 |
257 | -- Caster definitions, fire the caster with given arguments
258 | local ActiveDroplet = Caster:Fire(FinalStart, Direction, Velocity, Behavior)
259 |
260 | local RayInfo = ActiveDroplet.RayInfo
261 | local Droplet: MeshPart = RayInfo.CosmeticBulletObject
262 |
263 | -- Update the mesh's look and color
264 | Droplet:ApplyMesh(MeshMap[Data.Type])
265 | Droplet.Color = Data.DropletColor
266 |
267 | -- Assign the registry entry and update the attributes
268 | self.Registry[Droplet] = Data
269 | Droplet:SetAttribute(TypeAttribute, Data.Type)
270 | Droplet:SetAttribute(DecayAttribute, false)
271 | Droplet:SetAttribute(ExpandAttribute, false)
272 |
273 | -- Execute essential functions
274 | self:UpdateDroplet(Droplet, Data)
275 | Functions.PlaySound(Functions.GetRandom(StartFolder), Droplet)
276 | end
277 |
278 | --[[
279 | A small function, designed to update the properties
280 | of a recently emitted droplet.
281 | ]]
282 | function Operator:UpdateDroplet(Object: BasePart, Data: Settings.Class)
283 | -- Variable definitions
284 | local DropletTrail = Data.Trail
285 | local DropletVisible = Data.DropletVisible
286 | local IsDecal = Data.Type == "Decal"
287 |
288 | -- Object definitions
289 | local Trail = Object:FindFirstChildOfClass("Trail")
290 |
291 | -- Update Object properties
292 | Object.Transparency = DropletVisible and 0 or 1
293 | Trail.Enabled = DropletTrail
294 |
295 | -- Execute essential functions
296 | Functions.ApplyDecal(Object, IsDecal)
297 | end
298 |
299 | --[[
300 | Handles the given droplet/object after
301 | it landed on a surface.
302 | ]]
303 | function Operator:HandleDroplet(Object: BasePart, Data: Settings.Class)
304 | -- Object definitions
305 | local Trail = Object:FindFirstChildOfClass("Trail")
306 |
307 | -- Variable definitions
308 | local Tweens = Data.Tweens
309 | local DecayDelay = Data.DecayDelay
310 |
311 | local DecayInfo = Tweens.Decay
312 | local DecayTime = Functions.NextNumber(Unpack(DecayDelay))
313 |
314 | local ScaleDown = Data.ScaleDown
315 | local FinalSize = ScaleDown and Vector3.new(0.01, 0.01, 0.01) or Object.Size
316 |
317 | -- Tween definitions
318 | local DecayTween = Functions.CreateTween(Object, DecayInfo, { Transparency = 1, Size = FinalSize })
319 |
320 | -- Update Droplet properties
321 | Trail.Enabled = false
322 |
323 | -- Listeners
324 | DecayTween.Completed:Connect(function()
325 | DecayTween:Destroy()
326 | Object:SetAttribute("Decaying", nil)
327 | self:ReturnDroplet(Object)
328 | end)
329 |
330 | -- Reset the droplet after the given DecayDelay has passed
331 | task.delay(DecayTime, function()
332 | DecayTween:Play()
333 | Object:SetAttribute("Decaying", true)
334 | end)
335 | end
336 |
337 | --[[
338 | HitEffects, a sequence of effects to enhance
339 | the visuals of the droplet->pool
340 | ]]
341 | function Operator:HitEffects(Object, Velocity: Vector3, Data: Settings.Class)
342 | -- Variable definitions
343 | local SplashName = Data.SplashName
344 | local SplashAmount = Data.SplashAmount
345 | local SplashByVelocity = Data.SplashByVelocity
346 | local Divider = Data.VelocityDivider
347 | local IsDecal = Data.Type == "Decal"
348 |
349 | local Magnitude = Velocity.Magnitude
350 | local FinalVelocity = Magnitude / Divider
351 | local FinalAmount = (SplashByVelocity and FinalVelocity or Functions.NextNumber(Unpack(SplashAmount)))
352 | local Splash: Attachment = Object:FindFirstChild(SplashName)
353 |
354 | -- Execute essential functions
355 | Splash.Orientation = Vector3.new(0, 0, IsDecal and 0 or 180)
356 | Functions.PlaySound(Functions.GetRandom(EndFolder), Object)
357 | Functions.EmitParticles(Splash, FinalAmount)
358 | end
359 |
360 | --[[
361 | Simulates the pool expansion
362 | effect when a droplet is near
363 | a pool.
364 |
365 | It checks the distance between
366 | a threshold, then triggers changes
367 | on the droplet & pool.
368 | ]]
369 | function Operator:Expanse(
370 | Object: BasePart,
371 | ClosestPart: BasePart,
372 | Velocity: Vector3,
373 | Size: Vector3,
374 | Data: Settings.Class
375 | )
376 | -- Variable definitions
377 | local Divider = Data.ExpanseDivider
378 | local MaximumSize = Data.MaximumSize
379 | local IsDecal = Data.Type == "Decal"
380 |
381 | -- Info definitions
382 | local Tweens = Data.Tweens
383 | local Expand = Tweens.Expand
384 |
385 | -- Value definitions
386 | local PoolSize = ClosestPart.Size
387 | local FinalVelocity = Velocity / 20
388 | local GoalSize = Vector3.new(Size.X, Size.Y / Divider, Size.Z) / Divider
389 |
390 | local FirstSize = Functions.RefineVectors(
391 | IsDecal,
392 | Vector3.new(PoolSize.X - FinalVelocity.Z, PoolSize.Y + FinalVelocity.Y, PoolSize.Z - FinalVelocity.Z)
393 | )
394 |
395 | local LastSize = Vector3.new(PoolSize.X, PoolSize.Y, PoolSize.Z) + GoalSize
396 |
397 | local FinalSize = (LastSize.X < MaximumSize and LastSize or PoolSize)
398 |
399 | -- Update properties
400 | ClosestPart:SetAttribute("Expanding", true)
401 | ClosestPart.Size = FirstSize
402 |
403 | -- Transition to Expanded size
404 | local Tween = Functions.CreateTween(ClosestPart, Expand, { Size = FinalSize })
405 |
406 | Tween:Play()
407 | Tween.Completed:Connect(function()
408 | ClosestPart:SetAttribute("Expanding", nil)
409 | Tween:Destroy()
410 | end)
411 |
412 | -- Execute essential functions
413 | Functions.PlaySound(Functions.GetRandom(EndFolder), ClosestPart)
414 | self:ReturnDroplet(Object)
415 | end
416 |
417 | --[[
418 | Resets the given droplet/pool,
419 | then returns it to the Cache.
420 | ]]
421 | function Operator:ReturnDroplet(Object: Instance)
422 | -- Self definitions
423 | local Cache: PartCache.Class = self.Cache
424 | local Template: Instance = self.Droplet
425 |
426 | -- Execute essential functions
427 | Functions.ResetDroplet(Object, Template)
428 | Cache:ReturnPart(Object) -- Ignore, ReturnPart exists
429 | end
430 |
431 | -- Exports the class
432 | export type Class = typeof(Operator.new(...))
433 |
434 | return Operator
435 |
--------------------------------------------------------------------------------
/src/Settings.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | @ Description:
3 | A class that handles settings,
4 | a group of keys that have an assigned value.
5 | ]]
6 |
7 | -- Class definition
8 | local Settings = {}
9 | Settings.__index = Settings
10 |
11 | --[[
12 | Class constructor, constructs the class
13 | including other properties/variables.
14 | ]]
15 | function Settings.new(Data: {})
16 | local self = setmetatable({
17 | FolderName = "Droplets", -- Specifies the name of the folder containing the droplets.
18 | Type = "Default", -- Defines the droplet type. It can be either "Default" (Sphere) or "Decal".
19 | Limit = 500, -- Sets the maximum number of droplets that can be created.
20 | Filter = {}, -- An array/table of instances that should be ignored during droplet collision.
21 |
22 | YSize = { 0.1, 0.175 }, -- Specifices the range of the thickness/flatness/depth of the pool. Lesser is flatter.
23 | DefaultSize = { 0.4, 0.7 }, -- Specifies the default size range of a pool.
24 | DefaultTransparency = { 0.3, 0.4 }, -- Specifies the default transparency range of a pool.
25 | StartingSize = Vector3.new(0.1, 0.3, 0.1), -- Sets the initial size of the droplets upon landing.
26 | ScaleDown = true, -- Determines whether the pool should scale down when decaying.
27 |
28 | DropletDelay = { 0.01, 0.03 }, -- Sets the delay between emitting droplets in a loop (for the EmitAmount method).
29 | DropletVelocity = { 1, 2 }, -- Controls the velocity of the emitted droplet.
30 | DropletVisible = false, -- Determines if the droplet is visible upon emission.
31 | DropletColor = Color3.fromRGB(103, 0, 0), -- Determines the color of the emitted droplet.
32 |
33 | RandomOffset = true, -- Determines whether a droplet should spawn at a random offset from a given position.
34 | OffsetRange = { -5, 5 }, -- Specifies the offset range for the position vectors.
35 |
36 | SplashName = "Impact", -- The name of the attachment that releases particles on surface contact.
37 | SplashAmount = { 5, 10 }, -- Sets the number of particles to emit upon impact.
38 | SplashByVelocity = true, -- If true, sets the number of particles based on the velocity of the droplet.
39 | VelocityDivider = 8, -- Controls how much the velocity can affect the splash amount, Higher values reduce the effect.
40 |
41 | Expansion = true, -- Determines whether a pool can expand when a droplet lands on it.
42 | Distance = 0.2, -- Sets the distance (in studs) within which the droplet should check for nearby pools
43 | ExpanseDivider = 3, -- Controls how much a pool's size can increase. Higher values reduce the increase.
44 | MaximumSize = 0.7, -- Sets the maximum size a pool can reach.
45 |
46 | Trail = true, -- Controls the visibility of the trail during droplet emission.
47 | DecayDelay = { 10, 15 }, -- Sets the delay before the droplet decays and recycles
48 |
49 | -- Contains all the tweens used by the module
50 | Tweens = {
51 | Landed = TweenInfo.new(0.5, Enum.EasingStyle.Cubic), -- Used for when a droplet has landed on a surface.
52 | Decay = TweenInfo.new(1, Enum.EasingStyle.Cubic), -- Used for when a droplet is decaying.
53 | Expand = TweenInfo.new(0.5, Enum.EasingStyle.Cubic), -- Used for when a droplet is expanding (Pool Expansion).
54 | },
55 | }, Settings)
56 |
57 | -- Fill the default settings with values from the Data array
58 | for Setting, Value in Data do
59 | if Setting == "Tweens" then
60 | for Tween, Info in Value do
61 | self.Tweens[Tween] = Info
62 | end
63 |
64 | continue
65 | end
66 |
67 | self[Setting] = Value
68 | end
69 |
70 | return self, self:CreateParams()
71 | end
72 |
73 | --[[
74 | Updates settings with values from the provided array.
75 | ]]
76 | function Settings:UpdateSettings(Data: {})
77 | -- Variable definitions
78 | local Filter = self.Filter
79 | local Params = self.RaycastParams
80 |
81 | for Setting, Value in Data do
82 | self[Setting] = Value
83 | end
84 |
85 | -- Update Param properties
86 | Params.FilterDescendantsInstances = Filter
87 | end
88 |
89 | --[[
90 | Manages the instantiation of the RaycastParams
91 | aswell as the configuration of the filter.
92 | ]]
93 | function Settings:CreateParams()
94 | -- Variable definitions
95 | local Filter = self.Filter
96 | local Params = RaycastParams.new()
97 |
98 | -- Update Params properties
99 | Params.FilterType = Enum.RaycastFilterType.Exclude
100 | Params.FilterDescendantsInstances = Filter
101 |
102 | -- Assign Params as a self value
103 | self.RaycastParams = Params
104 | end
105 |
106 | -- Exports the class and its type
107 | export type Class = typeof(Settings.new(...))
108 |
109 | return Settings
110 |
--------------------------------------------------------------------------------
/src/init.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | @ Writer: @Smileeiles
3 | @ Version: v1.1.3
4 | @ Description:
5 | A droplet emitter system,
6 | used to emit droplets from a specified origin point.
7 |
8 | These droplets are then given a velocity,
9 | and upon landing on a surface, transform into pools.
10 |
11 | This process can be customized to suit various needs and genres.
12 | ]]
13 |
14 | -- Essential definitions
15 | local Operator = require(script.Operator)
16 | local Settings = require(script.Settings)
17 | local Functions = require(script.Functions)
18 |
19 | -- Globals
20 | local Unpack = table.unpack
21 |
22 | -- Class definition
23 | local BloodEngine = {}
24 | BloodEngine.__index = BloodEngine
25 |
26 | --[[
27 | Class constructor, constructs the class
28 | including other properties/variables.
29 | ]]
30 | function BloodEngine.new(Data: Settings.Class)
31 | local self = setmetatable({}, BloodEngine)
32 | return self, self:Initialize(Data)
33 | end
34 |
35 | --[[
36 | Immediately called after the construction of the class,
37 | defines properties/variables for after-construction
38 | ]]
39 | function BloodEngine:Initialize(Data: {})
40 | Functions.MultiInsert(self, {
41 | ActiveHandler = Settings.new(Data or {}),
42 | ActiveEngine = function()
43 | return Operator.new(self)
44 | end,
45 | })
46 | end
47 |
48 | --[[
49 | Emitter, emits droplets based on given amount,
50 | origin & direction.
51 |
52 | This is utilized when you prefer
53 | not to create a loop just for the
54 | purpose of emitting a few droplets.
55 | ]]
56 | function BloodEngine:EmitAmount(Origin: Vector3 | BasePart, Direction: Vector3, Amount: number, Data: Settings.Class?)
57 | -- Class definitions
58 | local Handler: Settings.Class = self.ActiveHandler
59 |
60 | -- Variable definitions
61 | local DropletDelay = Handler.DropletDelay
62 |
63 | for _ = 1, Amount, 1 do
64 | -- Define variables for later use
65 | local DelayTime = Functions.NextNumber(Unpack(DropletDelay))
66 |
67 | -- Emit a droplet in the specified direction & origin
68 | self:Emit(Origin, Direction, Data)
69 |
70 | -- Delays the next droplet to be emitted
71 | task.wait(DelayTime)
72 | end
73 | end
74 |
75 | --[[
76 | EmitOnce, a variant of the Emit method; emits a single droplet.
77 | Unlike Emit, which uses a loop to emit multiple droplets,
78 | EmitOnce only emits one droplet per call.
79 |
80 | This is useful when you want to control the emission
81 | loop externally.
82 | ]]
83 | function BloodEngine:Emit(Origin: Vector3 | BasePart, Direction: Vector3, Data: Settings.Class?)
84 | -- Class definitions
85 | local Engine: Operator.Class = self.ActiveEngine
86 |
87 | -- Variable definitions
88 | Origin = typeof(Origin) == "Instance" and Origin.Position or Origin
89 | Direction = Direction or Functions.GetVector({ -10, 10 }) / 10
90 |
91 | -- Emit a single droplet
92 | Engine:Emit(Origin, Direction, Data)
93 | end
94 |
95 | --[[
96 | GetSettings, returns all the settings of the
97 | current class instance.
98 |
99 | Use this function when you want to access
100 | the settings for external handling of the system.
101 | ]]
102 | function BloodEngine:GetSettings(): Settings.Class
103 | -- Class definitions
104 | local Handler: Settings.Class = self.ActiveHandler
105 |
106 | -- Export settings
107 | return Handler
108 | end
109 |
110 | --[[
111 | UpdateSettings, updates the settings of the
112 | current class instance.
113 |
114 | It uses the `Handler:UpdateSettings()`, which
115 | uses the given `Data` array/table to update individual settings.
116 | ]]
117 | function BloodEngine:UpdateSettings(Data: Settings.Class)
118 | -- Class definitions
119 | local Handler: Settings.Class = self.ActiveHandler
120 |
121 | -- Update the settings
122 | Handler:UpdateSettings(Data)
123 | end
124 |
125 | --[[
126 | Destroy, destroys anything associated
127 | with the settings/handler and the operator/engine.
128 |
129 | Use this function when there is no longer a use
130 | for the created engine. (Like when a character dies, etc.)
131 | ]]
132 | function BloodEngine:Destroy()
133 | -- Class definitions
134 | local Handler: Settings.Class = self.ActiveHandler
135 | local Engine: Operator.Class = self.ActiveEngine
136 |
137 | -- Destroy the handler & engine
138 | self.Handler = nil
139 | Engine:Destroy()
140 | self.Engine = nil
141 |
142 | -- Nullify/replace all the Engine methods
143 | self.UpdateSettings = Functions.Replacement
144 | self.GetSettings = Functions.Replacement
145 |
146 | self.EmitAmount = Functions.Replacement
147 | self.Emit = Functions.Replacement
148 | end
149 |
150 | -- Exports the class
151 | return BloodEngine
152 |
--------------------------------------------------------------------------------
/wally.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rotntake/blood-engine"
3 | version = "0.1.3"
4 | realm = "shared"
5 | registry = "https://github.com/upliftgames/wally-index"
6 | licence = "MIT"
7 | authors = ["rotntake"]
8 |
9 | description = "A droplet emitter system."
10 |
11 | exclude = ["**"]
12 | include = ["src", "src/*", "default.project.json", "wally.toml"]
13 |
14 | [dev-dependencies]
15 |
16 | [dependencies]
17 |
--------------------------------------------------------------------------------