Author Topic: Classic Tektite Random Jumping AI  (Read 1065 times)

Bagu

  • Newbie
  • *
  • Posts: 26
    • View Profile
Classic Tektite Random Jumping AI
« on: June 09, 2018, 08:21:21 pm »
I always liked the random jumping of tektites in Zelda 1 and ALTTP, and was a little disappointed by the tektites in Return of the Hylian which simply walked at the player. Thus once I figured out how to set up random jumping for some other enemies I figured I'd share this extremely basic and simple example of Nintendo tektite behavior.

This also would work for replicating Bits/Bots from Zelda 2 or Zols and Gels from Zelda 1.

example video: https://youtu.be/HGkoJe9JqEc

Code: Lua
  1. local enemy = ...
  2.  
  3. local choices = {"pause","jump","jump","jump"}
  4. -- these are functions the enemy will choose from randomly. I added extra copies of the jump function so he would move 75% of the time instead of 50/50. However, you could create more functions and place each one in the list only once.
  5.  
  6. function enemy:on_created()
  7.   enemy:set_life(3)
  8.   enemy:set_damage(2)
  9.   enemy:create_sprite("enemies/" .. enemy:get_breed())
  10. end
  11.  
  12. function enemy:on_restarted()
  13.     enemy[ choices[math.random( 1, #choices )] ](enemy) --this line calls a random function from the "choices" table at the top
  14. end
  15.  
  16. function enemy:pause()
  17.   local sprite = enemy:get_sprite()
  18.   --sprite:set_animation("idle")
  19. -- uncomment the line above if you want to use a custom pause animation, otherwise it'll use "walking"
  20.   enemy:stop_movement()
  21.   sol.timer.start(enemy, math.random(1, 2000), function() --the timer lasts randomly between 1 and 2000 ms. You can change the minimum and maximum to taste.
  22.     enemy[ choices[math.random( 1, #choices )] ](enemy) --again picks a random function from the choices table, you could also just call enemy:restart
  23.   end)
  24. end
  25.  
  26. function enemy:jump()
  27.   local sprite = enemy:get_sprite()
  28.   enemy:stop_movement()
  29.   --sprite:set_animation("jumping")
  30. -- uncomment the line above if you want to use a custom jump animation, otherwise it'll keep using "walking"
  31.   local movement = sol.movement.create("jump")
  32.   movement:set_direction8(math.random(0, 7)) -- picks a random direction to jump in
  33.   movement:set_distance(math.random(24, 64)) -- sets a distance between 24 and 64px
  34.   movement:start(enemy)
  35.   sol.timer.start(enemy, 800, function()  -- 800ms is the typical time for the enemy to land from a 64px jump.
  36.     enemy[ choices[math.random( 1, #choices )] ](enemy)
  37.   end)
  38.  

I probably should have used movement:on_finished() instead of a timer to close the jump function, because if you use distances greater than 64 the jump begins to get cut-off early and it looked bad.

Art-wise, if you use something like Return of the Hylian's tektite sprite, it'll look a little weird because there's a shadow drawn into the animations. Thus the shadow floats through the air with the character as he jumps, instead of staying on the ground. The easiest solution is to simply erase any shadows from your animation frames, though this will make it visually ambiguous whether the character is jumping through the air or sliding across the ground. For the best results, you'd want to use a custom entity or something to place a separate shadow sprite under the character.

Special thanks to llamazing for helping me sort out the random function selection call!
« Last Edit: June 10, 2018, 06:21:41 pm by Bagu »

Max

  • Full Member
  • ***
  • Posts: 174
    • View Profile
Re: Classic Tektite Random Jumping AI
« Reply #1 on: June 09, 2018, 09:29:54 pm »
Pretty cool! If you figure out how to display a shadow when something is jumping, let me know- I've tried to avoid using the jump movement because it's so hard to understand what's happening when the engine doesn't draw a shadow.

Question though- your random function thing is elegant, and I'm going to try to understand it because I don't quote yet. But my question is, would something like this work?:

Code: Lua
  1. function enemy:on_restarted()
  2. local action = math.random(1, 4)
  3. if action >2 then enemy:pause() else enemy:jump()
  4. end
  5.  


This seems simpler to me, but perhaps it's because I'm still at a pretty basic level of understanding tables and stuff in lua. Although, I guess yours is a more adaptable system, so if you were, for example, programming a game's boss and he had ten different attacks, you wouldn't need to do an "if/then" 10 times.

Bagu

  • Newbie
  • *
  • Posts: 26
    • View Profile
Re: Classic Tektite Random Jumping AI
« Reply #2 on: June 09, 2018, 10:02:42 pm »
I'm not very good at lua yet either!  I tried to replicate several different methods of randomization I googled and failed utterly at anything beyond printing strings until llamazing fixed my code. However, if you're interested in investigating, here's a couple examples:

https://wiki.garrysmod.com/page/table/Random
https://forums.coronalabs.com/topic/39315-how-to-iteratepick-randomly-from-associative-arrays/


The main reason I used the weighting method I did is because I lifted it from other scripts of mine with many unique functions. I like starting with this one because if I decide to add more functions I can just replace some of the duplicates in the list.

Your suggested method looks like it would work, and could be an effective basic means of weighting things. When I tried to research various methods of weighting random selections in lua, most were extremely complex (here's a good example) so I gave up and stuck to what I had working.

llamazing

  • Full Member
  • ***
  • Posts: 149
    • View Profile
Re: Classic Tektite Random Jumping AI
« Reply #3 on: June 10, 2018, 03:25:22 am »
I didn't understand what you were trying to do when I fixed your code. Now that I see your full code, something like this would be better:
Code: Lua
  1.     local enemy = ...
  2.      
  3.     local choices = {
  4.       pause = 25,
  5.       jump = 75,
  6.     }
  7.     -- these are functions the enemy will choose from randomly proportional to the value associated with each one
  8.    
  9.     local random_max = 0 --sum of all choice values, calculate once when script is first run (in this example, radom_max = 25 + 75 = 100)
  10.     for _,weight in pairs(choices) do random_max = random_max + weight end
  11.    
  12.     --this function could be moved to a different script for common utility functions
  13.     local function random_choice()
  14.         local choice_value = math.random(1, random_max) --choose random number between 1 and random_max
  15.        
  16.         --step through list of choices to find which choice correspondes to the randomly chosen value
  17.         local cumulative = 0 --sum of current choice entry plus all previous
  18.         for choice,weight in pairs(choices) do
  19.                 cumulative = cumulative + weight
  20.                 if choice_value <= cumulative then
  21.                         return choice --current entry corresponds to the randomly chosen value
  22.                 end
  23.         end
  24.        
  25.         --should have returned a value before getting to this line, as contingency returns some choice (undefined which one)
  26.         --you may prefer to throw an error here instead
  27.         local choice = next(choices)
  28.         return choice
  29.     end
  30.    
  31.     function enemy:random_action()
  32.         self[random_choice()](self) --this line calls a random function from the "choices" table at the top
  33.     end
  34.    
  35.     function enemy:on_restarted()
  36.         self:random_action()
  37.     end
I didn't include the full script here. You'll want to replace lines 22 & 36 of your original script with self:random_action() like in the enemy:on_restarted() function.

This is basically equivalent to the sample code posted by Max (yes your code works) except it allows the user to edit the values in the choices table (or add new entries) without having to change the rest of the script. Also note that the sum of the values in the choices table don't need to be 100 either. Giving pause a value of 1 and jump a value of 3 would be equivalent.

Though the values do need to be integers. It would be easy enough to alter it to work with decimal values too if that's what you wanted.

EDIT: code fixed to add the missing parentheses mentioned below
« Last Edit: June 11, 2018, 07:25:11 am by llamazing »

Bagu

  • Newbie
  • *
  • Posts: 26
    • View Profile
Re: Classic Tektite Random Jumping AI
« Reply #4 on: June 10, 2018, 06:43:25 pm »
llamazing, your version looks awesome, but when I try it, self[random_choice](self) is calling a nil value. Maybe I did something wrong, but I didn't think I could mess much up with what little I added to it:

Code: Lua
  1. local enemy = ...
  2.  
  3. function enemy:on_created()
  4.   enemy:set_life(3)
  5.   enemy:set_damage(2)
  6.   enemy:create_sprite("enemies/" .. enemy:get_breed())
  7. end
  8.  
  9. local choices = {
  10.   pause = 25,
  11.   jump = 75,
  12. }
  13.  
  14. -- these are functions the enemy will choose from randomly proportional to the value associated with each one
  15.    
  16. local random_max = 0 --sum of all choice values, calculate once when script is first run (in this example, radom_max = 25 + 75 = 100)
  17. for _,weight in pairs(choices) do random_max = random_max + weight end
  18.    
  19. --this function could be moved to a different script for common utility functions
  20. local function random_choice()
  21. local choice_value = math.random(1, random_max) --choose random number between 1 and random_max
  22.        
  23.  --step through list of choices to find which choice correspondes to the randomly chosen value
  24. local cumulative = 0 --sum of current choice entry plus all previous
  25. for choice,weight in pairs(choices) do
  26.   cumulative = cumulative + weight
  27.   if choice_value <= cumulative then
  28.     return choice --current entry corresponds to the randomly chosen value
  29.    end
  30. end
  31.        
  32. --should have returned a value before getting to this line, as contingency returns some choice (undefined which one)
  33. --you may prefer to throw an error here instead
  34. local choice = next(choices)
  35.         return choice
  36. end
  37.    
  38. function enemy:random_action()
  39.   self[random_choice](self) --this line calls a random function from the "choices" table at the top
  40. end
  41.    
  42. function enemy:on_restarted()
  43.   self:random_action()
  44. end
  45.  
  46. function enemy:pause()
  47.   local sprite = enemy:get_sprite()
  48.   --sprite:set_animation("idle")
  49.   enemy:stop_movement()
  50.   sol.timer.start(enemy, math.random(1, 2000), function()
  51.     self:random_action()
  52.   end)
  53. end
  54.  
  55. function enemy:jump()
  56.   local sprite = enemy:get_sprite()
  57.   enemy:stop_movement()
  58.   --sprite:set_animation("jumping")
  59.   local movement = sol.movement.create("jump")
  60.   movement:set_direction8(math.random(0, 7))
  61.   movement:set_distance(math.random(24, 64))
  62.   movement:start(enemy)
  63.   sol.timer.start(enemy, 800, function()
  64.     self:random_action()
  65.   end)
  66. end

llamazing

  • Full Member
  • ***
  • Posts: 149
    • View Profile
Re: Classic Tektite Random Jumping AI
« Reply #5 on: June 10, 2018, 08:39:54 pm »
Oops, I forgot a set of parentheses.

Change self[random_choice](self) to self[random_choice()](self) and it should work.

Bagu

  • Newbie
  • *
  • Posts: 26
    • View Profile
Re: Classic Tektite Random Jumping AI
« Reply #6 on: June 11, 2018, 04:25:50 am »
Yes, it works perfectly now, thanks! I also sorted out movement:on_finished for closing the jump function.