Dodge/Dash/Roll Action

Started by Max, May 28, 2018, 05:27:27 PM

Previous topic - Next topic
May 28, 2018, 05:27:27 PM Last Edit: May 28, 2018, 08:13:54 PM by Max
So, I've got a fairly robust script for allowing the hero to dodge/dash around when you press the action key- basically rolling from Minish Cap, except you're invincible during it so you can dodge through enemies. If you wanted to adapt this to rolling like many zelda games have, I'm pretty sure this would work fine, just take out the hero:set_invincible() line.

Here's a youtube video of it in action

So, it's working pretty well, except it currently allows the hero to dash over holes (I'm not entirely sure why this is). I have two options for how I want to deal with that. Either I want to stop this from letting you dash over holes, or I want to allow it, but prevent the "falling into hole" sound and animation from playing. Right now, the hero will dash over a hole, but after touching the pit, the hero's animation will be "falling" until she finishes the dash and the "falling" sound effect will play.

Anybody have any ideas how to deal with this?


Anyway, here's the code. If you'd like to use it right now (be aware of the issue with holes I mentioned.) you'll need an animation for the hero called "dash", replace the item on line 6 with whatever item you want to require to allow this, and you can put this code in your game_manager script or wherever. That's where I handle all of my key press events, personally.

Code (lua) Select

function game:on_key_pressed(key, modifiers)
  if key == "space" then
    local effect = game:get_command_effect("action")
    local state = hero:get_state()
    --make sure the conditions are right to dash and we're not doing something else or don't have the item that allows this
    if game:has_item("dandelion_charm") and effect == nil and state == "free" and game:is_suspended() == false then
      local dir = hero:get_direction()
      --convert the direction we just got into radians so straight movement can use it
      if dir == 1 then dir = (math.pi/2) elseif dir == 2 then dir = math.pi elseif dir == 3 then dir = (3*math.pi/2) end
      local m = sol.movement.create("straight")
      m:set_angle(dir)
      m:set_speed(325)
      m:set_max_distance(75) --you may want to make this into a variable so you could upgrade the dash
      m:set_smooth(true)
      hero:freeze()
--      hero:set_blinking(true, 200) --this is just a stylistic choice. It makes it move obvious that you can dash through enemies if you enable it, but in my situation the dashing animation didn't read as clearly.
      hero:get_sprite():set_animation("dash", function() hero:get_sprite():set_animation("walking") end)
      sol.audio.play_sound("swim") --this is a placeholder sound effect that everyone should have, you'd want to change it probably
      m:start(hero, function() hero:unfreeze() end)
      hero:set_invincible(true, 300) --you may want to experiment with this number, which is how long the hero is invincible for
      function m:on_obstacle_reached()
        hero:unfreeze()
      end
    end
end

You have 2 options:
1) Stop the movement on holes and make the hero fall on them.
2) Modify the ground below the hero during the dash so that the falling animation does not happen. This can be done with a custom entity under the hero, as I do in my custom jump script.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

May 28, 2018, 08:39:50 PM #2 Last Edit: May 28, 2018, 08:48:04 PM by Max
Awesome! Since I have several areas where pits are supposed to be obstacles and I don't want to redesign them, I'll try to stop the movement. I'm calling hero:on_state_changed(state), and if the hero's state == falling, then stopping the movement. This seems to work, hopefully it doesn't give me problems if I decide to use hero:on_state_changed(state) somewhere else!

I tried using the multievents script to format this, but I didn't know how to get the argument passed on with that syntax.

Code (lua) Select

hero:register_event("on_state_changed", function()
  if state == "falling" then --some code
  end
end)

With this syntax, I don't know how to pass on the state argument



Anyway, this works, even with pits, as long as you're not calling hero:on_state_changed() anywhere else!

Code (lua) Select

function game:on_key_pressed(key, modifiers)
  if key == "space" then
    local effect = game:get_command_effect("action")
    local state = hero:get_state()
    --make sure the conditions are right to dash and we're not doing something else or don't have the item that allows this
    if game:has_item("dandelion_charm") and effect == nil and state == "free" and game:is_suspended() == false then
      local dir = hero:get_direction()
      --convert the direction we just got into radians so straight movement can use it
      if dir == 1 then dir = (math.pi/2) elseif dir == 2 then dir = math.pi elseif dir == 3 then dir = (3*math.pi/2) end
      local m = sol.movement.create("straight")
      m:set_angle(dir)
      m:set_speed(325)
      m:set_max_distance(75) --you may want to make this into a variable so you could upgrade the dash
      m:set_smooth(true)
      hero:freeze()
