Post tutorial Report RSS qc++ macro #define framer(...)

Replace the default quake-c frame macro with this slick piece of code.

Posted by on - Advanced Server Side Coding

What is the quake-c frame macro?
This macro starts with frame definition somewhere prior to the macro call:

// standing
//
$frame stand1 stand2 stand3 stand4 stand5 stand6 stand7 stand8 stand9

This is simply a count substitution - $stand2 converts to 1

Next comes the macro:

void() ogre_stand1 =[ $stand1, ogre_stand2 ] {ai_stand();};

This single line macro is expanded by the compiler:

void() ogre_stand1 =
{
self.think = ogre_stand2;
self.nextthink = time + 0.1;
self.frame = $stand1; // frame 0
{
ai_stand();
};
};

This is how the quake-c compiler reads this macro. iD Software created this - it is a builtin that never needs defined.

Pros:

  • plenty of examples
  • easy to visualize and make model frame code - 1 line = 1 frame
  • compiler makes think assignments forward (ogre_stand2 macro can follow ogre_stand1)

Cons:

  • a lot of typing or cut & paste needed to code models
  • uses 1 global variable for every void() {frame function}
  • time consuming to create and modify
  • more code to debug !

How can this be made better?

You could code up a loop function. That gets tricky - you have to individualize it for every frame set.

Or you could make your own frame macro - framer(). If you use fteqcc as your compiler you could do something I'm going to start using the label "qc++" to describe:

// implement a frame macro seqeunce where anything might happen in the frames
// t_ means think - had to use shortcuts - define macros have a 256 byte limit!

// per entity frame reset - fix self refer loops - another shortcut
// if t_on and t_to are both the framer function - an endless frame loop, this causes the frame sequence to loop correctly
.float framereset;
#define freset self.framereset

//excode;
// run any arbitrary code - SUB_Null() for nothing, you can NOT leave this blank!

// there are some define limits:
// size limit WARNING - the define code after framer() can NOT exceed 256 bytes - you can use any define substitutions you want - like freset.
// parameter limit WARNING - you can NOT use parm1, parm2...etc. - parms can NOT have numerics - use pone, ptwo...peight - also note: you can only have 8 parms!

#define framer(sframe,eframe,t_on,t_to,t_time,excode) void() t_on { if (self.think != t_on || freset == -666) self.frame = sframe - 1; freset = 0; self.think = t_on; self.nextthink = time + t_time; self.frame = self.frame + 1; if (self.frame == eframe) { self.think = t_to; freset = -666; } excode; }

// run arbitrary code in a specific frame, range of frames or set of frames

#define frame_arb(thisframe,arbcode) if (self.frame == thisframe) { arbcode; };
#define framerange_arb(thisframe,thatframe,arbcode) if (self.frame >= thisframe && self.frame <= thatframe) { arbcode; };
#define frameset_arb(thisframe,twoframe,threeframe, fourframe,fiveframe,sixframe,arbcode) if (self.frame == thisframe || self.frame == twoframe || self.frame == threeframe || self.frame == fourframe || self.frame == fiveframe || self.frame == sixframe) { arbcode; };

This looks pretty complicated, what does it do?

This define creates a pre-compiler (the function like call defined is expanded according to the definition prior to compiling qc code) macro to handle model frame sets.

Example:

void() ogre_stand1 =[ $stand1, ogre_stand2 ] {ai_stand();};
void() ogre_stand2 =[ $stand2, ogre_stand3 ] {ai_stand();};
void() ogre_stand3 =[ $stand3, ogre_stand4 ] {ai_stand();};
void() ogre_stand4 =[ $stand4, ogre_stand5 ] {ai_stand();};
void() ogre_stand5 =[ $stand5, ogre_stand6 ] {
if (random() < 0.2)
sound (self, CHAN_VOICE, "ogre/ogidle.wav", 1, ATTN_IDLE);
ai_stand();
};

could be handled:

framer($stand1, $stand5, ogre_stand1, ogre_stand6, 0.1, frame_arb($stand5,if (random() < 0.2) sound (self, CHAN_VOICE, "ogre/ogidle.wav", 1, ATTN_IDLE);); ai_stand(); );

The advantage here is very obvious - you can replace 1 line of code, 2 lines of code, 5 lines of code, or any (!!) lines of code with this one statement. It gets complicated in "excode" - handling individual frame arbitrary code. Thats what the frame_arb macros are for.

This is how it expands:

