Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.


Topics - alexgleason

Pages: [1]
1
Development / Multi-events as the default?
« on: October 17, 2018, 08:41:47 pm »
I was just having this moment of great realization for why Lua is a powerful programming language, then I hit a wall again.

To give an example, I have a script which does get_metatable("map"):register_event("on_started", ...)

The problem is, Solarus Quest Editor creates every map script with an empty on_started defined. So I'll need to edit all my maps and change it to map:register_event("on_started", ...) in order to make both functions run.

Basically, I'm trying to have events in "layers". Every map will always call the same "on_started" function in the metatable, but it will also call the "on_started" function of the specific map itself.

So I'm wondering if there should be a push towards using multi-event functions in Quest Editor instead. Eg, creating a new map might look like this:

Code: Lua
  1. -- Lua script of map test_map.
  2. -- This script is executed every time the hero enters this map.
  3.  
  4. -- Feel free to modify the code below.
  5. -- You can add more events and remove the ones you don't need.
  6.  
  7. -- See the Solarus Lua API documentation:
  8. -- http://www.solarus-games.org/doc/latest
  9. require("scripts/multi_events.lua")
  10.  
  11. local map = ...
  12. local game = map:get_game()
  13.  
  14. -- Event called at initialization time, as soon as this map becomes is loaded.
  15. function map:register_event("on_started", function()
  16.  
  17.   -- You can initialize the movement and sprites of various
  18.   -- map entities here.
  19. end)
  20.  
  21. -- Event called after the opening transition effect of the map,
  22. -- that is, when the player takes control of the hero.
  23. function map:register_event("on_opening_transition_finished", function()
  24.  
  25. end)

This would offer a lot more flexibility, because you could disable the default behavior by switching to an empty function. This seems like a more common use-case. I can't think of a use-case where it's better off the way it currently is.

The only real issue I see is that Quest Editor would have to assume you have a multi_events script. So then it makes me think... maybe package that into the engine itself? I mean, it's an essential tool for quest development.

2
Your scripts / Script to save the state of hundreds of objects
« on: October 17, 2018, 08:49:55 am »
Video of this in action: https://vimeo.com/295527145

This is basically solving the problem of Banjo Kazooie's music notes (or Donkey Kong 64's bananas) - how do you save each individual banana/music note separately so that once you picked it up once, it is gone forever?

This script is unfinished, and it's only in a single map for now, but I wanted to share my progress. Later I'll clean it up and abstract it into a proper separate lua module.

My game has hundreds of pieces of trash on the map. When you load the map, the "beach_trash" savegame value looks like this:

Code: [Select]
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
After vacuuming 2 pieces of trash:

Code: [Select]
1111111111111111111111100111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
After vacuuming 6 pieces:

Code: [Select]
1111111111111110011111100111111111111111111111111111111111111111110011111111111111101111111111111111111111111111111111111111111111111111111111111111111111111111111110111111111111111111111
Essentially, when the map loads, it finds all the trash entities, loops through them, and gives them an ID based on the order they're returned from map:get_entities(). So the first piece of trash returned from get_entities() would have an ID of 1 and so on. A "state" table then keeps track of every piece of trash. The trash's ID is used as a key for the state table, and the value is 1 if existing, and 0 if removed. The state table is converted into the above binary string when saved. Here's the full code:

EDIT: Better version in the post below. Also, someone pointed out that this is basically a bitmap. Pretty much - it's like removing all the gaps between the entities and then creating a bitmap over that.

Code: Lua
  1. -- ♡ Copying is an act of love. Please copy and share.
  2.  
  3. local map = ...
  4. local game = map:get_game()
  5. local trash_state = {}
  6.  
  7. -- Takes in an entity state table and converts it to a string for save
  8. local function state_tostring(state)
  9.   local string = ""
  10.   for i, v in ipairs(state) do
  11.     string = string .. tostring(v)
  12.   end
  13.   return string
  14. end
  15.  
  16. -- Takes a state string and returns a table
  17. local function state_fromstring(str)
  18.   if str == nil then return {} end
  19.   local state = {}
  20.   for i=1, #str do
  21.     local n = tonumber(str:sub(i, i))
  22.     state[i] = n
  23.   end
  24.   return state
  25. end
  26.  
  27. -- Get all trash entities for a map
  28. local function get_trash_entities()
  29.   local trash_entities = {}
  30.   local custom_entities = map:get_entities_by_type("custom_entity")
  31.   for entity in custom_entities do
  32.     if entity:get_model() == "trash" then
  33.       table.insert(trash_entities, entity)
  34.     end
  35.   end
  36.   return trash_entities
  37. end
  38.  
  39. -- Assign ID's to all trash entities.
  40. -- Add on_removed event.
  41. local function process_trash()
  42.   trash_state = state_fromstring(game:get_value("beach_trash")) -- load from save
  43.   local entities = get_trash_entities()
  44.   for i, entity in ipairs(entities) do
  45.     entity.id = i -- Assign id to each piece of trash
  46.     if not trash_state[entity.id] then
  47.       trash_state[entity.id] = 1
  48.     end
  49.     function entity:on_removed() -- Update the trash state table
  50.       trash_state[self.id] = 0
  51.       local state_string = state_tostring(trash_state)
  52.       game:set_value("beach_trash", state_string) -- Save the trash state
  53.     end
  54.     if trash_state[entity.id] == 0 then
  55.       entity:remove()
  56.     end
  57.   end
  58. end
  59.  
  60.  
  61. function map:on_started()
  62.   process_trash()
  63. end
  64.  
  65. -- Remove the on_removed event, otherwise trash_state will be set to all zeros when the map is left
  66. function map:on_finished()
  67.   for i, entity in ipairs(get_trash_entities()) do
  68.     entity.on_removed = nil
  69.   end
  70. end
  71.  