--      hero:set_blinking(true, 200) --this is just a stylistic choice. It makes it move obvious that you can dash through enemies if you enable it, but in my situation the dashing animation didn't read as clearly.
      hero:get_sprite():set_animation("dash", function() hero:get_sprite():set_animation("walking") end)
      sol.audio.play_sound("swim") --this is a placeholder sound effect that everyone should have, you'd want to change it probably
      m:start(hero, function() hero:unfreeze() end)
      hero:set_invincible(true, 300) --you may want to experiment with this number, which is how long the hero is invincible for
      function m:on_obstacle_reached()
        hero:unfreeze()
      end
--this is the new code to stop the dash from going over holes.
      function hero:on_state_changed(state)
        if state == "falling" then m:stop() end
      end
    end
end


Thanks for the help!

May 28, 2018, 10:30:22 PM #3 Last Edit: May 28, 2018, 10:40:42 PM by llamazing
Quote from: Max on May 28, 2018, 08:39:50 PM
Code (lua) Select

hero:register_event("on_state_changed", function()
  if state == "falling" then --some code
  end
end)

With this syntax, I don't know how to pass on the state argument
In the multi_events script, all the arguments that would have been passed to the on_state_changed function will also be passed to your callback function.

The original function is function hero:on_state_changed(state), which is equivalent to function hero.on_state_changed(self, state). So state is actually the second argument of the function:
Code (lua) Select

hero:register_event("on_state_changed", function(self, state)
  if state == "falling" then --some code
  end
end)


Yes, you can use the event hero.on_state_changed. But i'd personally do it in a different way, with a timer each millisecond to check the ground below the hero during the dash movement. But both approaches are ok.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

This is awesome as I'd wanted to include dashing in my project but wasn't sure where to begin. I actually like that it crosses gaps as I'm a big Hyper Light Drifter fan, but I understand why you'd want to be able to turn that off. Great job, Max!

Quotea timer each millisecond

Isn't that very heavy on performance ?!

Not really, because that timer would only exist during the dash movement.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I got this working in my project over the weekend and it feels pretty nice: https://youtu.be/pLQduAnrbgI

One side effect I noticed is that the shield sprite appears to stay in the walking animation during the dash, instead of using an alternate dash animation. I added a blank dashing animation to my shield sprite and it definitely wasn't being called. Not the end of the world, but it's something people might have to work out depending on the poses in their hero animations.

I later added a cooldown timer to mine by changing a few lines, just so you can't just spam the dash constantly. It's pretty basic but here's what I did if it's helpful to anyone:

Code ( lua) Select

-- Add this somewhere OUTSIDE the "game:on_key_pressed" function (I put it right under game:start):

local can_dash = true

-- Then add it to the initial if/then statement:

if game:has_item("dandelion_charm") and effect == nil and state == "free" and game:is_suspended() == false and can_dash == true then

-- add this to the m:start like so:

      m:start(hero, function()
        hero:unfreeze()
        can_dash = false
        sol.timer.start(hero, 500, function()
          can_dash = true
        end)
      end)

-- and do the same to your on_obstacle_reached function:

function m:on_obstacle_reached()
        hero:unfreeze()
        sol.timer.start(hero, 500, function()
          can_dash = true
        end)
      end

-- obviously you can change "500" to any timing you want. You could also make the timing longer or shorter for on_obstacle_reached to either punish or forgive the player for slamming into a wall.


Eventually I'd like to make it use MP, convert it to an assignable item, and polish up the animations... but either way, it's great to have dash functionality in Solarus!

That looks great! I like your big sprites, how's that going?

I was also thinking about implementing a cooldown timer the same way, so nice! I don't have magic or multiple items in my game (never figured out an item selection menu and just decided to roll with it), so I never bothered pursuing those options, but those are both great ideas, especially magic consumption. Another good option might be having a very short dash available at the start, and upgrading it throughout the game to be faster, longer, have a shorter cooldown, use less magic, etc., and designing that to be a fundamental part of your game.

I think whether or not you have the dash as its own item would have a lot to do with how you intend the player to use it in combat. Is combat designed around the player being able to dodge enemy attacks, or is this just an add-on? I've found (to me) that Link to the Past's combat isn't amazing. I image it was great for the time, and it's functional if enemies have a wide variation of different movements, attacks, patterns, weaknesses, but it's never great (maybe some bosses might reach that level). Hopefully, with abilities like dodging, an active shield (like Link's Awakening or Minish Cap), and different enemy behaviors, we can make combat more engaging for a modern audience. Side note, I think the game Blossom Tales went a long way toward more engaging combat just by not freezing the hero when they're attacking.

Quote from: Diarandor on May 29, 2018, 01:41:55 AM
Yes, you can use the event hero.on_state_changed. But i'd personally do it in a different way, with a timer each millisecond to check the ground below the hero during the dash movement. But both approac
hes are ok.

Wouldn't hero:on_position_changed() works on this case ? It should be better to check the ground that way ?

Something like that

Code (lua) Select
-- local variable that detect if we are dashing (mostly used in on_key_pressed())
local dashing = false

