This page is intended for developers trying to get acquainted with the aussenposten source code.
Table of contents
Technologies used
The game is written in JavaScript as CommonJs modules. The webkit (node-webkit) JavaScript runtime is used to provide a native executable appearance. The GUI is built with HTML & CSS and the game graphics are rendered on a HTML5 canvas element.
Dev setup
For development you should serve the aussenposten source directory via http and open index.html in the latest chrome browser. This gives you chrome’s development tools which you can’t easily and conviniently use in the executable (node-webkit).
File and directory overview
Important files and directories:
- src/ - JavaScript source files.
- map.tmx - a Tiled map which is loaded in the game. Has some tile attributes for game logic (walkable, resources,…).
- index.html, style.css - game screen, all HTML and CSS composing most GUI
- images/ - images modified or create by me for the game
- imagesoga/ - unmodified images from opengameart.org (not all are used in the game)
- aussenposten.exe, nw - the node-webkit executables start the game
- aussenposten_linux.sh - starts nw, the node-webkit executable
Code overview: Simulation/model logic
For programmers interested in enhancing or fixing aussenposten.
Unit
A Unit
is a state machine moving through the defined dayState
s for a particular unit type. The different dayState
s (eat, relax, work,…) all have properties attached to them (see JSON below). The state’s properties describe in which buildings it can be accomplished and for how long it is active. In detail, the properties are:
- Each
dayState
as aresolveTime
which designates how much time the unit will use accomplishing the state. - The
buildings
property of thedayState
is always an array of building types which the unit should move to, to accomplish thisdaySate
. - And the final property on each state is
nextState
which simply designates to which state the unit will transfer to once the current state was completed or itsresolveTime
used up.
The complete definition of all dayStates
for the basic unit type “helot” is listed below:
// `dayState`s of a unit of type "helot"
Unit.STATES.helot = {
eat: {
resolveTime: 30 * 1000,
nextState: 'relax',
buildings: ['cantine']
},
relax: {
resolveTime: 30 * 1000,
nextState: 'work',
buildings: ['living'],
},
work: {
resolveTime: 90 * 1000,
nextState: 'eat',
buildings: ['farm', 'mine', 'maintenance', 'storage', 'oremine', 'teleport']
},
idle: {
resolveTime: 10 * 1000,
nextState: 'work',
buildings: ['farmarea', 'mineshaft', 'oxygen', 'solar', 'teleport']
},
gounderground: {
resolveTime: 500 * 1000,
nextState: null,
buildings: ['elevator']
}
}
The unit’s state machine only deals with the problem of moving the unit from building to building, and sometimes keeping the unit at the target building for the given amout time (for work or other interactions with the target building).
A unit’s core logic related to dayState
is thus:
- Move the unit to the next, appropriate building according to the current
dayState
definition - Stay in this building until the
resolveTime
is used up - Select and set the next
dayState
(add 1): more about how to select the “next and appropriate building” can be found in “Selecting the appropriate building”.)
The above logic is implemented by the method Unit.prototype.updateDayState()
. I simplified the code of this method and removed some special cases to make it more readable. It then looks like this:
// the method is passed the amount of time passed since the last invocation has `msTickDuration`
Unit.prototype.updateDayState = function(msTickDuration) {
// do we have to calculate the route for the current dayState?
if (this.dayState.route === null) {
var tilePosition = ...
var routeInfo = this.dome.getDayStateRoute(tilePosition, this.dayState, this);
if (routeInfo) {
this.dayState.route = routeInfo.route;
this.dayState.building = routeInfo.building;
this.dayState.building.workerReserve(this);
} else {
// -- removed for clarity
// unit gets idle behaviour
}
// wait for resolve time
this.dayState.resolveTimeRemaining -= msTickDuration;
// still moving towards building?
if (this.dayState.route && this.dayState.routeFinished == false) {
// note that `updatePathfinding()` moves the unit
this.dayState.routeFinished = this.updatePathfinding(msDuration, 'dayState');
// we arrived!
if (this.dayState.routeFinished == true) {
this.dayState.building.workerEnter(this);
}
// we are at the building and the resolve time is used up
} else if (this.dayState.routeFinished == true && this.dayState.resolveTimeRemaining <= 0)) {
this.dayState.building.workerExit(this);
// move on
this.setDayState(this.dayState.nextState);
}
}
Remember, how the unit itself (in the above method) mostly deals with route finding to the next building and staying there for some time.
When the unit is in a building, the building takes over. To signal to the building, that it can control the unit, various methods are called on the building to signal changes: when a unit has selected a route to the building and is moving towards it (Building.workerReserve()
), when the unit has arrived at the building (Building.workerEnter()
) and when he leaves it (Building.workerExit()
).
Building
Building
s are active objects themselves.
They have an update method in which they can change several things:
- properties of the units currently in them (e.g., a cantine schange the
fullness
property of units while they eat) - properties of themselves (e.g, a farm generates nutrition and puts it into it’s
storage
) - they can use resources from any storage in the same dome (e.g., a caantine uses food when it feeds units).
There are a couple of update methods for the different building types. All these update methods start with ´step´ (e.g., stepRelax, stepGather, stepEat, etc.). We will take a closer look at the ´stepProduce()´ method use by production buildings. It does several things:
- the amount produced (´out´) is calculated depending on how many unit are currently working here, the state of the building and more
- the produced amount is added to the storage
- units working here have their ´fullness´ and ´happiness´ lowered (beause they work)
- the damage of the building is increased
I extraced the above steps by simplifying the stepProduce()
method a bit:
Building.prototype.stepProduce = function() {
// calculate amount output depending on productivity, numworkers, etc.
var numWorkers = this.workers.length;
var productivity = ...
var out = ...
this.outputStorage += out;
this.workers.forEach(function(w) {
// degrades the fullnes and happiness of the workers.
w.fullness += this.typeInfo.workerFullness;
w.happiness += this.typeInfo.workerHappiness;
}, this)
// apply damage to the bulding if we produced something
if (out > 0 && this.typeInfo.updateDamage) {
this.damage += this.typeInfo.updateDamage * (this.workers.length /this.typeInfo.maxWorkers);
}
return {
resource: this.typeInfo.output.resource,
amount: out
};
}
Complex buildings. Moving units while they are working in a building.
The above “production” building type is very simple because the units working for it do not have to leave the building to get work done. Some building types, like the “repair station” or “storage”, require that the workers move between other buildings to get their work done. For example, a storage worker has to fetch resources from a production facility and bring it back to the storage building.
When a unit needs to move while working for a building, the building can set a jobState
on the unit. A jobState
is very similar to a dayState
in that it only deals with moving the unit to a different building and sometimes staying there for a while.
A unit’s jobState
can only bet set by the building it is currently in.
For example, the storage building sets the jobState
“fetch” on all its workers currently without a jobState
. The “fetch” state moves the unit to an appropriate building: a production building which has something in its output storage. The unit moves to this target building and signals it missionEnter()
on arrival, and later missionExit()
. The missionExit()
of the target building then does two things: it hands out the resource the unit came for, and it sets a new jobState “store”, which causes the unit to move back to the storage building.
I extracted the code which deals with a storage workers leaving a target building from the rather long missionExit()
method:
Building.prototype.missionExit = function(unit) {
// unit was here to fetch something? send it back home to store
// whatever it grabbed.
if (unit.jobState && unit.jobState.type === 'fetch') {
// fetch specific type from storage or just anything from outputStorage?
var res = unit.jobState.storageType;
var amount = Math.min(15, this.storage[res]);
this.storage[res] -= amount;
unit.setJobState('store');
} else {
unit.jobState = null;
}
}
Moving to the “appropriate” building
Which building a unit should target for the current job is determined by two functions, for dayState
and jobState
:
- Dome.getDayStateRoute
- Dome.getJobStateRoute
//@@@ explain
View logic
The view logic is contained in two modules:
- views.js with lots of View classes for most model classes
- animate.js a custom version of gamejs/animate
The views are stored in a simple tree which is used for draw caching. Views like BuildingView or UnitView have a reference to the model they represent (Building and Unit respectively in this example) but views don’t query the model every time to render themselves but will often output a cached canvas.
The tree of views is composed as follows:
- The PlanetView holds DomeViews and EnemieViews
- Each DomeView holds BuildingViews and UnitViews
When a model appears or disappears (e.g. building constructed or unit dies) an event is sent to the PlanetView or DomeView which creates the new view and attaches the model.
Other components
- gamestate.js - loading, saving. uses
serialize()
on all game models to store state to JSON localStorage. - modules with “gui.js” suffix - methods which hook the HTML GUI to the javascript logic. e.g. unitgui.js updates the HTML view shown which shows info about a unit when clicked.