void() ogre_stand1
{
if (self.think != ogre_stand1 || self.framereset == -666) self.frame = $stand1 - 1;
freset = 0;
self.think = ogre_stand1;
self.nextthink = time + 0.1;
self.frame = self.frame + 1;
if (self.frame == $stand5) { self.think = ogre_stand6; self.framereset = -666; }
// excode
{
// frame_arb($stand5,if (random() < 0.2) sound (self, CHAN_VOICE, "ogre/ogidle.wav", 1, ATTN_IDLE););
if (self.frame == $stand5)
{
if (random() < 0.2) sound (self, CHAN_VOICE, "ogre/ogidle.wav", 1, ATTN_IDLE);
}
ai_stand();
}
}

What does this do?

A function called ogre_stand1 is compiled.
This function is a looping think function. Self.think will be called every t_time interval.
If self.think is not ogre_stand1 or freset is -666, self.frame will initialize to $stand1 - 1.
The think will be set to loop, and the time for the next call calculated.
Self.frame is then incremented and freset is set to 0.
If the end frame is reached think is set to the next function call.
In case this loops back to itself freset is activated.
Next excode is expanded to whatever was provided.
Here a frame_arb checks for $stand5 and runs the indicated code when that frame occurs.
More excode - "ai_stand();" is called every frame.

This will run the same frame set as the 5 frame macros it replaced. It will make the same function calls. You save 4 global variables and 4 lines of code.

Where might you still need the old frame macro?

Where the per frame code is very complex. Frame*_arb can be written just like regular code - using the define does not have to be inline:

frame_arb($stand5,
if (random() < 0.2)
sound (self, CHAN_VOICE, "ogre/ogidle.wav", 1, ATTN_IDLE);
);

Will compile just fine. However, for already written code, the gains of replacing every last frame macro are minimal if those macros have a ton of arbitrary code. Unless you feel the need to re-write it all !

You also need it where the individual frame functions are called by other code.
In our example, if ogre_stand4(); was called outside the frame loop - we would need that defined.

And you need them if you want to keep quake 1 death final frames:

void() ogre_bdie10 =[ $bdeath10, ogre_bdie10 ] {};

You dont have to keep this framing - you can make the t_to parm SUB_Null() and get the same effect with framer() - and have one less thinking dead monster. And one less line of code...

Warnings:

If you leave all warnings on, you will see a lot of:
ogre.qc:28: warning: Hanging ';'

fteqcc -Wno-mundane
Turns off warnings that have no effect on code operation - unused variables and the like.

Why would you want to use this versus the old frame macro?

A lot less typing is involved. You cant visualize the model frame sets with the macros - however, you can still use the $frame {...} code (which you still NEED - dont change these, they MUST match the model frames!) to visualize the model frames in code.

Less code to write is less code to debug. Its easy to screw up frame code in ways that compile. You can spend hours looking for the problem (or not even notice, if it is just one frame.) The trade off here is that you have to get frame*_arb() right. I would write these just like the old arbitrary code - dont make them all inline. Inline is fine for simple, but write the complex ones like regular code...

If you have a complicated mod like Archon - Moddb.com , or the HD pack - Moddb.com ... you might just want to save a few global variables. Why is that? Quake-c still has a hard limit of 32768 globals - and literally every unique identifier, literal and string is a global. It is possible (especially without compiler optimizing) to break that limit.

For me personally, I like the less debugging.

Good luck coding up frames!

Post comment Comments
numbersix Author
numbersix

Further research has uncovered other cases where you can NOT use framer() - without adjustments.

The framer() define is only setup to run sequential frames - where one model frame is displayed following the previous. This does cover 90% (or more) of model framing run in code. Any frame set that has out of order or other non sequential frame sequences will NOT be run properly by the base framer() code !

This frame code: void() lavacth $rise11, lavacthon_reblob2 ] {}; void() lavacth $rise10, lavacthon_reblob3 ] {}; void() lavacth $rise9, lavacthon_reblob4 ] {}; void() lavacth $rise8, lavacthon_reblob5 ] {}; void() lavacth $rise7, lavacthon_reblob6 ] {}; void() lavacth $rise6, lavacthon_reblob7 ] {}; void() lavacth $rise5, lavacthon_reblob8 ] {};

Runs the frames in reverse which would require some excode to do.
Mods could be made to framer() itself, but the define is already pushing the 256 byte limit.

Reply Good karma+1 vote
numbersix Author
numbersix

