|
Posted by metlslime on 2007/08/08 04:57:56 |
This is a counterpart to the "Mapping Help" thread. If you need help with QuakeC coding, or questions about how to do some engine modification, this is the place for you! We've got a few coders here on the forum and hopefully someone knows the answer. |
|
|
Good
#235 posted by madfox on 2009/02/07 17:52:32
explanation to make a monster invisible.
I'm still trying to find the right colours to improve Spectre in its translude way.
And as it is a sprite it has a funny way of walking. I think more darker colours on the outside and transparant inside.
http://members.home.nl/gimli/spectr.jpg
Can Be Shot But Doesn't Collide With Walking?
#236 posted by JneeraZ on 2009/02/13 21:52:05
I'm probably going to think of the solution 5 seconds after asking this but ... let's say I want a monsters corpse to be shootable but I don't want it to collide with other corpses or the player. I just want it to react to gunfire (both instant hit and missiles).
Doable?
Look At The Corpse, It's Voodoo QC Dancing...
#237 posted by Preach on 2009/02/14 01:59:02
It can't be done easily. I say that because darkplaces has an extension for just this purpose, where you can make an entity SOLID_CORPSE, which behaves like you say. The following is ugly, not only in itself, but also in the dark secrets about standard ID1 code it exposes. Read on if you dare...
The key thing is that you can't change the .solid status of things during touch functions, it can cause engine crashes. So, the vast majority of the time, you want your corpse to be the same .solid type as a trigger_multiple entity, which is SOLID_TRIGGER.
We're going to use the grunt as our test dummy here. This is the kind of thing you need to do in your death sequences, during the frame that usually makes the monster SOLID_NOT:
void() army_die3 =[ $death3, army_die4 ]
{
self.solid = SOLID_TRIGGER;
setmodel(self, self.model);
self.touch = corpse_touch;
self.health = 20;
self.takedamage = DAMAGE_AIM;
self.th_die = GibPlayer;
self.th_pain = SUB_Null;
self.ammo_shells = 5;
DropBackpack();
};
We make it SOLID_TRIGGER, then call setmodel in order to link it, which prevents a "trigger in clipping list" crash. We make it's touch function corpse_touch, see below. We also make it damageable again, give it 20 hp, and define both th_die AND th_pain, the latter being important if you don't want to reanimate your corpse.
So here's the deal. In this state(SOLID_TRIGGER), when a missile collides with one, you get a touch event where the trigger is self and the missile is other. You do not get one where the missile is self and the trigger is other! I'm not sure if this is an optimisation just for missiles, or that any entity touching a trigger doesn't set off its own touch function. In any case, we're about to cheat:
void() corpse_touch =
{
local entity oself;
if(other.movetype != MOVETYPE_FLYMISSILE && other.movetype != MOVETYPE_BOUNCE)
return;
oself = self;
self = other;
other = oself;
self.touch();
};
See what we're doing? The first bit of code is just so that we don't bother doing anything if we aren't hit by a missile. You might want to use a different criteria for what is a missile, I went for something that required no changes elsewhere. Then we create a touch event on the missile by the trigger, but by hand instead of from the engine.
Once you've done that, you might think we're done for missiles now, and you can give it a try by lobbing a grenade into a corpse. It'll explode on contact as desired (you can of course change the corpse's .takedamage to DAMAGE_YES if you want to prevent grenades from detonating on contact, I set it like this for illustrative purposes). However, if you fire some nails into your corpse, you might be surprised to find nothing happens.
The reason for this is due to some entirely redundant code in the original QC, which must have been moved engine-side for performance, but then remained in the QC source by mistake. Look at the start of spike_touch in weapons.qc:
void() spike_touch =
{
if (other == self.owner)
return;
if (other.solid == SOLID_TRIGGER)
return; // trigger field, do nothing
...
You'll actually notice the first line in the code for almost all quake missiles. It's totally pointless, as the engine never makes collisions between entities and their owners. Just think how many times that code has been called in a million deathmatches across the world, and not once has it been true...
As we discovered near the top of this section(and to be honest I never knew about this before I tried today), SOLID_TRIGGER entities don't generate touches on missiles either, so the next line is similarly pointless. To get things working, comment both statements out, and do the same in superspike_touch. If you're bored, get rid of the self.owner lines in the grenade and the rocket.
If all has gone to plan, at this point all the missiles will damage the corpse.
Join us after the break when we investigate the shotgun and other tracelines...
You Can't Talk To A Man With A Shotgun In His Hand...
#238 posted by Preach on 2009/02/14 02:59:57
Before we start the business proper, I should alter you to two bugs with the code which only became apparent once the grunts started hitting corpses with their shotguns. The first is that when monsters begin infighting, they don't realise that their target monster has died when they become a corpse. This is because they consider their enemy dead if their health is below 0, but the corpse has positive health. The fix for this is left as an exercise.
Also, if the corpse gets damaged by another monster, it will get resurrected. To prevent this, add the line
self.flags = self.flags - (self.flags & FL_MONSTER);
when creating the corpse.
~~~~~~~~~#~~~~~~~~~
Ok, so the question is, how do we deal with weapons that use a traceline. And the solution is we make the corpse solid only for long enough to perform the tracelines. We in fact toggle the solid state of all the corpses in the map. Although in theory we might end up changing the solid status of something during a touch function(if a touch caused a shotgun to fire, for example) we hopefully* put everything back before the engine can notice.
So the simplest way I could think of to change all of the corpses to solid and back was: give all of your corpses the classname "corpse", then use the following functions to toggle the solid status:
void() corpse_solid_on =
{
local entity corpse;
corpse = find(world, classname, "corpse");
while(corpse)
{
corpse.solid = SOLID_BBOX;
setmodel(corpse, corpse.model);
corpse = find(corpse, classname, "corpse");
}
}
void() corpse_solid_off =
{
local entity corpse;
corpse = find(world, classname, "corpse");
while(corpse)
{
corpse.solid = SOLID_TRIGGER;
setmodel(corpse, corpse.model);
corpse = find(corpse, classname, "corpse");
}
}
The trick is then to surround calls to traceline with these functions. The axe is very simple, just put corpse_solid_on(); on the line above the traceline and corpse_solid_off(); on the next line after it.
For the shotgun, it's worth remembering that there are lots of calls to traceline in a single blast, and since toggling the solid status is fairly expensive in terms of performance, we should do it once per blast. So sandwich the functions around the
while (shotcount > 0)
{...}
block.
The lightning gun is left as a small exercise, but it's only really worth wrapping the first traceline in the function, the other two are basically enormous bugs which speedrunners exploit to kill things through walls.
That's almost it, but there's one finishing touch. If you go up point blank to shoot a corpse, you'll find that your shot disappears, or attack does nothing. This is because the traceline starts inside an entity, and so the returns from the traceline are a bit weird. The telltale signs of this happening are:
trace_fraction == 1 (not 0 as you'd expect)
AND
trace_ent != world (if it is world, then the trace really did hit nothing)
So what we do is add a bit of extra code which can cope with this case to the various weapon attacks. As an example, add the following code just below
if (trace_fraction != 1.0 )
TraceAttack (4, direction);
in the shotgun firing code:
else if (trace_ent != world) //if so, we are stuck inside what we have hit
{
trace_endpos = src + direction*16; //set some reasonable values
trace_fraction = 16/2048;
trace_plane_normal = -1 * direction;
TraceAttack (4, direction);
}
The code for the axe/lightning gun is similar, but if people get stuck on that exercise I can supply good fixes.
Well, that wraps things up for today. I would say that this latter part is quite an ugly hack, but the code in the first post is actually fairly clean. In all cases you should ask yourself "Would I show my mother this code" before you do something you might regret.
As a final parting thought, you might want to adjust the height of the bounding box of the corpse from the default, so that shots at head height don't blow up the corpse. If you do, remember that you need to do this in both of the corpse_solid toggling functions.
*I say hopeful, but I'm pretty sure it should work. I'm just not sure how careful the engine is at checking, and it's such an awkward thing to create a test case for. So I'm gonna be cautious in my endorsement.
#239 posted by JneeraZ on 2009/02/14 11:14:18
Preach, you are fucking awesome. :) Thanks man, I'm off to digest all of that.
#240 posted by JneeraZ on 2009/02/14 12:59:11
Absolutely beautiful, that all worked. Thanks again!
Yeah
#241 posted by ijed on 2009/02/18 14:05:06
That's very nice, thanks for the explanations.
Bit Of A Backtrack
#242 posted by Preach on 2009/02/19 22:17:00
A few pages ago I said QC only checks the first component of a vector when evaluating boolean logic on them. This is not always true.
if((!'0 1 1'))
will work correctly - notice the double brackets. If you take the inner brackets away, then it stops functioning correctly on compilers which perform optimisations. Here is why:
When an if statement is compiled, it gets boiled down into an instruction to skip over the next n lines conditional on the value of an integer fed to it. QC actually supports two operations: IF and IFNOT. The former skips a number of lines when the integer is non-zero, and the latter them if it equals zero.
When the original QC compiler encountered code like:
if(!my_number)
where my_number is a float in qc, it would actually break it up into two instructions: The first one would apply the logical operator ! to my_number, and store that in a temporary. The second instruction would then feed that temporary to an IF_NOT.
One of the first created QC compilers saw a chance to optimise there, change the IF_NOT to an IF, and skip the ! instruction entirely. This sounds reasonable enough, except that you must not do it for a vector. There is a special operation for applying ! to a vector, which actually applies it to all three components. If you don't use it, then the first component of the vector gets mistaken for a single float, and you get bugs.
In conclusion, the original QC design was perfect and Carmack remains a god. The bug lies in the newer compilers, which should not optimise in the vector case.
Coming up soon is a description of the sequence in which things occur in a frame, which is the reason I was poking about in the source.
#243 posted by JneeraZ on 2009/02/19 22:27:00
Are optimizing compilers really even necessary? I mean, has Quake ever been slow when fed reasonably coded QuakeC?
Possibly
#244 posted by Preach on 2009/02/19 23:52:12
QC probably used to be a potential bottleneck, back in the day when r_speeds of 800 was considered unacceptable. The engine contains a "profile" command to see the 10 most demanding QC functions, so they were being optimised back in the day. Probably this is where some of the builtin functions come from, like find and findradius, which could be written in pure qc given the earlier builtins.
Nowadays modern limit-pushing maps increase the number of faces by a greater factor than they do number of monsters/entities(most of which are idle and no serious drain anyway). In addition, much of the greater rendering cost gets push onto the graphics card.
So in that sense bad QC is unlikely to cause a framerate drop or anything like that. But there are other limits that have to be fought when it comes to quake. There is a maximum size that a progs file can be, for instance, and compiler optimisations which eliminate duplicated strings in the file have helped large mods compile.
Also, the runaway loop counter in qc is 100000 instructions in a function. If you're coding some kind of loop, then any instructions you can eliminate from the inner loop section are invaluable in avoiding the runaway limit. The findradius builtin mentioned above swaps what would have been a loop through all the entities with a pass through a linked list of the relevant ones.
Of course, there's also some personal satisfaction involved with some optimisation. I've been playing a kind of "programmer's golf" with the ID1 progs, trying to get as few instructions as possible in the monster ai functions like "visible" and "range". At the moment I'm trying to resist writing them directly in assembler to eliminate another temporary variable...they're the best targets for optimisation if you'd like to make a 20,000 monster map support 25,000 instead!
Tracebox In QC
#245 posted by Preach on 2009/03/07 14:31:16
Nobody asked for it, but here it is anyway: A way to perform a tracebox, similar to traceline but with a full sized object, without engine extensions. Caveats: The emulation of a trace is not perfect; it can throw a false negative in the case where the trace travels through a small but non-zero vertical distance. The test has been designed to prevent false positives. In addition, it's quite expensive in QC operations, although look to the previous post to see whether you should care about that.
"Your mother's a tracer!"
So, the key to being able to trace is, strangely enough, flying monsters. Most of the box-tracing which the engine performs is during physics updates. This makes them unsuitable for testing traces "on-demand", since you have to give control back to the engine first. However, monster movement is performed in discreet steps, moving instantly from one point to another using the walkmove function. Flying monsters have the additional advantage of not being bound to the ground, so we create an entity with FL_MONSTER and FL_FLY set to perform our trace.
The carrot and the stick
It's important to understand exactly what causes a flying monster to change altitude. Every time walkmove is called, the moving monsters decides if it is at the right height, too low, or too high. Accordingly, it attempts to trace a move straight forwards, forwards and up by 8 units, or forwards and down by 8 units. It makes the decision based on the height of its enemy relative to itself. So to control which way our monster goes, we need an enemy entity whose height we can set. This is quite like the carrot on a stick you wave in front of a donkey to guide it onwards, and I shall refer to it as the "carrot". Since our donkey can fly, it will be named "pegasus".
Running the course
The first problem is that we can only climb 8 units at a time. The solution is to call walkmove repeatedly, dividing up the trace up into segments, where each segment has a z component of 8. Since it is unlikely that the trace will have a height exactly divisible by 8, we need to perform an initial run which stops less than 8 units away from the target height, then reset the origin of pegasus so that it lies on the path of the trace, 8 units from the target height. Full marks for whoever spots the tricky boundary case of this method.
Preach's Pro Tip: You can use this trick of calling walkmove more than once to get regular flying monsters to ascend/decend more rapidly!
Jumping the fences
The next problem is what happens when the movement of the pegasus gets blocked. What few people realise about walkmove is that if the trace is blocked, the monster doesn't move at all - I personally imagined it would move the monster as far as it could before being blocked. But if the movement with vertical motion is blocked, the code does attempt a move with no vertical motion before giving up. This could create false positives easily. To make sure that pegasus is properly jumping the hurdles, check that the height of the pegasus changes from one walkmove to the next. This is the simplest test, as it doesn't matter if the trace is going up or down. Of course, you shouldn't do this if the trace is truly flat!
The final straight
A big problem arises if you want a trace which isn't flat, but rises/falls less than 8 units - a shorter distance than a single walkmove. This is the corner case astute readers may have caught earlier. It might be tempting to give yourself a "run-up", to trace from a position behind the desired origin, through the desired start to the finish, so that this extended trace has the right height, and accept the false negatives that the longer trace might cause. The problem is that this might actually cause false positives!
If the pegasus starts within a solid part of the bsp (or possibly other object), then it seems to skip through without colliding. As traces get closer to 0 height, the pegasus takes a longer and longer run-up until it is exceedingly likely it will be outside the level. So instead, the best solution I could come up with for this case is: Increase the height of the pegasus by the desired height, adding it to the maximum height if the trace goes up, and the bottom if the trace goes down. Then perform a horizontal trace. If you look at the 2-D side-on view of the trace, this is effectively taking the rhombus which would be the exact trace, and replacing it with the smallest rectangle which could enclose it.
Tracebox In QC II
#246 posted by Preach on 2009/03/07 14:35:13
You can lead a horse to water...
Through the steps in the previous post, we've built a reliable tracebox which reports few false positives and no false negatives. So it's time for a little nicety - stopping splash noises. The splash noise occurs when the pegasus crosses a water boundary, but since the pegasus is an imaginary beast, no noise should occur. To avoid it, we test for water by performing a regular traceline from start to end. If trace_inwater i set, then we add 1000 to the height of the trace_start and trace_end, then subtract 1000 from the mins and maxs of the bounding box(thus the box remains in the correct place. Although you could construct a map where a splash would still occur, I think it's unlikely to arise in actual maps.
Photo finish
And that's it. I'm sure I can hear you saying "That's all very well in theory Preach, but it sounds like a bitch to actually implement!" Fear not, for here is a qc file with just such an implementation. In this version, the carrot and pegasus are globally allocated entities, which reduces the overhead of spawning them each time you want to use the function. This could be changed if entities are at more of a premium than function cost.
http://www.btinternet.com/~chapterhonour/tracebox.qc
As a final warning, remember that the trace results are invalid if the tracebox starts outside of the level. When determining this, you have to account for the 3 hull sizes which quake supports. Work out which hull the tracebox fits to, and fit THAT inside the level. This almost always means that you need a player sized space!
Bloody Hell
#247 posted by Lardarse on 2009/04/20 14:07:52
(You missed a trick with the post icon, btw...)
All we need now is tracetoss, and we have a much more dangerous AI waiting to happen for several monsters...
#248 posted by necros on 2009/04/20 19:59:44
you could try to approximate a toss path with a while loop. maybe in just 25% increments as you don't need a ton of precision.
Adapting For AI
#249 posted by Preach on 2009/04/20 20:18:35
You can use a similar, but much simpler trick for use with a monster, which would allow it to predict if it can navigate to a certain spot in the future. You again spawn an entity, and we'll call it the pegasus here for ease of comparison. Give the pegasus the same movetype, size and origin as the monster, and make the owner of pegasus the monster.
Then just perform some short distance walkmoves with the pegasus - best to do 4-5 each of a distance around the average walk speed of the monster. Then just see if they succeed or fail. The easiest way is just to see if the walkmoves return true, but you might instead want to test if the pegasus is within a certain distance of a target spot after each step.
You can also substitute walkmove with movetogoal, which attempts to navigate around obstacles. Then the question you're asking is more "will the monster be able to get near the goal if he is given a second to wander about?". I remember using this when coding the scripted death sequence for the final Travail boss, to ensure he could actually walk to a spot where the sequence would make sense(and sneakily teleport him if he was stuck!).
You can actually do this without the need for a proxy entity like the pegasus. Just save the original origin of your monster, and then walkmove it as many times as you need to test whatever it's planning. Once you've decided what the best navigational plan is, reset the monster's origin back to where it started.
(If I have the time tonight, I'll do a little post about making ogres shoot at angles without cheating by changing the overall velocity of the grenades. I'll also put tracetoss for grenades into that.)
#250 posted by Lardarse on 2009/04/20 23:57:52
I was more thinking for fiends, so that they won't jump at you if they can't reach you.
Ok, Some Thoughts
#251 posted by Preach on 2009/04/21 01:27:05
The way a projectile moves in quake can be described by the increment in it's position and velocity per frame.
1. new_velocity = old_velocity + frametime * '0 0 -800'(assuming that sv_gravity is still 800).
2. new_position = old_position + frametime * new_velocity
(Maths people may recognise this as the Euler method - it's applied to each section separately though)
So if you want to trace a path up until the first contact with something, then you just need something that iterates traceline in this fashion until one collides. Here's me writing a qc function straight onto func:
void tracetoss(vector posa, vector vel, entity ignore, float nomonsters, float inc)
{
local vector posb;
local float startz;
if(inc <= 0)
inc = frametime;
startz = posa_z;
//cut off if it falls too far, like out of level
while(startz - posa_z < 1024)
{
vel = vel + inc * '0 0 -800';//lazy way to do gravity
posb = posa + vel * inc;
traceline(posa, posb, nomonsters, ignore);
if(trace_fraction != 1)
return;
vel = vel + inc * '0 0 -800';
posa = posb + vel * inc;
traceline(posb, posa, nomonsters, ignore);
if(trace_fraction != 1)
return;
}
}
Notice that you can specify the increment you want to use, with the default being the current frametime. Also, the duplicated code inside the loop is somewhere between double buffering and loop unrolling, and is solely for optimizing what could be an intensive loop. The idea is to look at trace_endpos to find where you hit. The maximum height could be made a function parameter, I was trying to keep the function simple.
Similarly, the proper way to do the gravity would be
local vector g;
g = '0 0 -1' * cvar("sv_gravity");
OUTSIDE OF THE LOOP
To make this simulate grenades best, use nomonsters = 2, this makes the trace more generous with collisions in the same way that flymissile does.
"But I didn't want grenades!" I hear you cry. What can we do about fiends? Well, you need to do something similar to this, but using tracebox. Now, the first thing I would recommend is not to just call tracebox as a function, because that might just cause you to exceed the runaway loop counter. Instead you should duplicate the qc-side tracebox code, and incorporate this new loop into that, so that you're only setting the size of the pegasus once, letting it maintain it's origin from one step to the next etc...
The other trick that I would recommend is to pick the time increment you use very carefully. In fact, I'd recommend that you pick the time increment so that at every step the z_difference you require is exactly 8 units, so that it can be done in a single call to walkmove. You have to swap the order that you do things in for that to work:
FIRST trace the new position based on the old velocity(using the old velocity to calculate the timestep)
THEN update the old velocity with gravity based on that timestep.
So the timestep you seek is abs(8 / old_velocity_z). Check for vel_z = 0 if you like. One final thing to remember is that when the sign of old_velocity_z changes from positive to negative (if it does) then you need to move the carrot from being waaaay above the pegasus to waaaay below it!
Now, here comes the gotcha. What do you do once you've performed the trace? If you hit something, you somehow need to decide whether that was the player or the world, and qc-tracebox can't do that (it's hard being a quake monster and therefore entirely blind, isn't it?). You also have to remember your ending position doesn't include the last step of the trace, as that one failed! The best suggestion I can think of right now is:
* Once you have your end position where you hit something, work out how far the player is from you ignoring the z position
* Try to tracebox horizontally towards the player to end up 64 units away from them (using the distance just calculated), again ignoring the z axis.
* Once that tracebox is ended, check if you're within, say 128 units. If so then the jump probably hits. Otherwise it probably misses.
Some of those numbers may need tweaking. I was basing it on the maximum seperation between a touching fiend and player being ~64 units (corner to corner ignoring z axis). Again, you want to trace as close as you can get, without actually colliding with the player, because then the trace fails - like going bust in blackjack. You could also split the horizontal trace into a few smaller steps, which might protect you from going bust.
And I totally ran out of time(and characters) to do anything about ogre aiming. Maybe tomorrow...
Gremlin
#252 posted by madfox on 2009/05/11 00:20:07
I was trying to add a gremlin to the standard qcc and of course I got a lot of errors.
When I finally had cleaned them up I started the game and reached:
progs.dat system vars have been modified.
progsdefs.h is out of date.
It might sound familiar, but is there a solution?
Defs.qc
#253 posted by Preach on 2009/05/11 00:37:22
Check the changes you made to defs.qc, you can't add new fields to that file until all of the system fields have been created.
Typically
#254 posted by ijed on 2009/05/11 01:29:55
It means you've added new stuff too high up - put your changes at the bottom of defs.
Confirmed
#255 posted by madfox on 2009/05/11 01:36:54
I added four strings to the defs.qc as they were the errors I recived after compiling.
float visible_distance;
.float gorging;
.float stoleweapon;
.entity lastvictim;
How can I make addittions to the defs.qc , or should I delete all functions in grem.qc that work with doe.qc
Fixable
#256 posted by Preach on 2009/05/11 01:40:10
Like ijed said, just move those lines right to the bottom of defs.qc, and you should be fine.
Yes!
#257 posted by madfox on 2009/05/11 01:45:47
that worked.
You earned your pronounciationmark back Ij!ed.
Alhough I don't see it steal my gun, it works fine.
Hit That One
#258 posted by ijed on 2009/05/11 02:04:18
Alot of times. When you're learning blind a molehill is everest.
!
#259 posted by ijed on 2009/05/11 02:04:46
|
|
You must be logged in to post in this thread.
|
Website copyright © 2002-2024 John Fitzgibbons. All posts are copyright their respective authors.
|
|