For the past year or so, I have been writing a custom framework to support complicated attacks for advanced combat purposes. The goal of the framework is to make development less tedious by abstracting away user input, cleanup, error checks (all effects are coroutines, and so the attack will gracefully fail in the event of an error), and the small minutiae of roblox development by providing an easy sandbox environment with prewritten code. The framework currently supports stuff like Preemption (i.e. stopping an attack in the middle), chargeable attacks (where the user can hold a button to vary the damage inflicted), and also supports things like projectiles and combos. I wanted to know how I could possibly improve upon the syntax to make it clearer, as I was thinking of potentially open-sourcing the framework at some point.
For reference, here is a clip of the attack in action - You can see the preemption in action here, where the pickaxe animation is automatically preempted if the pickaxe hits a rock.
And here is the code:
--1) Setup variables
-- Attack name should be unique within each attack context
local ATTACK_NAME = "Mining"
local HitBox = script.Parent.Parent:WaitForChild("Parts"):WaitForChild("Hitbox")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ASSETS = ReplicatedStorage:WaitForChild("Assets")
local ANIMATIONS = ASSETS:WaitForChild("Animations")
local require = require(ReplicatedStorage:WaitForChild("Nevermore")) -- I use Quenty's Nevermore module loader to not have to worry about specific module paths in my game. Give it a try!
local RunService = game:GetService("RunService")
local IsClient = RunService:IsClient()
local IsServer = RunService:IsServer()
local Attack = require("Attack")
local AttackPhase = require("AttackPhase")
local Effect = require("Effect")
--2) Initializer function (should instantiate a new instance of the attack, given owner)
return function(owner, network_channel, input_name)
local Attack = Attack:New(owner, ATTACK_NAME)
local StartPhase = AttackPhase:New(owner, "START") -- start phase shown for illustrative effect (not strictly needed - you can have as many phases as you'd like)
local ActionPhase = AttackPhase:New(owner, "ACTION")
local animation_effect = require("AnimationEffect"):New(owner, ANIMATIONS:FindFirstChild("Mining",true))
animation_effect:SetPreemptive(true) -- allows the animation to end the phase early upon yielding
local HitEffect = require("RaycastHitboxHitEffect"):New(owner, HitBox)
HitEffect:WithPlayerHitEffectFunction(function(effect, plr, part, pos, normal) -- callback for when the raycast hit detection effect detects a player (in this example, the ores are coded as players for code reuse)
effect:DealDamage(plr, 20)
effect:PlaySound("Mining", {Position = pos})
effect:EmitParticleAtPosition("MineParticle", pos)
animation_effect:Yield() -- preempts the running animation_effect
end)
animation_effect:BindKeyframeEffect(HitEffect, "Swing") -- starts running the hit effect at the "Swing" keyframe of the animation
local whoosh = Effect:New(owner)
whoosh:SetFunction(function(effect)
effect:PlaySound("Whoosh", {Position = HitBox.Position})
end)
animation_effect:BindKeyframeEffect(whoosh, "Swing") -- play a "Whoosh" sound when the pickaxe is swung
ActionPhase:WithEffect(animation_effect)
local EndPhase = AttackPhase:New(owner, "END")
Attack:AddLinearPhasePathway({StartPhase, ActionPhase, EndPhase}) -- chains the phases, so the order is StartPhase -> ActionPhase -> EndPhase, in that order
return Attack
end
The framework is designed with three major classes in mind:
- An Attack is a collection of AttackPhases, where the phases are states in a finite state machine (so one could write attacks that vary their logic based on branching conditions).
- An AttackPhase is a collection of effects, where each effect runs in a separate coroutine. Attack phases are preemptible, meaning they can be interrupted without any side-effects, and should cleanup all effects upon being unloaded.
- An effect is a class that provides a sandbox environment that provides basic abstractions for the user to build upon. Each effect has a function, which is passed a sandbox environment as a parameter (named effect). Note that you can define whatever you want in this function - the sandbox methods are provided only to make life a bit easier
The workflow for someone who wants to create an attack would be to
- define AttackPhases
- define effects for those phases (certain types of effects are allowed to have sub-effects, as with
animation_effect:BindKeyframeEffect
) - add phases to the attack, in whatever order/conditions that you wish
The main questions I have are:
- Is it obvious what the code is doing, just by looking at the names of the functions?
- Is there any way to structure it better?
- Are there any glaring issues you foresee with this (or otherwise, would you find this useful)?