3
I noticed something weird today: my sword can be activated during a dialog box. It goes to the first frame of animation and then is frozen until the dialog box finishes.



When I first opened the dialog box I checked "hero:get_state()" and it says "free". When I press the item button assigned to my sword, it says "sword activated". Keep in mind my sword is an item assigned to item_1 (using hero:start_attack() internally), so it's not surprising that I have strange behavior. But I did think it was strange the dialog box doesn't freeze my hero. I can fix this issue by calling hero:freeze() when the dialog box starts, and hero:unfreeze() when it finishes.

Okay, I understand that some menus (like a HUD) shouldn't freeze the player. But it raises a question: how exactly are button events handled? My dialog box has this:

Code: Lua
  1. function dialog_box:on_command_pressed(command)
  2.   -- "action", "attack", "up", and "down" are handled here. I'm using "item_1" and not pressing any other buttons.
  3.   ...
  4.  
  5.   -- Don't propagate the event to anything below the dialog box.
  6.   return true
  7. end

It returns true, so it shouldn't propagate. The context of dialog box is "game", and "game:on_command_pressed" is where the hero swings her sword.

In main.lua,

Code: Lua
  1.   -- I've simplified my code, but it shouldn't matter. This function shouldn't be called in the first place.
  2.  
  3. function game:on_command_pressed(command)
  4.  
  5.   print("you've reached me") -- This prints in the console when I perform the action in the screenshot, confirming that this function is called
  6.  
  7.   -- Makes the sword swing
  8.   local item = get_item_for_command(command)
  9.   item:on_command_pressed(command) -- this code calls hero:start_attack() internally
  10.  
  11.   return true
  12. end

Any ideas why the event is propagated to game:on_command_press?? Thank you!

4
General discussion / The perfect controller
« on: October 15, 2018, 04:27:37 am »
I spent today working on controller input for my Solarus game, and it sparked me to write about my decades-long yearning for the perfect video game controller. Just wanted to share, article here:

https://blog.alexgleason.me/the-perfect-controller/

5
Development / Adding gamepad support to the title screen
« on: October 14, 2018, 04:24:20 pm »
Hey all, just wanted to share my progress with this. I ended up writing a script that gives default configs to any menu object:

