Object orientation & code complexity [Multiplayer 4X]

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
User avatar
Lap
Party member
Posts: 256
Joined: Fri Apr 30, 2010 3:46 pm

Object orientation & code complexity [Multiplayer 4X]

Post by Lap » Wed Jul 09, 2014 3:32 am

I have a client-server turn-based game that I have been making. I am trying to figure out how much data and tables to leave raw versus turning them into class based objects. I am currently using latest version of MiddleClass.

In general, I under utilize objects and mainly keep them for clientside things like GUI widgets and animations. I do this because I tend to use a lot of class inheritance here. However, there are extremely few on the server side.

Here's an example of something I am thinking of turning into an object:

I have a table for each region of a planet. There is a lot of information in each one of these including things like value, owner, units stationed, terrain type, etc.

Currently, a region is stored like:

Code: Select all

ServerGalaxy.Planets.Earth.Regions['North America'] = {Tons of values}
It gets sanitized of serverside only information and sent to the client where it essentially just becomes

Code: Select all

ClientGalaxy.Planets.Earth.Regions['North America'] = {Mostly same values}
All the functions related to regions are named things like addArmyToRegion(regionName, armyID). This is starting to become a pain with so many functions. I guess I could do something like region.addArmy(regionName, armyID), but it would be nice to do Region:addArmy(armyID), but then I have to start creating these objects again clientside after serialization and transfer from server (and to a lesser extent again serverside with client information).

It would be a lot of work to convert some of these things to objects and I am hesitant since I've gone so far without them. I'm kind of looking for an excuse not to do it and I have a feeling I'm overlooking something. I've tried the MiddleClass performance tests and it appears I could make an almost unlimited number of objects without performance problems...unless there's a memory use problem or if object creation is going to make garbage collection more difficult somehow? I also have the current code support a listen server where the player is a client and host in the same executable. [I don't know if that really is going to make a difference here].

TLDR: Should I make absolutely everything possible a class object to better group functions, realizing that this will mean more work making objects after accepting serialized data, or should I only use class objects for things that have a lot of inheritance?
Last edited by Lap on Wed Oct 08, 2014 8:53 pm, edited 2 times in total.

User avatar
ArchAngel075
Party member
Posts: 317
Joined: Mon Jun 24, 2013 5:16 am

Re: Object orientation decision in a multiplayer game

Post by ArchAngel075 » Wed Jul 09, 2014 10:47 am

I currently am working on a multiplayer baboviolent2 like game - the issue you have seems doable via a method i use :

Say you have ServerGalaxy and ClientGalaxy, all instances of objects (class instances) are stored in the #Galaxy table with a unique id.
For instance, I create a new planet and it is at id "1" (that is a string 1 not integer)
Thus it is ServerGalaxy["1"]

Next if I then send the 'sanatized' object over to clients I make sure the sanitized data also known of the id used.
Now when Client receives the data it checks if it has anything at index ClientGalaxy[n] where n is the index of the object on the server.
If the object exists already then we pass its data to it and let the object update itself uniquely..that way objects don't need a "you must send XYZ only in format ABC"
If it doesnt exist, then get the class type etc (also stored and sent) and simply create a new instance of the class but as a "proxy" of the parent. Yet unlike the server where its index is automatically gained from lowest open index it uses instead its parents index, so ClientGalaxy["n"] = new instance; and pass finally the update data as per usual

Next if a object is deleted server side then send a message to clients alerting the deletion, all clients will simply delete that object using the classes defined :delete method (or other?)

Deleting a object should set its database index to nil, ie ServerGalaxy[n] = nil

Now aquiring open slots :
use code like this :

Code: Select all

function getOpenIndex(databaseToUse)
local slot
 local Address = databaseToUse
 local lowerLimit = 1
 local upperLimit = 2^32 --set this as high as you want, but i doubt 2^64 would ever be exceeded ?
 for i = 1,upperLimit do
  if not Address[tostring(i)] then
   slot = tostring(i)
   break
  end
 end
 return tostring(slot)
