Script to save the state of hundreds of objects

Started by alexgleason, October 17, 2018, 08:49:55 AM

Previous topic - Next topic
October 17, 2018, 08:49:55 AM Last Edit: November 23, 2018, 02:46:07 PM by alexgleason
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
RIP Aaron Swartz

October 17, 2018, 10:00:07 PM #1 Last Edit: November 23, 2018, 02:46:30 PM by alexgleason
I created a far superior version. It's encapsulated entirely within the custom entity script itself:

Code ( lua) Select

-- ♡ 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!
RIP Aaron Swartz