Object animation and other fun in m4nfo
Introductionm4nfo is a "high-level language" frontend for generating so-called "nfo byte code", compiling "human-readable" source code into
plain nfo, which m4nfo then feds into
grfcodec (probably together with appropriate graphics) to generate newGRFs for TTDPatch and OTTD.
m4nfo pursues a two-fold approach: whilst retaining the efficiency characteristic of plain nfo, with the prior-ranking aim to generate small and fast code, it introduces higher abstraction levels where it makes sense for the programmer, considering established newGRF coding habits.
To achieve these goals, m4nfo has been implemented as a set of small 'modules' instead as a large monolithic application. There are separate modules for trains, stations, objects, ..., reflecting the fact that most of the time a newGRF only deals with one of TTD's 'features'. (In fact, even outside the scope of m4nfo, it is recommended behaviour to not mix different features into one newGRF.)
This approach doesn't only keep m4nfo small and lightweight (M4: 143kB, module 'trains': 40kB), it also allows to use identical identifiers and names for same methods on different features, keeping its name space small, and avoiding redundant labeling.
Taking into account the modular structure of m4nfo, it is very easy to add extra user-supplied functions or even whole modules. This can be done either 'on-the-fly', placing functions directly inside of the user's source files; or it could be done by adding additional include files. It is even possible to pre-compile user-supplied modules, or - rather exotic - include sections of plain nfo code directly into m4nfo source files.
In addition, m4nfo natively includes a macro processor suitable for private customizing or templating applications (indeed, m4nfo is written in M4, a very efficient macro processor itself), so it won't need any external tools, like CPP for macro usage or artificially crafted extra Python layers, resulting in bloated installations.
There are pre-compiled modules, needing no m4nfo system include files and running even slightly faster, for the diverse features, which might be easily used manually or from a makefile script.
In conclusion, m4nfo is best used for large projects because of its unrivalled speed and code efficiency, the latter only depending on the experience of the coder, because there is no additional overhead introduced by the compiling process. Last but not least, the ability of m4nfo to handle distributed source files is best suited when using a build-management tool like 'Make', to constitute an efficient programming environment for the really large projects.
Attachment:
PSPad_full.png [ 34.64 KiB | Viewed 10933 times ]
For more detailed information see the
m4nfo User Manual and Technical Report.
Tutorial"newGRFs with 'objects' - animation and other fun in m4nfo"
Part 1 - simple animation
The first example will demonstrate a very basic animation which would be constantly looping and does not use any triggers. It is based on one of the mole lights from the MariCo set. The object in question would use its animation to generate a "character" (to help the sailors to discern this very light from others). This is achieved by splitting the (animated) "light" from the base object (the "lighthouse" in the further course) and thus showing two different objects in a continuous loop, either only the lighthouse or the combination of lighthouse and light.
First thing to do would be to define the object at all. In m4nfo, this is done by use of function "defineobject()", and here it is:
Code:
defineobject(_LIGHTS,
classid(MC01)
classname(moles)
objectname(molelight)
climate(TEMPERATE, ARCTIC, TROPIC)
size(1,1)
price(80)
timeframe(1.1.1880 .. 1.1.2050)
flags(NOBUILDONLAND, HASANIMATION)
callbacks(CB_TEXT)
anim_info(10, LOOP)
anim_speed(5)
anim_triggers(BUILT)
buildingheight(2)
numviews(4)
)
As can be seen from the function body, this object´s ID is "_LIGHTS" (some number), its class-ID is set to "MC01" (to place it correctly in the object building menu), its classname is set to the string referenced by the text identifier "moles", and its objectname to the string referenced by "molelight" (both left out in this tutorial, see the user manual). It´ll be available in temperate, arctic and sub-tropical climates, it´s of size 1*1, it may be built for a certain price inside a timeframe from january 1st, 1880 to january 1st 2050, must be built on water, has animation, and uses the "additional text" callback.
W/r to animation, it uses 10 animation frames in a loop, with animation speed of "5", and the only animation trigger would be the building of this particular object.
Other than this, it has a height of "2" (i.e. 2 * 8px), and comes in 4 "views", i.e. 4 different graphical representations to be chosen when being built.
Now, some real (graphics) sprites have to be prepared, this is done in m4nfo in the following way:
Code:
spriteblock(
...
// 8 lighthouses green (LIGREEN[1 .. 8])
set(
sprite(mole.pcx 10 175 09 22 8 -1 -16)
)
set(
sprite(mole.pcx 20 175 09 26 6 0 -20)
)
set(
sprite(mole.pcx 28 175 09 27 8 -1 -21)
)
set(
sprite(mole.pcx 38 175 09 25 12 -3 -19)
)
set(
sprite(mole.pcx 52 175 09 24 10 -2 -18)
)
set(
sprite(mole.pcx 64 175 09 23 4 1 -17)
)
set(
sprite(mole.pcx 70 175 09 27 8 -1 -21)
)
set(
sprite(mole.pcx 80 175 09 23 6 0 -17)
)
...
// animated green light (ANIMGREEN)
set(
sprite(mole.pcx 171 175 09 3 2 0 0) // green
)
...
)
As can be seen, we´re going to use 8 different lighthouse sprites, please note that these aren´t the "views", but our code will randomly display those 8 different lights per view.
In addition, we need a green light (ANIMGREEN) for the animation. And exactly that´s what the whole procedure is for: TTD has no animated green colour!
Now, the needed sprites having been defined in a "spriteblock" function, we need means to compose the object from its individual sprites into a "tile". This is done by function "spriteset":
Attachment:
animtut.png [ 11.44 KiB | Viewed 10933 times ]
Code:
//------------------------------------------------------------------
// sprite sets with 8 lighthouses green
//------------------------------------------------------------------
// #1
def(50) spriteset(
set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole
set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light
)
def(51) spriteset(
set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole
set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light
set(normal(ANIMGREEN), xyoff(3,2)) // animated light (green)
)
def(22) anim_frame(
ref(50) if(4, 9) // dark
ref(51) else
)
This is the code for one (#1) of the 8 objects, they´re different w/r to the sprite for the light and the animation sequence.
In m4nfo, assembling sprites into tiles is done by function "spriteset" which supports all the subtle ways a tile might be defined in TTD, e.g. sprites defining their own 3D "bounding box", or so-called "child sprites", sharing their parents bounding box, recoloured or transparent sprites, etc.
At first, let´s take a look on the un-animated light (the "base"):
Code:
def(50) spriteset(
set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole
set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light
)
First of all, note the function "def()". Unlike other "high-level" languages, m4nfo handles plain nfo´s "c-IDs" (by which the chain of control is set up) manually. In this way, the introduction of an inefficient garbage collection is avoided, and the user may benefit from "re-usage of c-IDs" which is a very handy feature in plain nfo, especially because of the existing limit of 255 c-IDs. (O/c, it´s always possible to "label" the defs in m4nfo, and re-use them at a later time by that label. OTOH, using plain numbers is especially clear in a local context, rather than using endlessly long identifiers ...)
So, let´s take a look on that "def(50)". It defines a spriteset consisting of two "sets", both defining a so-called "parent sprite". The first parent sprite (the mole where the lighthouse will be placed on) uses TTD´s "water" sprite as its groundtile and real sprite number "2" (the mole piece) from the spriteblock above for its building tile. It defines a bounding box of 16 * 16 pixels with a height of 6 pixels, which completely covers the ground tile, because its origin is placed at coordinate (0,0), see picture.
The second sprite is the lighthouse to be placed on top of the mole piece, using sprite LIGREEN1 from the sprite block above. This set defines a smaller bounding box of 8 * 8 pixels, located at (1,5) at a height of 6 pixels, i.e. right on top of the first bounding box.
Both sprites are handled "normally", that´s why the function "normal()" is used for them. (There are special ways to handle real sprites in TTD/m4nfo, e.g. using company colour, two company colours or recolouring, those would be handled by different functions than "normal()".)
Well, that´s our lighthouse tile now, consisting of a water ground sprite, a mole piece, and a lighthouse building.
Now, we need the same thing with an additional green light, and here it is:
Code:
def(51) spriteset(
set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole
set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light
set(normal(ANIMGREEN), xyoff(3,2)) // animated light (green)
)
First two sets are identical to those from def(50), but there´s now a third one. This one is a so-called "child sprite", i.e. it has no bounding box, but it does share the bounding box of the previos sprite, i.e. that of the lighthouse. The xyoff() parameter specifies the offset of sprite ANIMGREEN with regards to LIGREEN1: there´s an offset in x of 3 pixels, and an offset in y (height) of 2 pixels.
The deep reason to split up the sprites in this particular way lies with the problem of displaying the tile in 4 directions (x front, x back, y front, y back) which would need four times the number of sprites, while we´re able now to "shift" the light sprite w/r to the mole sprite according to the direction of the tile, just by setting the xyz() coordinates in a clever way.
Well, let´s move on now to the animation step. This is simply done by
Code:
def(22) anim_frame(
ref(50) if(4, 9) // dark
ref(51) else
)
Are you still remembering that our animation was defined for 10 frames? Well, function anim_frame() references def(50) on the 4th and the 9th frame, and def(51) for all the other frames, generating the neat effect of switching off the light on frames 4 and 9.
Needless to say that the other 7 light versions are using different "characters" (animation sequences), helping the sailors to keep the lights apart.
So, in a next step, we need to randomize those 8 light versions:
Code:
def(14) randomrel(CONSTRUCT, 1, ref(22), ref(23), ref(24), ref(25), ref(26), ref(27), ref(28), ref(29)) // green
This function defines yet another def(14) randomizing defs 22, 23, ..., 29 on construction, i.e. after being built, they´d stay the same. (Object don´t have any more random triggers.)
Unfortunately, now it gets a bit lengthy, because we need those three other directions (and then, we´d need everything again for the 8 red lights, doh!), but we´ll skip it here, because it doesn´t introduce anything new, except from different sprites and x/y-offsets.
Anyway, eventually we´re ending at this part of the code:
Code:
//------------------------------------------------------------------
// mole straight in x with lighthouse green
//------------------------------------------------------------------
// mole in x, find end of straight line
def(40) objinfo_water(pos(-1,0), // back
ref(14) if(ENDMOLE) // lighthouse
ref(0) else
)
def(41) objinfo_water(pos(1,0), // front
ref(16) if(ENDMOLE) // lighthouse
ref(40) else
)
def(42) objinfo_slope(
ref(10) if(WEST+SOUTH) // slope back
ref(12) if(NORTH+EAST) // slope front
ref(41) else // flat, check for end tile
)
def(50) callback(
animcontrol(0) if(CB_ACONTROL) // start animation
ref(42) else
)
Remember, nfo (and m4nfo) goes *backwards* from its "action3" (m4nfo: "makeobject()" function), so let´s take a look first on def(50). Its function "callback()" checks for callback CB_ACONTROL (animation control) active, and in case starts our animation at frame 0. In case, it´s not a CB_ACONTROL, the function branches off to def(42).
Now, function def(42) checks for a potential slope (e.g., when building this object on a coast tile). Because this part of the code is only concerned with x-direction, it´s sufficient to check for one back (WEST+SOUTH) or one front slope (NORTH+EAST). On such slopes, no mole lights can be built, so these branches are left for special mole building on coast tiles (defs 10 and 11).
So, only if there´s no back or front slope tile, we carry on with def 41. Fortunately, we don´t have to check for a flat (water or land) tile, because we can only built moles on water. So, next thing to check would be for a mole´s free end (i.e., a clean water tile), because we´d like to allow the building of a mole light only at the mole´s end. This is done by checking the neighbour tiles in front and back of the mole piece for being "water" by using function "objinfo_water()": We first check for a possible water tile in front of our mole piece ("pos(1,0)"), and then at the back ("pos(-1,0)"). Depending on that direction (+1 in x == front, or -1 in x == back) we´re branching off to our animated mole light, i.e. our def(14) from above gets references for the back direction.
If no water tile is found ("ref(0)"), a normal straight mole piece in x-direction is being built.
Now, again we have left out three other possibilities, namely green light in y-direction and red lights in x- and y-direction. Imagine, these got defs 51, 52, and 53, we now come to an end of the whole exercise:
Code:
//------------------------------------------------------------------
// make 4 "views" (green light in x and y, red light in x and y)
//------------------------------------------------------------------
// graphics
def(60) getviews(
ref(50) if(0) // green light in x (this is our test case)
ref(51) if(1) // green light in y
ref(52) if(2) // red light in x
ref(53) else // red light in y
)
// menu
def(61) getviews(
ref(36) if(0) // green light in x
ref(37) if(1) // green light in y
ref(38) if(2) // red light in x
ref(39) else // red light in y
)
def(62) callback(
reftxtcb(warn_water) if(CB_TEXT)
ref(61) else
)
makeobject(_LIGHTS,
link(ref(62),MENU)
default(ref(60))
)
First of all, def(60) sets up 4 "views" which can be chosen from the building menu. def(50) is the green light in x which we have come along with today, and obviously, the other three variants are linked to the remaining three views.
Now, the same is done with the menu sprites, which had been set up elsewhere (defs 36 .. 39).
So, let´s finish our object: When in the building menu (MENU), control is transfered to def(62) checking for an additional CB_TEXT callback (displaying some help text) or to the menu sprites; and if not in the building menu, control is handed directly to def(60).
Yeah, this was the first animation tutorial, an easy one. Congratulations you came through! Soon, we´ll moving on to something more challenging. Stay tuned.
regards
Michael