end
The reason why we use tostring(i) is because if we insert new instances into the database in any way at the index tostring(n) and not straight up n is because inserting via a intger can not overwrite and otherwise table.insert(table,data,i) only shifts everything, which may make the client and serverDatabase not match in order.

I use this method actively on my engine and it i inspired from reading how UnityEgnine and UnrealEngine handle networking of thier classes.
It is only one method i use and there are other ways to go about doing this :)

User avatar
Lap
Party member
Posts: 256
Joined: Fri Apr 30, 2010 3:46 pm

Re: Object orientation decision in a multiplayer game

Post by Lap » Mon Jul 14, 2014 4:19 am

Thanks for the quick reply. I took a step back to reevaluate this whole thing because switching so many things to objects would be a lot of work and I didn't want to go in halfheartedly.

Your method works well for a game that operates in real time as entities are getting created and destroyed rather frequently. In my current project client and server data is exchanged just like old PBEM (play by email) where players receive a large save file every X minutes or hours. Because things can radically change from turn to turn and because the server may only play a turn a day I don't updated much. Almost all client data is wiped clean and recreated from the new save file. Unique ID's for things are already handled server-side.

I guess this is more of a housekeeping issue in choosing between three ways of doing the same thing:

*addArmyToRegion(regionName, armyID) -no actual class objects needed which means no need to create or destroy region objects. They're all just normal tables. It's hard to remember all the functions. Can get messy in namespaces.

*region.addArmy(regionName, armyID) - Same as above, but at least it won't pollute namespace as much. Probably easier to remember.

*region:addArmy(armyID) - This would mean each region is a class object. It's easy to remember, cleaner in the code and fast to write. However it means having to add another couple of steps in serialization of these objects and with transferring them back to objects on the other side.

Maybe I just need a "code consultant" to look over this project...though the spaghetting and lack of unified coding conventions may make their head explode. :ehem:

User avatar
Lap
Party member
Posts: 256
Joined: Fri Apr 30, 2010 3:46 pm

Re: Object orientation decision in a multiplayer game

Post by Lap » Tue Oct 07, 2014 12:59 am

Maybe another way of saying this would help:

I'm in the process of changing most of my game to be object oriented and I'm using middleclass.

This is a turn-based game so this process I am describing below happens every 30 seconds at shortest.

Server stores a list of units like:

Code: Select all

MasterUnitList = {
{UnitType= 'Tank', Health = 100},
{UnitType = 'Infantry', Health = 100}
} 
To make it easier to code serverside I make each unit a class object so that function calls are cleaner.

Each unit listed is gets initialized and turned into an object of class Unit.

Code: Select all

MasterUnitList = {
{UnitType= 'Tank', Health = 100, Class = "Unit"},
{UnitType = 'Infantry', Health = 100, Class = "Unit"}
} 
and the server can then do stuff like Unit:upkeep(), Unit:destroy(), etc.

Server sends MasterUnitList to the client, who initializes all of these units as class objects with something like

Code: Select all

Unit = class('Unit')

function Unit:initialize(unitTbl)
  if regionTbl ~=nil then
  --Copy over all keys to self
  table.Merge(self,regionTbl)
  end
end

ClientUnitList = MasterUnitList

  for unitTbl, unitID in pairs (ClientUnitList) do
  ClientUnitList[unitID] = Unit:new(unitTbl)
  end
Client then sends back his list serialzed and the server makes new Unit objects from the data again.


Does this seem like an ideal way to do this? Will the frequent creation of all these object cause a memory build up from lost, garbage resistant objects?

Would it be better to have nothing be class objects since there aren't very many subclasses/inheritances? Would it be better in this case to make a library called unit and call functions like "unit.destroy(unitID)"?

User avatar
kikito
Inner party member
Posts: 3153
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: Object orientation decision in a multiplayer game

Post by kikito » Tue Oct 07, 2014 8:57 am

Hi there,

