Advertisement

Lua: Load all functions at once?

Started by July 15, 2004 06:57 PM
4 comments, last by ktw6675 20 years, 4 months ago
I'm whipping together some amateur adventure game experiments in my spare time, and I've recently begun looking into Lua to script the whole thing. Now, I'm wondering about something. Say I have a bunch of rooms (tables), and these rooms contain several objects (also tables), with certain properties. So, for instance:

Room1 = {}
Room1.ObjectA = { Name = "Object A", Active = false, }
Room1.ObjectB = { Name = "Object B", }

Room2 = {}
Room2.ObjectA = { Name = "Another Object A", }
Room2.DummyObject = { Name = "Object", timesClicked = 0 }

Room3 = {}
Room3.OneObject = { Name = "Only one object here.", }

Something like that. Now, I want to script 'events' for these objects. For instance, an "OnEnter()" event for rooms, or an "OnClick()" event for objects. Now, I could just add these all in the source file:

Room1 = {}
Room1.ObjectA = { Name = "Object A", Active = false, }
Room1.ObjectB = { Name = "Object B", }

Room2 = {}
Room2.ObjectA = { Name = "Another Object A", }
Room2.DummyObject = { Name = "Object", timesClicked = 0 }

Room3 = {}
Room3.OneObject = { Name = "Only one object here.", }

function Room1:OnEnter()
  -- Do stuff when you enter this room
  ...
end

function Room2:OnEnter()
  -- Do stuff when you enter this second room
  ...
end

function Room3:OnEnter()
  -- Do stuff when you enter this third room
  ...
end

function Room1.ObjectA:OnClick()
  ...
  -- loads of code to execute when you click on this object...
  ...
end
function Room1.ObjectB:OnClick()
  ...
  -- loads of code to execute when you click on this object...
  ...
end

function Room2.ObjectA:OnClick()
  ...
  -- loads of code to execute when you click on this object...
  ...
end

function Room2.DummyObject:OnClick()
  ...
  -- loads of code to execute when you click on this object...
  ...
end

function Room3.OneObject:OnClick()
  ...
  -- loads of code to execute when you click on this object...
  ...
end

Etcetera. Problem with that is that all those methods are all loaded at all times. This is okay for object properties like Name and such, I want to be able to access those for all rooms at all times (even when in a different room). For those methods, it's not. Since I won't click on an object in a room I'm not currently in, I don't need those functions. In fact, it would be better not to have the event-methods of objects in all the other rooms (except the one you're in), to avoid accidentally calling OnClick for an object in the next room. Plus, only loading the methods you actually need saves on memory use. So, I've been wondering. I was thinking of putting function definitions for each room in a different file, and then calling dofile() for a specific file when entering a room. However, I don't see a way of un-doing a file. Another thing I thought of was to add a LoadEvents() and UnloadEvents method to each room, where the LoadEvents method basically contains the function definitions for that particular room, and the UnloadEvents method sets them all to nil. It all seems rather awkward. What I want to know: How badly will it affect memory use when loading all function definitions at once, and is there a way of only loading specific function definitions, and then getting rid of them again when they are not needed anymore?
Nein heer du smign. ah open up the nine im heer du shmine
A sometimes not-so-obvious technique is to use each file as a "chunk" that returns a value. So yes, you could seperate each room into files, for example, here's one:

---- this file is Room1.lua--local room = {  ObjectA = {    Name = "Object A", Active = false  },  ObjectB = {    Name = "Object B"  }}return room


The important concept there is that the room variable is local to the file, but since you return it to the caller, then it exists solely in the caller's hands, so to speak...you're not polluting the global namespace.

Then you call "require 'room1'" which is the preferred method of running a file (dofile would also work fine but might provide less error information should you need it). The main thing, though, is that the FILE returns the room as a value. So somewhere in your code you could have

currentRoom = require 'room1'

Then currentRoom contains all the data (and functions you need to operate on that room. And then, when you want to unload it from memory, just turn it over to the garbage collector by either

1) reassigning currentRoom to a new room, or
2) setting currentRoom = nil

