Actions
- run every game step
- nothing special, more like an animation finite state machine
- those "ChangeAction" events are the transitions between states/actions
- in Unity, it's represented similarly.
-->the animation system has a bag of variables that are used to decide when wand what to transition to if a requirement succeeds (--> ChangeActions)
- Early on, I noticed that Actions never(?) contain any timer events (Asynchronous, Synchronous timers), but made plenty use of infinite loops + loop rests.
-->this backs up the idea that Actions are ran once per game step,
-->anyone who uses timers in there are probably misusing the event and are pausing the action for an entire animation frame instead of a single game step (loop rest)
-Since actions are just FSMs, you can create an entire alternative moveset if you can add actions. --within memory limits ofc.
-gives control of an Action at the per game step
Subaction
- run once per animation step, sets an animation
- really, just gives control of an Action/animation code on a per animation frame step. (very useful as everyone who PSA's should know by now)
- I used to have an "AllowInterrupt" "event" in my game too. Straight to the point, "AllowInterrupt" (do you guys still name it that?) is just a fancy word for "specific change actions".
--All current ChangeActions on my side are cached and checked at the end of a game step.
--when an Action changes, any currently existing ChangeActions events are thrown away
-Again, Actions are nothing special but a FSM -but at the PSA-like level, they're very easy to use (for those who program, a FSM doesn't scale well in code. However, the way Brawl does it for Actions, they scale fine.)
-Subactions also aren't anything special but a small script
uhh if I think of anything else that might be useful, I'll be sure to add to this. These are just things I learned about when I was using PSA docs for guidance on how to setup a PSA-like system. AFAIK, both systems turned out similar.
This is pretty much advertising a game I don't work on anymore... but I dunno it might be useful to see how my side turned out?
*Note*, anywhere you see coroutine.yield(runStatus.Running) within an infinite loop, it's similar to a LoopRest in PSA
*WARNING* raw and uneditted. This is a debug/test moveset using Lua
Actions
local mitActions = function(_events)
local events = _events
events.lightPunchKeyPress = function()
return events:isKeyPress(keys.X)
end
events.jumpKeyPressed = function()
return events:isKeyPressed(keys.Z)
end
events.crouchKeyPressed = function()
return events:isKeyPressed(keys.Down)
end
events.upKeyPressed = function()
return events:isKeyPressed(keys.Up)
end
events.heavy_UpKeyPressed = function()
return events:isKeyPressed(keys.Up) and events:isKeyPressed(keys.C)
end
events.horizontalKeysPressed = function()
return events:areKeysPressed(events.horizontalKeys)
end
events.keyPressedTowardsWall = function()
local wallSide = events:getWallSide()
return (events:isKeyPressed(keys.Left) and wallSide == -1) or
(events:isKeyPressed(keys.Right) and wallSide == 1)
end
--add change actions for: attack11,jumpBegin,crouch,walk,heavyUpAttackBegin
events.changeActions_Ground0 = function()
events:changeAction(events.actions.walk,events.horizontalKeysPressed)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.jumpBegin,events.jumpkeyPressed)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.crouch,events.crouchKeyPressed)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.attack11,events.lightPunchKeyPress)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
end
--add change actions for: airN,airF,wallSlide,hammerUp
events.changeActions_Air0 = function()
events:changeAction(events.actions.walk,events.horizontalKeysPressed)
events:changeAction(events.actions.walk,events.horizontalKeysPressed)
events:changeAction(events.actions.walk,events.horizontalKeysPressed)
events:changeAction(events.actions.walk,events.horizontalKeysPressed)
end
--empty func for now,replace with specifics later.
events.allowInterrupt = function()end
events.actions =
{
idle = action(
{
id = 0,
method = function()
--yield once to stay in sync with how game handles coroutines internally
coroutine.yield()
events:changeAction(events.actions.walk,events.horizontalKeysPressed)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.jumpBegin,events.jumpKeyPressed)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.attack11,events.lightPunchKeyPress)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.heavyAttack_Up_Begin,events.heavy_UpKeyPressed)
events:changeAction(events.actions.fall,events.isFalling)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:setSubaction(events.subactions.idle)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
walk = action(
{
id = 1,
method = function()
coroutine.yield()
events:changeAction(events.actions.idle,events.horizontalKeysPressed,true)
events:changeAction(events.actions.fall,events.isFalling)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.jumpBegin,events.jumpKeyPressed)
events:addRequirement(events.onGround)
events:changeAction(events.actions.attack11,events.lightPunchKeyPress)
events:addRequirement(events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.heavyAttack_Up_Begin,events.heavy_UpKeyPressed)
events:setSubaction(events.subactions.walk)
while true do
events:moveRelative()
coroutine.yield(runStatus.Running)
end
end
}),
jumpBegin = action(
{
id = 5,
method = function()
coroutine.yield()
events:offsetRelative(vector2(0,-3))
events:applyJumpImpulse()
events:setGroundAirTime(0)
--events:ignoreGroundCollisions()
--events:playSound(3,0.5)
events:setSubaction(events.subactions.jumpBegin)
coroutine.yield(runStatus.Running)
events:setShouldBeInAir(true)
events:setInAir(true)
events:changeAction(events.actions.fall,events.isFalling)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.rise,events.isRising)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
rise = action(
{
id = 6,
method = function()
coroutine.yield()
events:changeAction(events.actions.idle,events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.fall,events.isFalling)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.airN,events.lightPunchKeyPress)
events:addRequirement(events.horizontalKeysPressed,true)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.airF,events.lightPunchKeyPress)
events:addRequirement(events.isKeyPressed_Forward)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.heavyAttack_Up_Begin,events.heavy_UpKeyPressed)
events:setSubaction(events.subactions.jumpLoop)
while true do
events:move()
--try to rise
if(events:isRiseTimeLessThanAllowed() and
events:isKeyPressed(events.jumpKeys)) then
events:setVelocityY(-150)
end
coroutine.yield(runStatus.Running)
end
end
}),
fall = action(
{
id = 7,
method = function()
coroutine.yield()
events:changeAction(events.actions.idle,events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.rise,events.isRising)
events:addRequirement(events.inAir)
events:changeAction(events.actions.wallSlide,events.isFalling)
events:addRequirement(events.isTouchingWall)
events:addRequirement(events.keyPressedTowardsWall)
events:changeAction(events.actions.airN,events.lightPunchKeyPress)
events:addRequirement(events.horizontalKeysPressed,true)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.airF,events.lightPunchKeyPress)
events:addRequirement(events.isKeyPressed_Forward)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.heavyAttack_Up_Begin,events.heavy_UpKeyPressed)
events:setSubaction(events.subactions.fall)
while true do
events:move()
coroutine.yield(runStatus.Running)
end
end
}),
attack11 = action(
{
id = 10,
method = function()
coroutine.yield()
events:changeAction(events.actions.idle,events.isSubactionNull)
events:setSubaction(events.subactions.attack11)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
attack12 = action(
{
id = 11,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.attack12)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
attack13 = action(
{
id = 12,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.attack13)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
heavyAttack_Idle = action(
{
id = 13,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.heavyAttack_Idle)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
heavyAttack_Up_Begin = action(
{
id = 14,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.heavyAttack_Up_Begin)
events:changeAction(events.actions.heavyAttack_Up_Loop,events.isSubactionNull)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
heavyAttack_Up_Loop = action(
{
id = 15,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.heavyAttack_Up_Loop)
events:changeAction(events.actions.heavyAttack_Up_End,events.upKeyPressed,true)
while true do
events:moveRelative()
coroutine.yield(runStatus.Running)
end
end
}),
heavyAttack_Up_End = action(
{
id = 16,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.heavyAttack_Up_End)
events:changeAction(events.actions.idle,events.isSubactionNull)
while true do
events:moveRelative()
coroutine.yield(runStatus.Running)
end
end
}),
heavyAttack_AirN = action(
{
id = 20,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.heavyAttack_AirN)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
airN = action(
{
id = 21,
method = function()
coroutine.yield()
events:changeAction(events.actions.airNFall,events.isSubactionNull)
events:setSubaction(events.subactions.airN)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
airNFall = action(
{
id = 22,
method = function()
coroutine.yield()
events:changeAction(events.actions.idle,events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.rise,events.isRising)
events:addRequirement(events.inAir)
events:changeAction(events.actions.wallSlide,events.isFalling)
events:addRequirement(events.isTouchingWall)
events:addRequirement(events.keyPressedTowardsWall)
events:changeAction(events.actions.airN,events.lightPunchKeyPress)
events:addRequirement(events.horizontalKeysPressed,true)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:changeAction(events.actions.airF,events.lightPunchKeyPress)
events:addRequirement(events.isKeyPressed_Forward)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:setSubaction(events.subactions.airNFall)
while true do
events:move()
coroutine.yield(runStatus.Running)
end
end
}),
airF = action(
{
id = 23,
method = function()
coroutine.yield()
events:changeAction(events.actions.fall,events.isSubactionNull)
events:setSubaction(events.subactions.airF)
while true do
coroutine.yield(runStatus.Running)
end
end
}),
wallSlide = action(
{
id = 30,
method = function()
coroutine.yield()
events:changeAction(events.actions.idle,events.onGround)
events:addRequirement(events.shouldBeOnGround)
events:changeAction(events.actions.fall,events.isFalling)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
events:addRequirement(events.keyPressedTowardsWall,true)
events:changeAction(events.actions.wallJump,events.jumpKeyPressed)
events:setSubaction(events.subactions.wallSlide)
while true do
events:setVelocity(vector2(0,15))
coroutine.yield(runStatus.Running)
end
end
}),
wallJump = action(
{
id = 31,
method = function()
coroutine.yield()
events:setSubaction(events.subactions.wallJump)
coroutine.yield(runStatus.Running)
events:changeAction(events.actions.fall,events.isFalling)
events:addRequirement(events.inAir)
events:addRequirement(events.shouldBeInAir)
while true do
--events:move()
--try to rise
--if(events:isRiseTimeLessThanAllowed() and
-- events:isKeyPressed(events.jumpKeys)) then
-- events:setVelocityY(-150)
--end
coroutine.yield(runStatus.Running)
end
end
}),
}
end
mitActions(...)
Subactions
local create = function(_events)
local events = _events
--to prevent creating strings everytime used
local bones =
{
HandL = 'HandL',
HandR = 'HandR',
FootL = 'FootL',
FootR = 'FootR',
Hammer = 'Hammer'
}
events.subactions =
{
idle = subaction(
{
id = 0,
animationID = 0,
looped = true,
functions =
{
main = function()
-- first yield gets consumed by coroutine start
coroutine.yield()
end
}
}),
walk = subaction(
{
id = 1,
animationID = 1,
looped = true,
functions =
{
main = function()
coroutine.yield()
events:allowInterrupt()
events:playSound(0,0.1)
events:synchronous(12)
events:playSound(0,0.1)
events:synchronous(11)
end
}
}),
jumpBegin = subaction(
{
id = 5,
animationID = 2,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:addParticleRelative(particle(13,1,color.White,vector2(0,10),vector2(-20,0),false,true))
events:addParticleRelative(particle(13,1,color.White,vector2(0,10),vector2(20,0),false, true))
events:allowInterrupt()
end
}
}),
jumpLoop = subaction(
{
id = 6,
animationID = 2,
looped = true,
functions =
{
main = function()
coroutine.yield()
events:allowInterrupt()
end
}
}),
fall = subaction(
{
id = 7,
animationID = 3,
looped = true,
functions =
{
main = function()
coroutine.yield()
events:allowInterrupt()
end
}
}),
attack11 =subaction(
{
id = 10,
animationID = 9,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:synchronous(4)
events:playSound(5,1)
events:offensiveCollision(0, true, bones.HandL, 1, 2, 180,0, 3, 4, vector2(-1, 0), 3, 1,0,5)
events:offensiveCollision(1, true, bones.HandL, 1, 2, 135,0, 4, 4, vector2(-1, 0), 8, .2,0,5)
events:offensiveCollision(2, true, bones.HandL, 1, 2, 0,0, 1, 1, vector2(-1, 0), 10, 0,0,5)
events:asynchronous(5)
for i=0,10 do
events:synchronous(1)
if(events:isKeyPress(events.punchKeys)) then
events:terminateAllCollisions()
events:setSubaction(events.subactions.attack12)
end
end
events:asynchronous(16)
events:terminateAllCollisions()
end
}
}),
attack12 =subaction(
{
id = 11,
animationID = 10,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:asynchronous(4)
events:playSound(6,1)
events:synchronous(1)
events:offensiveCollision( 0, true, bones.HandR, 1, 2, 270,0, 5, 5,vector2(-1, 0), 10, 1)
for i = 0,10 do
events:synchronous(1)
if(events:isKeyPress(events.punchKeys)) then
events:terminateAllCollisions()
events:setSubaction(events.subactions.attack13)
return coroutine.yield(runStatus.Success)
end
end
events:asynchronous(16)
events:terminateAllCollisions()
end
}
}),
attack13 =subaction(
{
id = 12,
animationID = 11,
looped = false,
functions =
{
main =function()
coroutine.yield()
events:asynchronous(4)
events:playSound(7)
events:offensiveCollision(0, true, bones.FootR, 1, 2, 45,0, 4, 10,vector2(-1, 0), 10, 2)
events:asynchronous(16)
events:terminateAllCollisions()
end
}
}),
heavyAttack_Idle =subaction(
{
id = 13,
animationID = 4,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:playSound(3,1)
end
}
}),
heavyAttack_Up_Begin =subaction(
{
id = 14,
animationID = 13,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:playSound(28,.1)
events:asynchronous(15)
if(events:inAir()) then
events:playSound(15,.2)
events:setVelocityY(-200)
end
end,
gfx = function()
coroutine.yield()
--BIFF
local _particle =particle(32,.5,color.White)
_particle:SetInitialMatrix(-15,-25,0,.5,.5,0,0)
_particle:SetPosition(0,10,0,-100)
_particle:SetRotation(3.14/-2,3.14)
_particle:SetScale(1.5,1,0,0)
_particle.FadeOut=true
events:addParticleRelative(_particle)
events:asynchronous(15)
--MARTY
_particle.AnimationIndex = 34
_particle:SetInitialMatrix(15,-25,0,.5,.5,0,0)
_particle:SetRotation(3.14 / 2,-3.14)
events:addParticleRelative(_particle)
if(events:inAir()) then
--smoke puff
_particle.AnimationIndex = 11
_particle:SetInitialMatrix(0,5,0,.5,.5,0,0)
_particle:SetPosition(0,20,0,-60)
_particle:SetRotation(0,0)
_particle:SetScale(3,3,-10,-10)
events:addParticleRelative(_particle);
_particle.LifeTime = 1
--leaves
for i=1,4 do
_particle.AnimationIndex = i
_particle:SetInitialMatrix(0,5,0,1,1,0,0)
_particle:SetPosition(random.NextDouble(-30,30),random.NextDouble(-10,30),0,50)
_particle:SetRotation(6.14,-6.14)
_particle:SetScale(0,0,0,0)
events:addParticleRelative(_particle)
end
end
end
}
}),
heavyAttack_Up_Loop =subaction(
{
id = 15,
animationID = 14,
looped = true,
functions =
{
main = function()
coroutine.yield()
local offset = vector2(0,-1)
events:offensiveCollision(0, true, bones.Hammer, 0, 0, 135,0, 0, 10, offset, 9, 1)
events:offensiveCollision(1, true, nil, 0, 0, 135,0, 0, 10,vector2.Zero,10,1)-- vector2(0,-5), 6, 1)
events:synchronous(15)
events:terminateAllCollisions()
end,
gfx = function()
coroutine.yield()
local _particle = particle(0,.5,color.White)
_particle.LifeTime = .5
for i=0,2 do
if(events:onGround()) then
--smoke trail
_particle.AnimationIndex = 12
_particle:SetInitialMatrix(0,5,0,.5,.5,0,0)
_particle:SetScale(-.5,-.5,0,0)
events:addParticleRelative(_particle)
--V particles
_particle.AnimationIndex = 4
_particle:SetInitialMatrix(0,10,0,.5,.5,0,0)
_particle:SetPosition(-60,-60,0,0)
_particle:SetScale(5,5,0,0)
events:addParticleRelative(_particle)
_particle:SetPosition(60,-60,0,0)
events:addParticleRelative(_particle)
end
events:synchronous(5)
end
end
,sfx = function()
coroutine.yield()
events:playSound(27,.1)
if(events:onGround()) then
events:playSound(20,.01)
end
end
}
}),
heavyAttack_Up_End = subaction(
{
id = 16,
animationID = 15,
looped = false,
functions =
{
main = function()
coroutine.yield()
end
}
}),
heavyAttack_AirN = subaction(
{
id = 20,
animationID = 22,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:playSound(10,1)
for i = 0,18 do
if(events:shouldBeOnGround()) then
events:setSubactionPassTime(events.subactions.heavyAttack_Idle,events:getCurrentAnimationFrameTime())
return coroutine.yield(runStatus.Success)
end
events:synchronous(1)
end
end
}
}),
airN = subaction(
{
id = 21,
animationID = 18,
looped = false,
functions =
{
main = function()
coroutine.yield()
local offset = vector2(0,0)
events:synchronous(4)
--events:playSound(4,1)
events:enableConstantMomentum()
events:setConstantMomentum(vector2.Zero)
events:asynchronous(9)
events:offensiveCollision(0, true, bones.HandL, 0, 0, 135,0, 0, 10, offset, 10, 1)
events:offensiveCollision(1, true, bones.HandR, 0, 0, 45,0, 0, 10, offset, 10, 1)
events:offensiveCollision(2, true, bones.FootL, 0, 0, 225,0, 0, 10, offset, 10, 1)
events:offensiveCollision(3, true, bones.FootR, 0, 0, 315,0, 0, 10, offset, 10, 1)
events:synchronous(3)
events:terminateAllCollisions()
events:offensiveCollision(0, true, bones.HandL, 0, 0, 135,0, 0, 8, offset, 8, 1)
events:offensiveCollision(1, true, bones.HandR, 0, 0, 45 ,0, 0, 8, offset, 8, 1)
events:offensiveCollision(2, true, bones.FootL, 0, 0, 225,0, 0, 8, offset, 8, 1)
events:offensiveCollision(3, true, bones.FootR, 0, 0, 315,0, 0, 8, offset, 8, 1)
events:synchronous(15)
events:terminateAllCollisions()
events:synchronous(5)
events:disableConstantMomentum()
events:allowInterrupt()
end
}
}),
airNFall = subaction(
{
id = 22,
animationID = 19,
looped = false,
functions =
{
main = function()
coroutine.yield()
end
}
}),
airF =subaction(
{
id = 23,
animationID = 17,
looped = false,
functions =
{
main = function()
coroutine.yield()
events:asynchronous(4)
events:applyImpulse(vector2(0,-50))
events:offensiveCollision( 0,true, bones.HandL, 1, 1, 0,0, 50, 8,vector2(0, -4), 10, 3)
--events:playSound(5)
events:asynchronous(13)
events:terminateAllCollisions()
events:allowInterrupt()
end
}
}),
wallSlide =subaction(
{
id = 30,
animationID = 16,
looped = true,
functions =
{
main = function()
coroutine.yield()
--I don't want this subaction being called every frame because it plays sound
local rnd = random.NextDouble(0,10)
local _particle = nil
if(rnd >= 5) then
local life = random.NextDouble(.3,2)
_particle = particle(11,life,color.White)
_particle:SetInitialMatrix(10,0,0,.7,.7,0,0)
_particle:SetPosition(0,-5,0,0)
_particle:SetRotation(random.NextDouble(0,6),0)
_particle:SetScale(-1/life,-1/life,0,0)
_particle.DeathOnAnimationEnd = true
_particle.FadeOut = true
events:addParticleRelative(_particle)
end
rnd = random.NextDouble(0,10)
if(rnd >= 8) then
local life = random.NextDouble(.3,2)
_particle = particle(11,life,color.White)
_particle:SetInitialMatrix(10,10,0,.7,.7,0,0)
_particle:SetPosition(0,-10,0,0)
_particle:SetRotation(random.NextDouble(0,6),0)
_particle:SetScale(-1/life,-1/life,0,0)
_particle.DeathOnAnimationEnd = true
_particle.FadeOut = true
events:addParticleRelative(_particle)
end
events:allowInterrupt()
end
}
}),
wallJump =subaction(
{
id = 31,
animationID = 2,
looped = false,
functions =
{
main = function()
coroutine.yield()
--events:playSound(13,1)
events:addParticleRelative(particle(11,1,color.White,vector2(10, 0),vector2(0,-20),true, true))
events:addParticleRelative(particle(11,1,color.White,vector2(10, 0),vector2(0, 20),true, true))
local wallNormal = events.context.PlacementData.BodySensorNormalCollection.AverageNormal
local jumpDirection = vector2.Transform(wallNormal,matrix.CreateRotationZ(-mathHelper.PiOver4))
jumpDirection.X = jumpDirection.X * 300
jumpDirection.Y = jumpDirection.Y * 300
if(jumpDirection.Y > 0) then
jumpDirection.Y = jumpDirection.Y * -1.0
end
local levelMask = events.context.BasicPhysicsData.LevelCollisionMask
if(events:waitForPermission()) then
events:offsetRelative( wallNormal)
levelMask:ApplyLinearImpulse(jumpDirection)
end
events:setFacingDirection(events:getWallSide() * -1)
end
}
}),
}
end
return create(...)
You can check my dead devlog for gifs what this code actually does . If this is against the rules, I have no problem if this post is deleted or editted.