There are lots of things to discuss here.

The first one I want to address is this:
I'm in the process of changing most of my game to be object oriented and I'm using middleclass.
Make sure you are not making it OO "for the shake of being OO", especially if you have something which already works. Doing a Big Rewrite is very tempting, but it also takes a lot of time, so the benefits have to be clear. You probably know this already, but it is important, so I didn't want to not mention it.

Also, when OO does not fit with your design, it is ok to not use it. You can have OO code and non-OO code living together, and that can do the job just fine. Your game logic could be OO while your network code could remain simple-table-based, and that would also be ok.
To make it easier to code serverside I make each unit a class object so that function calls are cleaner.
...
and the server can then do stuff like Unit:upkeep(), Unit:destroy(), etc.
I don't like this too much because it sounds like you have changed the way your game logic is structured in order to comply with a network serialization need. Those are two different concerns and, ideally, should be handled by different classes / functions. Right now you have "put them together". That can work for a while, but you might come to a point where your Game Logic and your Network Needs don't "match" perfectly (for example you decide to do some transformations or checks before sending/receiving the unit list).
Client then sends back his list serialzed and the server makes new Unit objects from the data again.
I am not an expert, but most of the client-server architectures that I've heard about don't allow clients to send "lists of modified units" to the server. Instead, the client usually sends "commands" (move this unit up, this unit has teleported, this unit has fired to this other unit, I want to create this unit here) and the server executes the commands, and returns the result (which could be a list of units - probably just the modified ones instead of the whole list, to save bandwidth). The client has the bare minimum to process user input, send the commands to the server, get its response, and display the game state on the screen. It should not have "authority" to modify the units by himself (that should be done in the server).

If I had to do a game like yours, the code for my "Client Units" and the code for my "Server Units" would probably be related, but different. The server unit would know what a unit can do, and have its status (health points, position, etc). It would also transform the unit into data to be sent to the client. The client unit would be in charge of displaying those values, without being able to modify them directly - the only way to modify them would be using what the server sends.

My server code would probably look like this:

Code: Select all

local ServerArcher = class('ServerArcher')

function ServerArcher:initialize(id,x,y,hp)
  self.id, self.x, self.y, self.hp = id,x,y,hp
end

function ServerArcher:receiveCommand(command)
  if command.name == 'move_up' then
    ... -- check if the archer can move up (i.e. no obstacles, no out of map, etc)
    self.y = self.y - 1
  end
  ...
end

function ServerArcher:toTable()
  return { class = 'Archer', x = self.x, y = self.y, hp = self.hp }
end
-----------

local GameServer = class('GameServer')
...

function GameServer:receiveClientCommands(player_id, commands)
  local player = self:getPlayer(player_id)
  for i=1, #commands do
    local unit = player:getUnit(commands[i].unit_id) -- checks that the unit is valid (player can move it, it exists, etc)
    unit:receiveCommand(commands[i])
  end
end

function GameServer:sendUpdatedUnitList(player)
  local updateUnits = player:getUpdatedUnits()
  local message = {}
  for i=1, #updatedUnits do
    message[i] = updatedUnits:toTable()
  end
  ... -- send message to player's client
end

The archer's receiveCommand & toTable methods might be the same for lots of units, but different in others, depending on the game. So it might make sense to put them in a superclass of Archer, or in a Mixin. Or not. It really depends on the game.

The client code would look like this:

Code: Select all

local ClientArcher = class('ClientArcher')

function ClientArcher:initialize(id,x,y,hp) -- we need x,y,hp to display, id to send commands
  self.id, self.x, self.y, self.hp = id,x,y,hp
end

function ClientArcher.static:newFromTable(t)
  return ClientArcher:new(t.id, t.x, t.y, t.hp)
end

function ClientArcher:updateFromTable(t)
  self.x = t.x
  self.y = t.y
  self.hp = t.hp
end

function ClientArcher:draw()
  love.graphics.draw(archerImage, ...)
end

-------