If you look up require in the Lua book you can find out about it's other benefits, a main one being that it will cache values that it returns so if you call the same require again it will return a preassigned value instead of doing the expensive compilation/running like usual.
Advertisement
Thanks for that clear reply, I see what you mean. I've thought about those separate files, I just couldn't think of a way to remove them from memory again.
Interesting... Isn't it so, though, that when the chunk returns the value, the local room value goes out of scope and gets removed? Won't the caller receive a reference to a nonexistant value? Or do variables local to a chunk remain if there's still a reference to them (Does the return statement return a copy or a reference)?
EDIT: Sorry, reread your reply and I noticed you mentioned that "the FILE returns the room as a value," which answers my question above.


One thing that might still be a problem here though is that you can't access a room's properties from another room. Say in room 1 there's a "light" object with a boolean "On" variable set to false, and in room 2 there's a "switch" object with a "OnClick()" event... There would be no way of setting Room2.Switch:OnClick() to something like:

Room1.Light.On = True

would there? Also, if you modify those properties, the state of the objects would not persist... If you set the Enabled property of an object to false, go to another room, and then return, it would just load the default values again...

Then again... Maybe I could keep those global objects and their properties, so that they're always accessible, and only load the -functions- in a separate chunk... Or is there a better way?

[Edited by - Boris Karloff on July 16, 2004 8:22:56 AM]
Nein heer du smign. ah open up the nine im heer du shmine
Wouldn't it be better to separate code instance from object instance?

You'd have functions like:

function EnterRoom( player, room )  player.where = room;  print "You are in room " .. room.Name;end


You can also use the metatable system to write this function once, and make it part of a Room class, rather than a Room instance, but that's a little more advanced Lua than you seem to be looking at right now.

Also, I'd strongly recommend against having the objects sit directly in the rooms like you do it. Instead, I would keep a table "Objects" in each room, where each object sits. That way, it's easy to iterate over all objects, and it's easy to remove an object from a room (when taken) and add it back somewhere else (when dropped).

Room1 = {  Name = "a large empty room",  Objects = {},};Room2 = {  Name = "a small crowded room",  Objects = {    Lantern = {      Name = "Brass Lantern",      Value = 3,      LightSource = 1,    },    Rock = {      Name = "Gray Rock",      Value = 0,    },  },};

enum Bool { True, False, FileNotFound };
That Objects table is a good call, actually. I'm currently just iterating over all tables in a room table, which gets me all the objects, since those are the only tables in a room. However, maybe other, non-object tables will get added to rooms later, so it might be a good idea to just wrap them all up in their separate objects table.

I'm not sure what you mean by separating code instance from object instance, though.. Instead of something like EnterRoom( player, room ), I find Room:Enter(player) a lot more intuitive (from an OO perspective). I'm basically looking to write event handlers for these objects in rooms.. So, for instance, when someone wants to look at the lantern object, I execute Lantern:OnLookAt(). Which, in turn, is defined as something like print("You see a shiny brass lantern.").



Nein heer du smign. ah open up the nine im heer du shmine
hplus is still correct, though: ultimately the easiest design choice that will benefit you down the road is to maintain a seperation of functions and data. (however, sometimes in adventure games, it seems, it is useful to just throw everything together since there are so many unique and individual actions) But maybe I can just show you some snippets of my (early) code right now that would make things more clear.

In my game, "Rooms" are the individual areas the player runs around in. Rooms are connected to other Rooms through doors, and when a player goes through a door, the new Room starts being "processed" (AI, physics) and the old room is serialized.

Here is the lua file that defines the functions for a Room.

---------------------------------------------------------------------------------- Room.lua--------------------------------------------------------------------------------lightclass 'Room'function Room:draw(camRect)	for i,layer in ipairs(self.map) do		local pCamRect = ZRect(camRect:x() * layer.p,camRect:y() * layer.p,camRect:width(),camRect:height())				gl.PushMatrix()		gl.Translate(-pCamRect:x(),-pCamRect:y(),0)			RoomDrawTileLayer(layer.data, self.map.width, pCamRect, self.tileset.tileVector)		gl.PopMatrix()	end	endfunction Room:setTile( l, x, y, t )		self.map[l].data:set(y * self.map.width + x, t)endfunction Room:getTile( l, x, y )	return self.map[l].data:get(y * self.map.width + x)endfunction Room:__init()	print "Room constructor!"end


The function "lightclass" is my OO system, with it's workings inspired by the lua book. So a call to "lightclass('thing')" will make it so that you can create new things by writing

t = thing()

Then functions defined for thing...

function thing:doSomething(param)
print "thing is doing something! " .. param
end

...will then operate on all things. It seems kind of complicated at first, but it ends up really being worth it, and you can inherit and do all the fun polymorphism stuff too.

---------------------------------------------------------------------------------- lightclass.lua--------------------------------------------------------------------------------function lightclass(name)	_G[name] = _G[name] or {}		_G[name].new = 			function(self,o)								o = o or {}										setmetatable(o, self)			self.__index = self			self.__lightclass = name			if self.__init then o:__init() end			return o		end						setmetatable(_G[name],{__call = _G[name].new})	end


So I have a set of operations for Room defined. Now when I want to load a room, I call a Lua file that was automagically created from a Room table before (I'll get to that). Here's an example of a file that returns a Room object...

local room = Room()room["map"] = {}room["map"][1] = {}room["map"][1]["p"] = 0.7room["map"][1]["data"] = IntVector{10,10,10, -- ...goes on for each tile in the layerroom["map"][2] = {}room["map"][2]["p"] = 1room["map"][2]["data"] = IntVector{1,1,1, --... goes onroom["map"]["height"] = 40room["map"]["width"] = 40room["name"] = "mountainpath"room["tileset"] = Tileset{filename='mountain.bmp'}return room


Right now there are no entities stored in the Room files...I'm working on that. IntVector is a class just like Room, so Lua knows how to construct them. To create this file, I just used a nifty function that serializes any Lua table. The table can have cycles (it can refer to itself or objects already in the table). The bulk of this function is also from the Lua book (which, if you can't tell by now, is amazingly helpful)--they just released it online, btw, at www.lua.org. save looks like this:

---------------------------------------------------------------------------------- Serialization.lua---------------------------------------------------------------------------------- Allows for serialization of tables WITH cycles. This quite clever code was -- stolen (with a few modifications) from Programming in Lua, the programming -- guide book for the Lua language. You can find it online at -- http://www.lua.org/pil/---------------------------------------------------------------------------------- note on lightclass functionality:-- the rule is, if the object has a tostring method defined then the save-- method will not delve any deeper into its table; it is assumed that all of-- the object's state is contained in the result of the tostring. if there is no-- tostring then the name of the class is pulled form the __lightclass element,-- an "empty" constructor is written, and the table is recursively saved.-- the examples are: Room is depth-written, and Tileset is surface-level-written-- the reasoning behind this is that some data, like Tilesets, cannot be written-- to text, because they have image data. instead the image data filename is-- written with a conststructor and serialization transparency is maintained.--local basicSerialize = function(o)	if type(o) == "number" then		return tostring(o)	elseif type(o) == "string" then		return string.format("%q", o)	elseif type(o) == "boolean" then		if o == true then return "true" else return "false" end	elseif type(o) == "userdata" then		if not getmetatable(o).__tostring then return 'nil -- error: no __tostring method defined for this userdata! KTW' end		return tostring(o)	end		endfunction save (name, value, saved, chunkReturn)	saved = saved or {}       -- initial value		local cReturnText = ""	if chunkReturn then cReturnText = "local " end			io.write(cReturnText, name, " = ")		if type(value) == "number" or 	   type(value) == "string" or 	   type(value) == "userdata" or 	   type(value) == "boolean" then		io.write(basicSerialize(value), "\n")	elseif type(value) == "table" then		if saved[value] then    -- value already saved?			io.write(saved[value], "\n")  -- use its previous name		else			saved[value] = name   -- save name for next time			local endHere = false			if value.__lightclass then 				if ( value.__tostring ) then					io.write( tostring(value) .. '\n' )						endHere = true				else					io.write( value.__lightclass .. "()\n" )				end							else				io.write("{}\n")     -- create a new table			end			if not endHere then				for k,v in pairs(value) do      -- save its fields					local fieldname = string.format("%s[%s]", name,													basicSerialize(k))					save(fieldname, v, saved)				end			end		end	else		io.write("nil -- cannot save a " .. type(value),"\n")	end	if chunkReturn then io.write("\nreturn ",name) endend -- function save


So maybe that will give you something to chew on as far as the whole object-data-state-serialization mess is concerned--it's never very easy, there's always gotchas, but Lua can make it simple.

This topic is closed to new replies.

Advertisement