And I dont know why it screwed this up:
void() lavacth $rise11, lavacthon_reblob2 ] {};
void() lavacth $rise10, lavacthon_reblob3 ] {};
void() lavacth $rise9, lavacthon_reblob4 ] {};
void() lavacth $rise8, lavacthon_reblob5 ] {};
void() lavacth $rise7, lavacthon_reblob6 ] {};
void() lavacth $rise6, lavacthon_reblob7 ] {};
void() lavacth $rise5, lavacthon_reblob8 ] {};

Reply Good karma+1 vote
numbersix Author
numbersix

This frameset just does not copy and paste right - the $rise* frame sequence is what is important - even though this code as displayed will not compile.

Reply Good karma+1 vote
numbersix Author
numbersix

These frame codes:

void() do_lava_splash1 = [$splash1, do_lava_splash2 ] {};
void() do_lava_splash2 = [$splash1, do_lava_splash3 ] {};
void() do_lava_splash3 = [$splash2, do_lava_splash4 ] {};
void() do_lava_splash4 = [$splash2, do_lava_splash5 ] {};
void() do_lava_splash5 = [$splash3, do_lava_splash6 ] {};
void() do_lava_splash6 = [$splash3, do_lava_splash7 ] {};
void() do_lava_splash7 = [$splash4, do_lava_splash8 ] {};
void() do_lava_splash8 = [$splash4, do_lava_splash1 ] {Remove_Splash();};

void() player_grapfire1 = [$lightfatt1, player_grapfire2 ] {};
void() player_grapfire2 = [$lightfatt1, player_grapfire3 ] {};
void() player_grapfire3 = [$lightfatt2, player_grapfire4 ] {};
void() player_grapfire4 = [$lightfatt2, player_grapfire5 ] {};
void() player_grapfire5 = [$lightfatt1, player_grapfire6 ] {};
void() player_grapfire6 = [$lightfatt1, player_grapfire7 ] {};
void() player_grapfire7 = [$lightfatt2, player_run ] {};

Have non-sequential or other complex frame sequences.
These can be handled with excode, however, it really doesnt happen enough to be entirely worth the savings.

If you use framer(), you want to check the frame sets to make sure they run sequentially.
Or you have to write excode that adjusts self.frame to the proper value.

Reply Good karma+1 vote
numbersix Author
numbersix

Another discovery.

Watch using // style comments in code define will expand.

In other words - avoid this situation:

frame_arb($stand5,

if (random() < 0.2)

// play an idle sound
sound (self, CHAN_VOICE, "ogre/ogidle.wav", 1, ATTN_IDLE);

);

In theory /* comment */ style should not affect define expanded code.

Reply Good karma+1 vote
numbersix Author
numbersix

A necessary logic change eliminates the need for "freset".

I've discovered transitioning between the traditional frame macro and framer can spiral the frame loop off the end of the model frames. This breaks monsters.

The reset logic is changing to trigger on _any_ frame outside the loop.

I'll post the code update in the vault() when its ready and link it here.

Reply Good karma+1 vote
numbersix Author
numbersix

What happens when you define a function macro with length over 256 bytes:

compiler.qc:201: error: Macro framer too long (267 not 256)

If you see this, that is why.

Reply Good karma+1 vote
ceriux
ceriux

i'm curious in quake why cant you just say self.frame = 0; if velocity > 1 self.frame == self.frame +1;

if self.frame > 10 self.frame = 0;

else if velocity < 1

self.frame == 0;

i know thats not really how its written. but basically if you're moving your frame starts at 0 and continues to the end up the walk/run animation and if the frame is greater than the final walk frame, restart it and if you're not moving. your frames are a stand frame.

um.. why do you HAVE to use macros?

Reply Good karma Bad karma+2 votes
numbersix Author
numbersix

You can do that!
A velocity check is used to switch between stand and walk / run frames.

You could easily use the loop code you have there to operate run frames.
In fact, the player stand and run frames in player.qc are essentially that:

void() player_stand1 =[ $axstnd1, player_stand1 ]
{
if (self.velocity_x || self.velocity_y)
{
self.walkframe=0;
player_run();
return;
}

// other stuff

if (self.walkframe >= 5)
self.walkframe = 0;
self.frame = $stand1 + self.walkframe;

self.walkframe = self.walkframe + 1;

// other stuff
}

This creates a function that calls itself every 0.10 secs. The self.walkframe variable is incremented exactly as you describe. Just add the baseframe of the set $stand1, and it loops through the animation sequence.

The original frame macro was most likely created as a shortcut to manage running frame animation. Which ends up being a whole bunch of repetitive code mostly the same, except for frame number. Set the frame, set think for next frame function, and set think time.