local GameClient = class('GameClient')
...
function GameClient:sendCommands(commands)
  ... -- send a list like
      -- { { name = 'move_up', unit_id = 5 }, { name = 'fire', unit_id = 6, to = 28 } }
      -- depending on the player's input
end

function GameClient:receiveUpdatedUnits(updatedUnitsTable)
  for i=1, #updatedUnitsTable do
    local unit = self:getUnitById(updatedUnitsTable[i].unit_id)
    unit:updateByTable(updatedUnitsTable[i])
  end
end

Again, the newFromTable and updateFromTable might be shareable in a superclass or mixin, or not, depending on the game logic.

I hope this helps!
When I write def I mean function.

User avatar
Lap
Party member
Posts: 256
Joined: Fri Apr 30, 2010 3:46 pm

Re: Object orientation decision in a multiplayer game

Post by Lap » Wed Oct 08, 2014 3:20 am

The one thing I have going for me is that the client is safely isolated from simple methods of cheating. In my examples I simplified the back and forth between client and server for brevity. Client pretty much does as you suggested with only sending changes, which are checked for validity by server. Here's the way I have it designed now:

Server[Data held in raw tables] -> Cut out data client doesn't need - > Serialize -> Send to client - > Client incorporates new data

Client[Data held in raw tables] -> Gather specific orders & changes - > Serialize -> Send to server -> Verify validity -> Execute Changes

