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.
-- ♡ 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
I created a far superior version. It's encapsulated entirely within the custom entity script itself:
-- ♡ Copying is an act of love. Please copy and share.
local entity = ...
local game = entity:get_game()
local map = entity:get_map()
local hero = game:get_hero()
-- Name of the savegame variable for this map and model
local valname = string.format("entitystate__%s__%s", map:get_id(), entity:get_model())
-- Takes in a trash_state and converts it to a string for
-- storage into a savegame variable
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
-- Event called when the custom entity is initialized.
function entity:on_created()
-- Initialize the properties of your custom entity here,
-- like the sprite, the size, and whether it can traverse other
-- entities and be traversed by them.
entity:set_traversable_by("hero", false)
-- Initialize some variables in the map
if map._trash_count == nil then map._trash_count = 1 end
if map._trash_state == nil then
map._trash_state = state_fromstring(game:get_value(valname))
end
-- Add an ID to this entity by the order it was created
self._id = map._trash_count
map._trash_count = map._trash_count + 1 -- increment counter
-- Initialize the trash state
if map._trash_state[self._id] == nil then
map._trash_state[self._id] = 1
end
-- Remove this if it was already sucked up
if map._trash_state[self._id] == 0 then
self:remove()
end
end
-- Like remove(), but not called when the map ends
function entity:destroy()
if self.on_destroyed ~= nil then
self:on_destroyed()
end
self:remove()
end
-- Update the trash state table and save
function entity:on_destroyed()
map._trash_state[self._id] = 0
local state_string = state_tostring(map._trash_state)
print(state_string)
game:set_value(valname, state_string) -- Save the trash state
end
This custom entity can now be placed in any map, and calling entity:destroy() will cause it to save the state of that entity for that map.
Best of all, it requires not a single loop over the map entities!