hero:register_event("on_position_changed", function()
  if dashing then
    local x, y, layer = hero:get_position()

    -- Check ground bellow code
  end
end


Also, little hint, if you want to use the action key to "dash" if possible, do like so

Code (lua) Select
if key == game:get_value("_keyboard_action") 

That way, if you rebind the action key, the dash action will also be rebinded

An important detail is that you should not use the normal position of the hero but the ground position.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Quote from: Max on June 03, 2018, 10:05:28 PM
That looks great! I like your big sprites, how's that going?

Thanks! The big sprites are fun but there's lot of hit detection bugs to work out. I haven't set correct sizes for enemies yet, and I haven't figured out how to change the true launch "height" for hero projectiles like arrows and boomerangs, so I faked it by lowering the sprite origin, so of course weird stuff happens. Still though, there are fewer problem than you'd think considering how much of the engine is built around 16x16.

Quote from: Max on June 03, 2018, 10:05:28 PMI think whether or not you have the dash as its own item would have a lot to do with how you intend the player to use it in combat. Is combat designed around the player being able to dodge enemy attacks, or is this just an add-on? I've found (to me) that Link to the Past's combat isn't amazing. I image it was great for the time, and it's functional if enemies have a wide variation of different movements, attacks, patterns, weaknesses, but it's never great (maybe some bosses might reach that level). Hopefully, with abilities like dodging, an active shield (like Link's Awakening or Minish Cap), and different enemy behaviors, we can make combat more engaging for a modern audience. Side note, I think the game Blossom Tales went a long way toward more engaging combat just by not freezing the hero when they're attacking.

Yeah I totally agree. I loved ALTTP when I was a kid, but as I grew older I began to feel like it's a little too simple and forgiving... even combat in Zelda 1 and 2 is a lot more challenging IMO. There's a lot of different ways I'd like to build on the formula, though I know it will take time to implement everything. That's one of the reasons I wanted to at least implement a cooldown ASAP... until my AI is as challenging as i.e. Hyper Light Drifter, being able to dash constantly will likely feel OP and make things too easy.

I love the fact in Link's Awakening that almost everything is an assignable item, including stuff like the shield, gauntlets, and boots which were previously passive abilities. Being able to assign and "use" an item is inherently more fun and makes you appreciate it more, and it forces the player to make tough decisions about which items they want equipped at a given time. Your equipment menu also pretty much becomes your keybinding menu at that point, which potentially streamlines the configuration process.

That being said though, as you say it all depends on the style of game you're going for. For example if your game requires a ton of jumping, then at some point having a dedicated jump button as in Beyond Oasis or Landstalker is preferable to an assignable item like Zelda's feather. At that point being able to unassign such an important command could become a frustrating n00b trap rather than an interesting equipment choice. The same would be true of a crouch command if you need to frequently duck to avoid high attacks or kill short enemies, and of course if the player needs to dash away from enemy attacks constantly, it may not make sense to let players unassign their dash.

However for the moment, I haven't made any progress creating an assignable equipment menu either, so it's kind of a moot point... my dash command will definitely be a dedicated key for the foreseeable future.  ;)

Quote from: MetalZelda on June 03, 2018, 10:14:45 PM
Also, little hint, if you want to use the action key to "dash" if possible, do like so

Code (lua) Select
if key == game:get_value("_keyboard_action") 

That way, if you rebind the action key, the dash action will also be rebinded

That's a great observation! Thanks, changing that in my game right away, haha.


Quote
Wouldn't hero:on_position_changed() works on this case ? It should be better to check the ground that way ?

I agree, that'd be a good way. Then you'd probably want to use hero:get_ground_below() to check to see if you're over a hole or water or lava- I only addressed holes in my script because in my game, you never gain the ability to swim and I don't have any lava, but upon further introspection I can see how these issues would also need to be addressed by other developers using this code, lol :p

Exactly, you should use hero:get_ground_position() and not hero:get_position() if you do things in that way.
In any case, I will give a remark on the advantages/disadvantages of both approaches:

1) If you register that function in on_position_changed to check your dash state, as you did, that code will be called always (and many times unnecessarily) when the hero changes position. This may be faster than a timer, but it's also being called when you are not dashing, whenever you move the hero.

2) The second approach, with the timer, would only be used when you are dashing, and you can destroy the timer after the dash. If you think that calling the timer once per millisecond is not optimal, you can call the timer once each 10 milliseconds instead (but be careful, because the ground may change between 2 callbacks of the timer, although this is not a problem if you check the ground again when the dash has finished and independently of the timer). This approach is slower but only during the dash, the rest of the time your code is faster.

I don't think there is really a noticeable difference in speed between approaches 1) and 2). For instance, in my rain/snow/hail scripts, I am updating a screen-sized surface each 5 or 10 milliseconds (depending on the mode), and the game runs as fast as a lightning. The choice is just a matter of personal preference.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."