Since all of this data isn't OO it is very easy to serialize and I don't have the extra step of converting serialzed data back into objects (don't need to destroy any either). This basic process wouldn't change that much whether objects are involved or not.
Make sure you are not making it OO "for the shake of being OO", especially if you have something which already works. Doing a Big Rewrite is very tempting, but it also takes a lot of time, so the benefits have to be clear. You probably know this already, but it is important, so I didn't want to not mention it.

Also, when OO does not fit with your design, it is ok to not use it. You can have OO code and non-OO code living together, and that can do the job just fine. Your game logic could be OO while your network code could remain simple-table-based, and that would also be ok
This seems to be the heart of the matter.

When I started this project I came from doing Lua in Supreme Commander, Garry's Mod and some other minor projects that used object oriented programming. They used object orientation, but I mostly saw it used on real-time entities and traditional "actor" type objects.

I still wasn't used to coding an entire game with OO and it just didn't seem to make sense at the time. Very few things needed inheritance or subclasses so I didn't see the point. It seemed like using OO for most things was the difference between Table.Key and Table["Key"]. The only thing I used OO for was goo (UI) and LUBE. Both of them are heavily OO and they benefit greatly from it.

The project ran for well over a year, the code ballooning in size. Now I have hundreds of functions in dozens of files and organization is becoming a serious problem. Does "function addUnitToArmy" get put in the army.lua file or the unit.lua file? Is it a client, shared or server function?

I tried a few IDE's that had built in function linking/autocomplete and also tried using things like javadoc, but guessing function names is still a problem. I get less and less done each coding session because I can't remember where things are or what they are called.

My global namespace is a polluted mess, which adds to the problem.

I wonder if OO is the answer, but as you said, a huge time investment and it may have its own problems.
*addArmyToRegion(regionName, armyID) -no actual class objects needed which means no need to create or destroy region objects. They're all just normal tables. It's hard to remember all the functions. Can get messy in namespaces.

*region.addArmy(regionName, armyID) - Same as above, but at least it won't pollute namespace as much. Probably easier to remember.

*region:addArmy(armyID) - This would mean each region is a class object. It's easy to remember, cleaner in the code and fast to write. However it means having to add another couple of steps in serialization of these objects and with transferring them back to objects on the other side.
The decision is crippling me.

User avatar
bartbes
Sex machine
Posts: 4946
Joined: Fri Aug 29, 2008 10:35 am
Location: The Netherlands
Contact:

Re: Object orientation decision in a multiplayer game

Post by bartbes » Wed Oct 08, 2014 8:21 am

Lap wrote:It seemed like using OO for most things was the difference between Table.Key and Table["Key"].
You do realize they're strictly equivalent in lua (always), as long as the key is a valid identifier (could be a variable name).

User avatar
undef
Party member
Posts: 438
Joined: Mon Jun 10, 2013 3:09 pm
Location: Berlin
Contact:

Re: Object orientation decision in a multiplayer game

Post by undef » Wed Oct 08, 2014 8:42 am

First of all, my deepest respect.
I'm a big fan of strategy games, especially turn-based ones lately (Civ5 has captured me) and I believe making a strategy game is quite the complex task.
Lap wrote: The project ran for well over a year, the code ballooning in size. Now I have hundreds of functions in dozens of files and organization is becoming a serious problem. Does "function addUnitToArmy" get put in the army.lua file or the unit.lua file? Is it a client, shared or server function?

I tried a few IDE's that had built in function linking/autocomplete and also tried using things like javadoc, but guessing function names is still a problem. I get less and less done each coding session because I can't remember where things are or what they are called.

My global namespace is a polluted mess, which adds to the problem.
I think the thread title is not well chosen, because what you really want to know is not whether you should use a more object orientated design or not, but how to contain complexity.

I don't claim I know the awnser to that question, but I have given this question a lot of thought lately, because I'm in a similar situation like you.
I am currently in progress of entirely rewriting the game I'm working on, because the code became long ugly and complex.
I decided to write it again from scratch, because I felt that it would be less work to write it anew than to clean up the mess I made.
And even though it is at times very frustrating (especially because there isn't new content emerging to keep you motivated), I think at least in my case it will be ultimately worth it.

My code is already far better structured, and it will be far easier to create content for my game once I've gotten the "core engine" to work.


Don't get me wrong though, it is a lot of work - so if you only want to get your game done, this might not be a good idea.


But anyway. I think a thorough code cleanup is definitely nescessary!


1. I think it's best to go through your code and delete everything you don't need (after making backups of course).
Ken Thompson wrote:One of my most productive days was throwing away 1000 lines of code.

2.
Check which functions can be abstracted to serve multiple purposes.
Your functions "addUnitToArmy" and "addArmyToRegion" seem quite similar to me, isn't it just adding a table to another table?
If you had a general function addTo( element, targetTable ), you could conveniently invoke it like this: element:addTo( targetTable )
Too many similar functions can be confusing and make your code less clear:

Image

3. Seperate the part of your code that always runs from the code that is [region/level/whatever] specific, then hide the code you don't modify so it won't stand in your way.
I thought of this as a nice approach to make my code a lot more readable.
The great thing about LÖVE is that you can make a great many things with just writing three functions ( load, update, draw), and LÖVE takes care of all the magic underneath.
So if you can manage to hide all the magic that happens all the time, I think it's far easier to work with your code.
And if you've made a good job, you might not even have to look under the hood anymore.

4. If something gets too complex too quickly, maybe there's a less complex way to describe it.
Happens to me all the time actually. Sometimes I might sit around for hours, because I refuse to write something in a way that just doesn't feel right Oh dear, how long have I been writing on this text?. And a lot of times I find a solution that simplifies my code a lot.
I think understanding a problem from many perspectives is crucial to describing it efficiently. (Both describing it with code or natural language).
That's why it is a huge help to draw a map of the program structure. (I still don't do it often enough, probably)




Concerning OOP:
I don't really use a lot of OOP, but there are some cases where it's just way more convenient to approach a problem using OOP.
I wouldn't try to radically force it on anything though, and just apply it when it seems reasonable.



Well I hope any of this might help, and if not - Hang in there!
Developing a game is tough, especially when it's a huge project like yours.
Just keep at it and you will get it done ;)
twitter | steam | indieDB

Check out quadrant on Steam!

User avatar
Lap
Party member
Posts: 256
Joined: Fri Apr 30, 2010 3:46 pm

Re: Object orientation & code complexity [Multiplayer 4X]

