Menu

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.

Show posts Menu

Topics - alexgleason

#21
Development / Multi-events as the default?
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) Select
-- Lua script of map test_map.
-- This script is executed every time the hero enters this map.

-- Feel free to modify the code below.
-- You can add more events and remove the ones you don't need.

-- See the Solarus Lua API documentation:
-- http://www.solarus-games.org/doc/latest
require("scripts/multi_events.lua")

local map = ...
local game = map:get_game()

-- Event called at initialization time, as soon as this map becomes is loaded.
function map:register_event("on_started", function()

  -- You can initialize the movement and sprites of various
  -- map entities here.
end)

-- Event called after the opening transition effect of the map,
-- that is, when the player takes control of the hero.
function map:register_event("on_opening_transition_finished", function()

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.
#22
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:

1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

After vacuuming 2 pieces of trash:

1111111111111111111111100111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

After vacuuming 6 pieces:

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) Select

-- ♡ Copying is an act of love. Please copy and share.

local map = ...
local game = map:get_game()
local trash_state = {}

-- Takes in an entity state table and converts it to a string for save
local function state_tostring(state)
  local chars = {}
  for i, v in ipairs(state) do
    table.insert(chars, tostring(v))
  end
  return table.concat(chars, "")
end

-- Takes a state string and returns a table
local function state_fromstring(str)
  if str == nil then return {} end
  local state = {}
  for i=1, #str do
    local n = tonumber(str:sub(i, i))
    state[i] = n
  end
  return state
end

-- Get all trash entities for a map
local function get_trash_entities()
  local trash_entities = {}
  local custom_entities = map:get_entities_by_type("custom_entity")
  for entity in custom_entities do
    if entity:get_model() == "trash" then
      table.insert(trash_entities, entity)
    end
  end
  return trash_entities
end

-- Assign ID's to all trash entities.
-- Add on_removed event.
local function process_trash()
  trash_state = state_fromstring(game:get_value("beach_trash")) -- load from save
  local entities = get_trash_entities()
  for i, entity in ipairs(entities) do
    entity.id = i -- Assign id to each piece of trash
    if not trash_state[entity.id] then
      trash_state[entity.id] = 1
    end
    function entity:on_removed() -- Update the trash state table
      trash_state[self.id] = 0
      local state_string = state_tostring(trash_state)
      game:set_value("beach_trash", state_string) -- Save the trash state
    end
    if trash_state[entity.id] == 0 then
      entity:remove()
    end
  end
end


function map:on_started()
  process_trash()
end

-- Remove the on_removed event, otherwise trash_state will be set to all zeros when the map is left
function map:on_finished()
  for i, entity in ipairs(get_trash_entities()) do
    entity.on_removed = nil
  end
end
#23
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) Select
function dialog_box:on_command_pressed(command)
  -- "action", "attack", "up", and "down" are handled here. I'm using "item_1" and not pressing any other buttons.
  ...

  -- Don't propagate the event to anything below the dialog box.
  return true
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) Select
  -- I've simplified my code, but it shouldn't matter. This function shouldn't be called in the first place.

function game:on_command_pressed(command)

  print("you've reached me") -- This prints in the console when I perform the action in the screenshot, confirming that this function is called

  -- Makes the sword swing
  local item = get_item_for_command(command)
  item:on_command_pressed(command) -- this code calls hero:start_attack() internally

  return true
end


Any ideas why the event is propagated to game:on_command_press?? Thank you!
#24
General discussion / The perfect controller
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/
#25
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) Select

-- ♡ Copying is an act of love. Please copy and share.
--
-- Use this script to pass in a game or menu object and have it set up
-- default keyboard and joystick controls so you can use `on_command_pressed`
-- as expected.
--
-- Usage:
--    local enable_commands = require("scripts/enable_commands")
--    enable_commands.enable(my_menu)
--
-- Then simply define `function my_menu:on_command_pressed(command)`

local enable_commands = {}


local function on_key_pressed(self, key)
  if self.on_command_pressed == nil then return true end
  if key == "space" then self:on_command_pressed("action")
  elseif key == "c" then self:on_command_pressed("attack")
  elseif key == "d" then self:on_command_pressed("pause")
  elseif key == "up" then self:on_command_pressed("up")
  elseif key == "down" then self:on_command_pressed("down")
  elseif key == "left" then self:on_command_pressed("left")
  elseif key == "right" then self:on_command_pressed("right")
  elseif key == "x" then self:on_command_pressed("item_1")
  elseif key == "v" then self:on_command_pressed("item_2") end
  return true