scripts/enable_commands.lua
Code: Lua
  1. -- ♡ Copying is an act of love. Please copy and share.
  2. --
  3. -- Use this script to pass in a game or menu object and have it set up
  4. -- default keyboard and joystick controls so you can use `on_command_pressed`
  5. -- as expected.
  6. --
  7. -- Usage:
  8. --    local enable_commands = require("scripts/enable_commands")
  9. --    enable_commands.enable(my_menu)
  10. --
  11. -- Then simply define `function my_menu:on_command_pressed(command)`
  12.  
  13. local enable_commands = {}
  14.  
  15.  
  16. local function on_key_pressed(self, key)
  17.   if self.on_command_pressed == nil then return true end
  18.   if key == "space" then self:on_command_pressed("action")
  19.   elseif key == "c" then self:on_command_pressed("attack")
  20.   elseif key == "d" then self:on_command_pressed("pause")
  21.   elseif key == "up" then self:on_command_pressed("up")
  22.   elseif key == "down" then self:on_command_pressed("down")
  23.   elseif key == "left" then self:on_command_pressed("left")
  24.   elseif key == "right" then self:on_command_pressed("right")
  25.   elseif key == "x" then self:on_command_pressed("item_1")
  26.   elseif key == "v" then self:on_command_pressed("item_2") end
  27.   return true
  28. end
  29.  
  30. local function on_key_released(self, key)
  31.   if self.on_command_released == nil then return true end
  32.   if key == "space" then self:on_command_released("action")
  33.   elseif key == "c" then self:on_command_released("attack")
  34.   elseif key == "d" then self:on_command_released("pause")
  35.   elseif key == "up" then self:on_command_released("up")
  36.   elseif key == "down" then self:on_command_released("down")
  37.   elseif key == "left" then self:on_command_released("left")
  38.   elseif key == "right" then self:on_command_released("right")
  39.   elseif key == "x" then self:on_command_released("item_1")
  40.   elseif key == "v" then self:on_command_released("item_2") end
  41.   return true
  42. end
  43.  
  44. local function on_joypad_button_pressed(self, button)
  45.   if self.on_command_pressed == nil then return true end
  46.   if button == 0 then self:on_command_pressed("action")
  47.   elseif button == 1 then self:on_command_pressed("attack")
  48.   elseif button == 4 then self:on_command_pressed("pause")
  49.   elseif button == 2 then self:on_command_pressed("item_1")
  50.   elseif button == 3 then self:on_command_pressed("item_2") end
  51.   return true
  52. end
  53.  
  54. local function on_joypad_button_released(self, button)
  55.   if self.on_command_released == nil then return true end
  56.   if button == 0 then self:on_command_released("action")
  57.   elseif button == 1 then self:on_command_released("attack")
  58.   elseif button == 4 then self:on_command_released("pause")
  59.   elseif button == 2 then self:on_command_released("item_1")
  60.   elseif button == 3 then self:on_command_released("item_2") end
  61.   return true
  62. end
  63.  
  64. local function on_joypad_axis_moved(self, axis, state)
  65.   if axis == 0 then
  66.     if state == 1 and self.on_command_pressed then self:on_command_pressed("right")
  67.     elseif state == -1 and self.on_command_pressed then self:on_command_pressed("left")
  68.     elseif state == 0 and self.on_command_released then
  69.       -- FIXME: Only release the last command?
  70.       self:on_command_released("right")
  71.       self:on_command_released("left")
  72.     end
  73.   end
  74.   if axis == 1 then
  75.     if state == 1 and self.on_command_pressed then self:on_command_pressed("down")
  76.     elseif state == -1 and self.on_command_pressed then self:on_command_pressed("up")
  77.     elseif state == 0 and self.on_command_released then
  78.       -- FIXME: Only release the last command?
  79.       self:on_command_released("down")
  80.       self:on_command_released("up")
  81.     end
  82.   end
  83.   return true
  84. end
  85.  
  86. -- Enable on an item
  87. function enable_commands.enable(item)
  88.   item.on_key_pressed = on_key_pressed
  89.   item.on_key_released = on_key_released
  90.   item.on_joypad_button_pressed = on_joypad_button_pressed
  91.   item.on_joypad_button_released = on_joypad_button_released
  92.   item.on_joypad_axis_moved = on_joypad_axis_moved
  93. end
  94.  
  95. return enable_commands
  96.  

Then just run your menu through this, either in main.lua or in your menu script itself, like so:

Code: Lua
  1. local enable_commands = require("scripts/enable_commands")
  2.  
  3. -- Ability to use game commands on these menus
  4. enable_commands.enable(title_screen)
  5.  
  6. function title_screen:on_command_pressed(command)
  7.   -- Handle commands here! No need to handle keys or joypad inputs.
  8. end
  9.  

It's a horribly verbose script! But it works. Normally the save file stores the controller config, so it creates a chicken and egg problem where you must create a save file before you can configure a gamepad, but need to go through the file select menu first. This partially works around that by assigning the Solarus default keyboard and joystick inputs to normal gamepad actions like "attack" and "action" etc. for a particular menu (the title screen).

What I think I'll actually end up doing is load the game before the title screen, and maybe let the player configure their input device here. My goal is to make the game playable on a platform like RetroPie where no keyboard exists. But this works for now.

As a side note, I think having 3 separate save files is overkill for most Solarus games. Unlike the days of Super Nintendo, most people have their own personal devices that can run Solarus, whether that's a laptop, desktop, Android phone/tablet, or Nintendo Switch. The exception might be a RetroPie device, but even then these often have hundreds of games and different people will probably be playing a different game during a different time. So, I'm going to focus on handling 1 save file at a time which will make things much easier.

6
Your scripts / Auto-assign an item to slot 1 if it's empty.
« on: October 13, 2018, 03:40:57 am »
This script is useful for when your character receives the first item of the game. Rather than making them equip the item, you can make it automatically assigned to slot 1. Adding slot 2 support probably wouldn't be too difficult. Here's the script.

Code: Lua
  1. -- ♡ Copying is an act of love. Please copy and share.
  2.  
  3. require("scripts/multi_events.lua")
  4.  
  5. -- When the player obtains their first item, assign it to slot 1 automatically
  6.  
  7. local function item_obtained_cb(self, variant, savegame_variable)
  8.   local slot_1 = sol.main.game:get_item_assigned(1)
  9.   if slot_1 == nil and self:is_assignable() then
  10.     sol.main.game:set_item_assigned(1, self)
  11.   end
  12. end
  13.  
  14. local item_metatable = sol.main.get_metatable("item")
  15. item_metatable:register_event("on_obtained", item_obtained_cb)

Install it by creating the file scripts/item_auto_assign.lua in your project and pasting the above code.

Then, open scripts/features.lua and add this to the list:

Code: Lua
  1. require("scripts/item_auto_assign")

I am assuming you're using the Solarus engine boilerplate code, which already includes the multi_events script.

7
Not sure if this is a bug or user error, but I wanted to share.

My goal is to disable the "attack" command completely and use the sword as a standard item (with hero:start_attack()). In main.lua:

Code: Lua
  1. function game:on_command_pressed(command)
  2.   -- Disable attacking; the stick is a regular item
  3.   if command == "attack" then
  4.     return true
  5.   end
  6.   ...
  7. end

However, when I swing the sword, I can actually still press and hold the attack button while the sword is mid-animation. This causes sword-loading to trigger. I'm baffled by the fact I can do this when I've overridden the attack command to "return true" whenever it's pressed. ???

I ended up adding this to scripts/meta/hero.lua to solve my issue:

Code: Lua
  1. -- The hero can only swing the sword, nothing else
  2. function hero:on_state_changed(state)
  3.   if state == "sword loading"
  4.   or state == "sword tapping"
  5.   or state == "sword spin attack" then
  6.     hero:freeze()
  7.     hero:unfreeze()
  8.   end
  9. end

Freezing and unfreezing the hero when going into the sword loading state effectively disables that state, which is what I want. (In my game the sword is limited)

8
Hi, I was hoping to create an item that triggers the hero's normal attack on_using(). It's simple:

Code: Lua
  1. local item = ...
  2. local game = item:get_game()
  3.  
  4. function item:on_created()
  5.   self:set_savegame_variable("stick")
  6.   self:set_assignable(true)
  7. end
  8.  
  9. function item:on_using()
  10.   print("beep")
  11.   game:get_hero():start_attack() -- Normal sword attack
  12. end

When I equip the item and press the button, the console outputs "beep", and then the player becomes frozen. The sword is not swung. From the console, I can unfreeze the player with: sol.main.game:get_hero():unfreeze().

Any clue why start_attack() isn't working here? I can call sol.main.game:get_hero():start_attack() from the console at any time and it works, but the same thing doesn't work inside this item script. Thanks!

9
Development / Slowdown after leaving the game on for a while
« on: September 29, 2018, 11:12:28 pm »
My game runs at full speed normally. But sometimes I'll leave the game window open while I'm working on some code. When I return 20 minutes later it's often lagging quite a bit. CPU usage goes up to 100% on one of my cores.

It's possible my game code is causing this, but I'm not sure where. I'm not using game:on_update() anywhere and I don't think I'm firing off any events that never stop. Has anyone else experienced this? Solarus 1.5.3

10
Development / How to filter an entity iterator?
« on: September 24, 2018, 12:03:15 am »
So I've used:

Code: Lua
  1. map:get_entities_in_rectangle(x, y, width, height)

which gave me back a nice entity iterator.

The problem is, I only want some of those entities. For example, I might want only entities where:

Code: Lua
  1. entity:get_type() == "custom_entity" and entity:get_model() == "trash"

I know how to loop through it and build a new array, but I'd like to learn how to create a new entity iterator. Is it possible to do this without dumping the contents of the first iterator into an array first? I'm new to Lua and trying to figure this out. :)

Thanks!

11
Your projects / Vegan on a Desert Island
« on: September 16, 2018, 08:23:13 pm »
Hello everyone! I'm in the process of developing a 2D puzzle adventure parody game using the Solarus Engine. It's called Vegan on a Desert Island.



Check out the video!

Screenshots:









The game is licensed under GPL-3.0 and CC-BY-SA-4.0. It uses Zoria Tileset for many of the background tiles, and many are custom made by me. The source repo is on GitLab. Feel free to use my sprites, music, characters, etc for anything you want! I plan to upload it all to OpenGameArt.org once the game is complete. It's about 10% finished now.

External links:

12
Your scripts / 8-bit Solarus Animated Logo Intro
« on: May 29, 2018, 11:22:03 pm »
Hello all,

I'm creating a game in Solarus which matches the Game Boy screen resolution (160x144px) and the Solarus logo which shipped with the editor didn't fit, so I created a new animated logo.

I couldn't figure out how to capture a gif of the animation, but here's the final frame:



The animation is basically the same as the included one. I've included a ZIP with the full source files so you can include it in your own project. :D

EDIT: I updated this package based on the feedback below.

Pages: [1]