Post by Lap » Wed Oct 08, 2014 10:18 pm

bartbes wrote:
Lap wrote:It seemed like using OO for most things was the difference between Table.Key and Table["Key"].
You do realize they're strictly equivalent in lua (always), as long as the key is a valid identifier (could be a variable name).
Yep. I didn't see a point in doing OO in most things as it was essentially two equal ways of writing the same thing. That's what I was trying to convey anyway.
I think the thread title is not well chosen, because what you really want to know is not whether you should use a more object orientated design or not, but how to contain complexity.
Indeed it is. I chose it a few months ago when I thought that OOP would be a solution to code complexity and I wanted to know if the serialization and transfer of all these objects was going to be a problem. Now I'm not so sure the OO is the solution.

Thanks for the tips. I'm trying not to make any new content until I can clean up existing code.

#1 may be the most helpful, but also the hardest to mentally do. I don't think I can start the whole thing from scratch, but I can definitely redo entire modules (which may eventually result in a gradual, but total rewrite).

#2 is something I'm currently evaluating and would if I use OO might be even easier to consolidate functions.

#3 I haven't really thought about before. I try to hide stuff I'm not working on in separate files so I don't see them, but I can clearly do better as some files still have 10,000+ lines.

#4 I have started using LucidChart for flow maps. I was seriously thinking of creating one just to better describe things in this thread. I find them more and more useful as the project goes on.

I'll probably post the project again for a basic code review, but at this time it's just too dirty and inaccessible. I don't mind having people over when I haven't vacuumed in a couple of weeks, but my code is currently at the soiled-underwear-lying-on-floor level. :P

User avatar
kikito
Inner party member
Posts: 3153
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: Object orientation & code complexity [Multiplayer 4X]

Post by kikito » Wed Oct 08, 2014 11:56 pm

Ok several things again.

Object Orientation is not a tool to Make Complexity Go Away, as you have already realised. It is a tool to think about code in a certain way. If the problem you are trying to resolve happens to align well with object orientation, then code will be easier to write in an Object-oriented fashion. But not all problems align well with OO (some concepts have this nasty tendency to touch several orthogonal categories).

Other paradigms are different ways to think about problems. If you just need a set of steps, maybe an iterative code might be simple. If your problem can be specified as a set of processes working on what other processes produce, without changing much of state, maybe a functional approach will be better. And so on.

The programming paradigm you use is about the quality of the problem you solve. You however have a quantity problem. You have lots of stuff. And you need to manage that.

The bad news is that it's not an easy problem to solve. Books are written on the matter. Careers are built around it. As a result, I can only give you some pointers and rules-of-thumb.

The good news is that these rules can work in parallel with pretty much any programming paradigm that you use.

First: Be able to test your changes quickly. For example: instead of changing all your Units code to OO in one go, change just one unit to OO, and make the code keep working. You might need to write some scaffolding code in order to make this work (eg. something that says "if this unit is OO then do this else do-old-stuff"). If it allows you to test the game faster, it is totally worth it.

Second: Save often. This basically means using a version control system, like git. Every time you do one of those little changes I mentioned on the first rule, and you have tested that they work, make a commit. Then make the code prettier, and make another commit. If you get to a point where you can't solve an issue, you can always go back. If you are not sure about a change that you are going to make, create a branch to experiment on it, so you can throw it away. Learn at least the basics of git

Third: Be coherent. The example that you put before about adding Units to Armies: It doesn't matter what you choose. Well, it in some cases it does. But what matters most is that you are coherent. Pick one. And then make sure you always use that one. Don't have army:add(unit) in one place and button:insertInto(window) in another. If necessary, document the decisions you take in a text file ("when adding foos to a collection, I shall always do collection:addFoo(foo)")

Fourth: Name things properly (and coherently!). This isn't so difficult as it seems. Sometimes a good name doesn't come in the first minute. Fine, spend more minutes. Open a thesaurus if you need to. It's time well spent. If you want to read more about naming things, I think the first chapter of Clean Code is very good.