The models were generally animated at 1 frame per 1/10th second.
The macro works out at 1 line or code segment per frame. Makes it easy to visualize what the model is doing.

You dont actually have to use macros.

They are just a code shortcut to minimize the amount of repetitive code needing to be written.

A slick trick in the original macro is that you can have a forward reference to a function that hasnt been prototyped!
void() player_axe1 = [$axatt1, player_axe2 ] {self.weap
void ) player_axe2 = [$axatt2, player_axe3 ] {self.weap
void ) player_axe3 = [$axatt3, player_axe4 ] {self.weap
void ) player_axe4 = [$axatt4, player_run ] {self.weap

In normal code, you would need:
void() player_axe2;
void() player_axe3;
void() player_axe4;

prototypes defined prior to the function calls. That saves a good bit of coding fluff as well, because there are a couple hundred frame op functions.

I just solved a problem where a prototype within a function body was creating a null pointer and crashing the engine.

I made my framer macro for one single purpose - run the same exact frame set, with the same internal ops code, however reduce the global variable count.

Every function, every pointer, every global variable, every literal (string, vector and float) - all take up a global variable. Even the local variables.
And the engine has a limit of 32768. It is easier to hit than you would think.

I was using frikqcc and I broke that with the mark II Archon code base.
Fteqcc alleviated the issue somewhat, and the -O{n} optimizations help.
But for some of the code I want, it will still be a show stopper.

You hit that limit - you can not add much new code. Then it becomes a challenge of ever diminishing returns. Remove an extra monster to get a few dozen globals. Blow those on a new weapon. And so on.

Thus my new focus on reducing globals where ever I can.

Reply Good karma+1 vote
numbersix Author
numbersix

>run the same exact frame set, with the same internal ops code, however reduce the global variable count.

In fact, I went to great lenghts - creating an entire frame test comparison package just to make sure the same frames were being run in the same sequence, with the same timing.

Reply Good karma+2 votes
ceriux
ceriux

can it be done for more than just walking and running. in the snippet you posted it still uses macros doesnt it?

void() player_stand1 =[ $axstnd1, player_stand1 ]

Reply Good karma Bad karma+2 votes
Teknoskillz
Teknoskillz

I like the alternate system, def has its benefits. I have found its alot more flexible when working with multiple models, like when I made the player swim model work in QC for example. The macro system is good if you are wanting to do some fast experimentations when doing dev.

Another thing you can do to add realism in the deathframes macros is add in a delay sub such as: SUB_FrameDelay (). Basicly there, you can adjust the .nextthink time randomly , and give the appearance a little more realism. In my mod, I use the negative health value, so the closer the result is to 0, the slower the animation for the ' deathframes' is. I also discovered another deathframe sequence by combining 2:

// New sequence, starts to sit, then falls backwards.
// Better results with this if you check for enough space behind the
// player , so that it dont wind up partially in a wall
void () player_die_01 = [ 61, player_die_02 ]
{
self.pos1 = HULL_CORPSE1;
self.pos2 = HULL_CORPSE2;
// final size corpse will be when image_travel is called

SUB_FrameDelay (); // call a delay for nextthink
};

void () player_die_02 = [ 62, player_die_03 ]
{
SUB_FrameDelay ();
};

void () player_die_03 = [ 63, player_die_04 ]
{
SUB_FrameDelay ();
};

void () player_die_04 = [ 64, player_die_05 ]
{
SUB_FrameDelay ();
};

void () player_die_05 = [ 65, player_die_06 ]
{
SUB_FrameDelay ();
};

void () player_die_06 = [ 66, player_die_07 ]
{
SUB_FrameDelay ();
self.corpseflag = CF_CORPSE; // half way through, we become gibbable
};

void () player_die_07 = [ 46, player_die_08 ]
{
SUB_FrameDelay ();
};

void () player_die_08 = [ 47, player_die_09 ]
{
SUB_FrameDelay ();
};

void () player_die_09 = [ 48, player_die_010 ]
{
SUB_FrameDelay ();
};

void () player_die_010 = [ 49, image_travel ]
{
SUB_FrameDelay ();

};


Reply Good karma Bad karma+2 votes
numbersix Author
numbersix

>can it be done for more than just walking and running.

Yes. You could write a loop of code to run any frame set that operates sequentially. Put in bounds checks.

This was what I was doing under frikqcc before I switched to fteqcc. I ran low on globals and it was the only way I could add monsters to the mod!

Reply Good karma+1 vote
Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: