2D Fighting Game Animation Issues

Sorry if this post has a long description, this is an unconventional game so I want to make sure I describe everything as best I can. A concise description of the problem is at the end.


I am trying to make a client-sided singleplayer 2D fighting game as a fun little project on roblox. Up to this point it has been smooth and fun to make, but I am facing a problem with getting animations to work.

(AttInpt means attack input. The game is planned to be a direct rip off of street fighter, but have simple directional inputs like smash bros where space is for normals and Z is for special attacks. As of right now I am trying to get a proof of concept done before all the fancy polish stuff and only space works with neutral attacks.)

Above is the diagram I made for how I think the animation system should work. There are two kinds of animations: looped (orange) and non-looped / finishing (green). I have gotten the looped animations to work, but my non-looped animations (specifically the attack animations) have strange problems.

Here is a part of my script. This has a couple variables, the PlayAnim() function, and my Update() function which runs every frame. I apologize in advance if the code isn’t the most clean.

local speed = 16 -- Speed in studs per sec
local myHeight = 3 -- How far the center of the character is above the ground
local gravity = GameFolder.Gravity.Value

local grounded = true
local xVelocity = 0
local yVelocity = 0

local curState = nil
local curAnim = nil
local curDir = nil
local animRunning = false -- Keeps track of when a time-based animation is running (i.e. attacks, jump start animations, anything that isn't looped)

-- Stops all animations and plays the input animation. If the animation is looped, return an event that fires when the animation is complete.
local function PlayAnim(animName, fadeTime, is_looped)
	local finalName = animName .. ":" .. curDir
	
	if finalName == curAnim then return	end
	curAnim = finalName
	
	StopAnimations(fadeTime)
	loadedAnims[finalName]:Play(fadeTime)
	
	if is_looped ~= true then
		coroutine.resume(coroutine.create(function()
			loadedAnims[finalName].Stopped:Wait()

			AnimEnded:Fire()
		end))
		
		return AnimEnded.Event
	end
end


-- Runs every frame
local function Update(deltaTime)
	local nextState = state.Idle -- The anticipated state after all inputs have been considered (Defaulted to Idle)
	
	-- Update my direction
	if grounded and Enemy then
		if hrp.Position.X >= Enemy.PrimaryPart.Position.X and curDir ~= direction.R then
			curDir = direction.R

		elseif hrp.Position.X < Enemy.PrimaryPart.Position.X and curDir ~= direction.L then
			curDir = direction.L

		end
	end
	
	-- Ground Inputs
	if grounded and animRunning == false then
		xVelocity = 0 -- Frictions stops ground motion, this is realism at its finest
		
		if UserInputService:IsKeyDown(Enum.KeyCode.Right) then
			xVelocity += speed
		end
		if UserInputService:IsKeyDown(Enum.KeyCode.Left) then
			xVelocity -= speed
		end
		
		if xVelocity ~= 0 then
			nextState = state.Walking
		end
		
		if UserInputService:IsKeyDown(Enum.KeyCode.Space) then
			nextState = state.Attack
		end
		
		if nextState ~= state.Attack then
			if UserInputService:IsKeyDown(Enum.KeyCode.Up) then
				grounded = false
				yVelocity = 0.5
				nextState = state.Jumping
			end
		end
		
	end
	
	-- Update position based on velocity
	hrp.CFrame = CFrame.new(hrp.Position.X - (xVelocity * deltaTime), hrp.Position.Y, hrp.Position.Z)

	if not grounded then
		yVelocity -= gravity * deltaTime

		if -yVelocity > GetDistanceFromGround() then
			grounded = true
			script.Land:Play()

			hrp.CFrame = CFrame.new(hrp.Position.X, myHeight + GameFolder.GroundLevel.Value, hrp.Position.Z)
		else
			hrp.CFrame = CFrame.new(hrp.Position.X, hrp.Position.Y + yVelocity, hrp.Position.Z)
		end
	end
	
	curState = nextState
	
	-- Update animations
	if curState == state.Idle and animRunning == false then		
		if grounded then
			PlayAnim("Idle", _, true)
		else
			PlayAnim("AerialIdle", 0, true)
		end
		
	elseif curState == state.Walking and animRunning == false then
		if (curDir == direction.R and xVelocity < 0) or (curDir == direction.L and xVelocity > 0)  then
			PlayAnim("Retreat", _, true)
		else
			PlayAnim("Advance", _, true)
		end
		
	elseif curState == state.Jumping and animRunning == false then
		print("Jump request received)")
		local jumpAnim = nil
		if (curDir == direction.R and xVelocity < 0) or (curDir == direction.L and xVelocity > 0)  then
			jumpAnim = "BackJump"
		else
			jumpAnim = "ForwardJump"
		end
		
		animRunning = true
		
		local finish = PlayAnim(jumpAnim, 0)
		finish:wait()
		print(curState)

		animRunning = false	
		
		
	elseif curState == state.Attack and animRunning == false then
		animRunning = true

		local finish = PlayAnim("NPunch", 0)
		finish:wait()
		print(curState)

		animRunning = false	
	end
	
end

RunService.RenderStepped:Connect(Update)

What am I trying to achieve: I am trying to make it so when you press the space bar, the attack animation is triggered and the character is locked into this action. He cannot move or jump until after his attack has finished.

The problem:



In the video, right before the script throws an error I hold the spacebar down.

If you hold down the spacebar so an attack is triggered immediately after the last one ends, the event in the PlayAnim() function returns as nil and the script throws an error and stops running.

If I didn’t explain this error properly enough or if I need to send the rest of the code (or even the place file), please comment, but I have been stuck on this problem for awhile and I hope someone is willing to help, thanks in advance!

1 Like

its saying that index nil if wait, you make somenthing wrong in wait? try use task.wait

1 Like

and for air attack you need detect if player jump and after use somenthing that look like debounce
you first need to make input that detect if player jump or is on air, that detect if make the debounce get false and you make if debounce == false then animation idk
sorry for bad english :clown_face:

1 Like

This is a different kind of wait, I am using :wait() on an event to wait until the event fires. The situation with wait() and task.wait() is something completely different.

IT WAS SO SIMPLE AND I HAVE BEEN STUCK FOR A MONTH AAAAAAAAAAAAAH!

ok so the reason why it would bug out when you held down the punch button is because of this one line of code from the PlayAnim function

if finalName == curAnim then return end

This was supposed to be a debounce to prevent idle animations from starting over and over again constantly but it messed up the “play-once” animations that needed to play on repeat as soon as they finished.

I really hope I can finish the rest of the game faster because I was stuck on that one tiny problem for a month, but thank y’all for your time XD

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.