Fifth: Make things as local as possible. Certainly your global space should not be polluted with lots of stuff. But you should not stop there. Every scope should "know" as little as possible. This means that it might not be not enough putting all the player-related functions into a Player class (or a player module). Go deeper. Consider that maybe the player-movement functions and the player-scoring functions should not be in the same place (maybe move the score ones to a different Score module/class?)

Sixth: Be mindful of the language's boundaries. Boundaries here means "concepts that can be used as separators" when dealing with complexity. In Lua, these are files, functions (with their closures) & tables. If you use Middleclass, you'll also get classes, instances and mixins, which in reality are types of tables, but are logically different. Be "aware" every time that you use one boundary ("why am I creating this function?"). And at the same time, be ready to put a boundary when necessary ("should I split this into a function?"). Read rule 3 again.

Seventh: Put boundaries between levels of abstraction. You should not be dealing with planets, continents, countries, cities, buildings, rooms, people, body parts and cells in the same piece of code. Ideally every piece of code should deal with 1 level. It's ok to deal with 2 levels when they touch (eg. parent-child relationships) but 3 should raise your eyebrow, and 4 is a no-no.

Eighth: Use composition. If you have something that is "a-x-and-a-y-and-a-z", try transforming it into something that "has a x, has a y, and has a z". For example, instead of giving your player a mass of 50, a position (x=100,y=200), a score(0), a list of items(empty), and healthpoints(10), along with all the methods and functions that depend/modify those, consider giving your player a body (with healthpoints) which in turn will have a physical_body (where you will put the mass & the position). Also give your player inventory (where the items will be stored). And maybe leave the score inside the player. Then move the correspondent methods out of player and into the classes/modules of body, physical_body and inventory.

Ninth: Pure function > function with explicit dependencies > the rest of the functions > too little functions. A function with explicit dependencies is a function that only "works" with its parameters. It doesn't use global variables. This means that if you call it with the same parameters, you will get the same result. A pure function is a function with explicit parameters which does not change any of them (probably it returns a calculation). Here are 3 versions of the same code, in ascending order:

Code: Select all

-- level 1: no function, changes/uses 2 globals
bullets[bullets + 1] = {x=player.x, y=player.y, math.atan2(player.x - t.x, player.y - t.y)}

-- level 2: A function, with better names(target, angle) changes/uses 2 globals
local function shootAt(target)
  local angle = math.atan2(player.x - target.x, player.y - target.y)
  bullets[bullets + 1] = {x=player.x, y=player.y, angle=angle}
end

-- level 3: A function with explicit dependencies, modifies "bullets"
local function shootAt(origin, target, bullets)
  local angle = math.atan2(origin.x - target.x, origin.y - target.y)
  bullets[bullets + 1] = {x=origin.x, y=origin.y, angle=angle}
end

-- level 4: 1 Pure functions and 1 (smaller) impure function
local function getAngleBetweenPoints(x1,y1,x2,y2)
  return math.atan2(x1-x2, y1-y2)
end

local shootAt(origin, target, bullets)
  local angle = getAngleBetweenPoints(origin.x, origin.y, target.x, target.y)
  bullets[#bullets+1] {x=origin.x, y=origin.y, angle=angle}
end
It is not always possible to make functions completely pure, but it is often possible to make their dependencies explicit ('self' is an explicit dependency in instance methods when using middleclass). Even when it's not possible to make a function pure, it is often possible to make parts of it pure.

Tenth: Involve others. Put your code in front of other people's eyes as soon as you can. Even if it's "too dirty and inaccessible". The worst thing that can happen is that no one will read it. But if you are lucky someone will. And even give you valuable feedback (there's lots of weird people out there). Put it on github, bitbucket, or whatever you prefer, as soon as possible!

That is all I have for now. Sorry for the wall of text, and good luck!
When I write def I mean function.

Post Reply

Who is online

Users browsing this forum: Bing [Bot], DKipppp and 19 guests