end

local function on_key_released(self, key)
  if self.on_command_released == nil then return true end
  if key == "space" then self:on_command_released("action")
  elseif key == "c" then self:on_command_released("attack")
  elseif key == "d" then self:on_command_released("pause")
  elseif key == "up" then self:on_command_released("up")
  elseif key == "down" then self:on_command_released("down")
  elseif key == "left" then self:on_command_released("left")
  elseif key == "right" then self:on_command_released("right")
  elseif key == "x" then self:on_command_released("item_1")
  elseif key == "v" then self:on_command_released("item_2") end
  return true
end

local function on_joypad_button_pressed(self, button)
  if self.on_command_pressed == nil then return true end
  if button == 0 then self:on_command_pressed("action")
  elseif button == 1 then self:on_command_pressed("attack")
  elseif button == 4 then self:on_command_pressed("pause")
  elseif button == 2 then self:on_command_pressed("item_1")
  elseif button == 3 then self:on_command_pressed("item_2") end
  return true
end

local function on_joypad_button_released(self, button)
  if self.on_command_released == nil then return true end
  if button == 0 then self:on_command_released("action")
  elseif button == 1 then self:on_command_released("attack")
  elseif button == 4 then self:on_command_released("pause")
  elseif button == 2 then self:on_command_released("item_1")
  elseif button == 3 then self:on_command_released("item_2") end
  return true
end

local function on_joypad_axis_moved(self, axis, state)
  if axis == 0 then
    if state == 1 and self.on_command_pressed then self:on_command_pressed("right")
    elseif state == -1 and self.on_command_pressed then self:on_command_pressed("left")
    elseif state == 0 and self.on_command_released then
      -- FIXME: Only release the last command?
      self:on_command_released("right")
      self:on_command_released("left")
    end
  end
  if axis == 1 then
    if state == 1 and self.on_command_pressed then self:on_command_pressed("down")
    elseif state == -1 and self.on_command_pressed then self:on_command_pressed("up")
    elseif state == 0 and self.on_command_released then
      -- FIXME: Only release the last command?
      self:on_command_released("down")
      self:on_command_released("up")
    end
  end
  return true
end

-- Enable on an item
function enable_commands.enable(item)
  item.on_key_pressed = on_key_pressed
  item.on_key_released = on_key_released
  item.on_joypad_button_pressed = on_joypad_button_pressed
  item.on_joypad_button_released = on_joypad_button_released
  item.on_joypad_axis_moved = on_joypad_axis_moved
end

return enable_commands


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

Code ( lua) Select

local enable_commands = require("scripts/enable_commands")

-- Ability to use game commands on these menus
enable_commands.enable(title_screen)

function title_screen:on_command_pressed(command)
  -- Handle commands here! No need to handle keys or joypad inputs.
end


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.
#26
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) Select
-- ♡ Copying is an act of love. Please copy and share.

require("scripts/multi_events.lua")

-- When the player obtains their first item, assign it to slot 1 automatically

local function item_obtained_cb(self, variant, savegame_variable)
  local slot_1 = sol.main.game:get_item_assigned(1)
  if slot_1 == nil and self:is_assignable() then
    sol.main.game:set_item_assigned(1, self)
  end
end

local item_metatable = sol.main.get_metatable("item")
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) Select
require("scripts/item_auto_assign")

I am assuming you're using the Solarus engine boilerplate code, which already includes the multi_events script.
#27
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) Select

function game:on_command_pressed(command)
  -- Disable attacking; the stick is a regular item
  if command == "attack" then
    return true
  end
  ...
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) Select
-- The hero can only swing the sword, nothing else
function hero:on_state_changed(state)
  if state == "sword loading"
  or state == "sword tapping"
  or state == "sword spin attack" then
    hero:freeze()
    hero:unfreeze()
  end
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)
#28
Hi, I was hoping to create an item that triggers the hero's normal attack on_using(). It's simple:

Code ( lua) Select
local item = ...
local game = item:get_game()

function item:on_created()
  self:set_savegame_variable("stick")
  self:set_assignable(true)
end

function item:on_using()
  print("beep")
  game:get_hero():start_attack() -- Normal sword attack
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!
#29
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
#30
Development / How to filter an entity iterator?
September 24, 2018, 12:03:15 AM
So I've used:

Code ( lua) Select
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) Select
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!
#31
Your projects / Vegan on a Desert Island
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:
#32
Your scripts / 8-bit Solarus Animated Logo Intro
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.