|
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. |
|
|
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
Gremlin
#260 posted by madfox on 2009/05/14 11:42:52
Well I trried including a Gremlin in my map without the whole SOA pak. And as I expected was the hipgrem.qc not enough. I had to make a change to the ai.qc and defs.qc as well.
When I was done and could compile without errors the gremlin came in game.
But for some reason the weapon steal trick doesn't seem to go. I did all I thought I need to do changing the needed hipsdef into the normal def.qc but something slipped away.
Substitute
#261 posted by madfox on 2009/05/14 11:45:33
it was neg!ke with his pronouncion mark.
Well
#262 posted by madfox on 2009/05/15 21:24:30
i guess its a dubious question asking where the gremlin has left my weapon.
Bit Masks
#263 posted by necros on 2009/05/24 21:21:36
i want to be sure i understand these:
Operations:
self.flags = self.flags - (self.flags & FL_ONGROUND);
this will subtract FL_ONGROUND but ONLY if FL_ONGROUND is present in self.flags.
whereas:
self.flags = self.flags - FL_SWIM;
would just blindly subtract the flag and risk breaking self.flags if FL_SWIM wasn't there when you subtracted.
likewise:
self.flags = self.flags | FL_ONGROUND;
would add FL_ONGROUND but only if FL_ONGROUND wasn't present in self.flags.
whereas:
self.flags = self.flags + FL_SWIM;
would add the flag as well, but would break if FL_SWIM was already present in self.flags.
Conditionals:
if (self.flags & FL_ONGROUND)
is the only way to check if FL_ONGROUND is present in self.flags.
Correct
#264 posted by Lardarse on 2009/05/24 23:17:31
But the safe versions are strongly preferred, because they don't break anything.
Note that you can work with multiple bits if you need to. The code for picking up armor removes all 3 armor flags before adding the correct one.
The only operation I'm not sure how to do is to toggle a flag (if off, set it; if on, clear it).
Toggle
#265 posted by Preach on 2009/05/25 01:35:54
The following code will toggle flags:
self.flags = (self.flags | FL_ONGROUND) - (self.flags & FL_ONGROUND);
The right hand side features two halves. In the case that FL_ONGROUND is already set, the first half does nothing, and the second half is equal to FL_ONGROUND, so the flag gets removed.
In the case that FL_ONGROUND is not set, the first half turns it on, and the second half is equal to zero, so has no effect.
There's a nice kind of symmetry going on with this formula. It combines the adding and subtracting flags, but in such a way that only one actually does anything in each case. The whole right hand side is evaluated before the result is assigned to the left hand side, you there is no risk that self.flags changes mid way through.
It's worth knowing that this can be extended to multiple flags:
float IT_TOP3WEAPONS = IT_GRENADE_LAUNCHER | IT_ROCKET_LAUNCHER | IT_LIGHTNING;
self.items = (self.items | IT_TOP3WEAPONS) - (self.items & IT_TOP3WEAPONS);
This code will toggle the most powerful weapons in the player's inventory correctly, regardless of which ones they may possess. In practice, it would be worth following up that line with a call to W_BestWeapon(), in case the player was using one of the weapons you just toggled.
Thanks
#266 posted by necros on 2009/05/25 02:12:46
didn't know about the toggle one.
That Sound Bug
#267 posted by jdhack on 2009/05/25 06:11:24
You know, the one where you pick up 2 items more-or-less simultaneously, and hear only 1 sound. I was thinking of fixing it engine-side, but then I realized this: often, 1 sound overriding another is the desired behavior.
I know of the null.wav being used to kill a playing sound, but I suspect there are other situations. Can you guys give me some examples?
Upon Further Consideration...
#268 posted by jdhack on 2009/05/25 07:44:34
It probably makes more sense to look at it from the opposite side, ie. when don't you want one sound to override another?
So, picking up multiple, different items; using a key which causes a door to open; what else?
There's Also The Classic
#269 posted by Lardarse on 2009/05/25 08:33:19
"Quad Cancel" sound bug, where you land just after picking up the quad, and the "oof" overrides the quad pickup sound.
|
|
You must be logged in to post in this thread.
|
Website copyright © 2002-2024 John Fitzgibbons. All posts are copyright their respective authors.
|
|