News | Forum | People | FAQ | Links | Search | Register | Log in
Coding Help
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.
First | Previous | Next | Last
 
ohhh ok, yes! that explains some OTHER whacked out behaviour i've been seeing!

i've changed movetogoal into a wrapper that (among other things) does a movetogoal(1) and then calls x number of walkmove(directionItMoved, 1) to complete the amount of movement requested.
this allows a monster to continue to move forward, even if it's full movement would have failed allowing it to get through thin or cluttered areas better.

i was seeing some weird stuff where a monster would touch a trigger and then not move (it was moving by 1, but not completing the walkmoves).

i guess it never mattered before because movetogoal was solely called at the very end of a qc function.

anyway, i've added in your suggested change and will test it today. :) 
Binary Chop 
If you like, there's a trick you can apply there to only call ~log(x) steps in the worst case rather than x for moving x distance.

The trick is try to walkmove(x) first - being optimistic!

Check the return value for failure, and we can finish if we succeeded.

If we failed, replace x by with x * 0.5. We then try to walkmove(x) with the new x.

We keep repeating the above step(in italics) until x is smaller than 1.

Note that after the first iteration we don't need to check for success.

Worked example:
Suppose we want to walk forward 32 units but only have space to move 21 units.

First time through walkmove fails
x=32, moved=0

Second time through we succeed
x=16, moved=16

Third time round we fail as we are 16 forward and another 8 would take us past 21
x=8, moved=16

Fourth round we succeed
x=4, moved=20

Fifth fails
x=2, moved=20

Sixth succeeds
x=1, moved=21

Since x has reached 1 we terminate.

I'm making the numbers quite friendly by choosing x as a power of 2. It also lets me gloss over the edge cases of when to reject x and stuff. Nonetheless the method is sound and a fairly simple loop. Probably the hardest idea would be proving that it always works, I've always been fond of Proof by Single Worked Example though... 
Yeah 
i've been meaning to do that. :)

i wasn't sure if there was a point where doing it linear was faster or if it was always slower and i just haven't been in the mood to try to figure it out. ^_^;

it looks like you're saying it's always faster?
also, while we're talking about walkmove and such...
is it ok to call a walkmove/movetogoal larger than bbox size?
ie: walkmove(yaw, 256) as opposed to looping walkmove (16) 16 times.
not sure if this messes up collision if the bbox is displaced by huge amounts... 
Empirical Answers 
Yeah, the binary chop should always reduce or equal the number of calls to walkmove in the linear algorithm. The nice thing about it is getting twice as high precision only costs you 1 more call to walkmove, although I doubt anyone needs monsters THAT much more finely tuned than 1 unit.

I tried to pick through the engine code to decide if you could get away with long distance walkmoves. But it's the densest part of the engine code and although my suspicion was that collisions would get skipped I wouldn't be able to answer with confidence.

So I bashed away through the latter half of Lewis and made a test mod. The emperical conclusion: no.

You can make a monster walkmove 256 units and completely skip a 64 unit trigger. So yes, limit yourself to width of a bbox per iteration. You can be assured that you won't ever skip through walls of the game world. BSP entities don't count though... 
 
You can be assured that you won't ever skip through walls of the game world.

this is more what i was asking. mainly the use for super long walkmoves would be to check for charging/leaping abilities like fiends instead of just checking if they can see the player.

a walkmove check of even just 16 or so units will stop a fiend from doing that psycho leap 'bug' around doorways.
otoh, it will also cause it to fail more often as a jump that would hit a wall yet allow it to bounce towards the player (and hit him) would fail unlike with just plain visibility checks.

you could possibly make a 'robust' walkmove that, when failing to move, would tweak angles by a small amount and try again.

hmmm... 
Rollback 
I was about to warn you about using walkmove checks(where you want to reserve the right to rollback the movement and do something else) too freely. What if you walkmove into a rocket during a 'check'. Luckily you are largely protected from this kind of thing. This is because the only thing that monsters will collide with during a walkmove or movetogoal is SOLID_TRIGGER entities.

So really all you need is to watch your triggers which respond to monsters when you're doing that kind of thing. In standard quake that's only really telefrag and trigger_hurt - which you could guard against with a quick change of takedamage.

If you really get into that kind of thing you can go down the route of having a 'proxy' entity instead. I was trying to think about what you'd need to do to get the proxy perfect. The think you'd need to get right is .owner, which would take 3 transformations:

1)Set the owner of monster's owner to the proxy.
2)Set the owner of proxy to the the monster.
3)Set the owner of everything else in the world currently owned by monster to proxy.

You also need to store enough information to reverse all these changes.

This is of course absurd levels of effort. Just set the proxy's owner to the monster, and accept that occasionally it'll be blocked by an entity that owner settings would have let the monster walk through. Never realistically a problem. 
 
thanks, i hadn't thought about the 'premature triggering'. yeah, a proxy looks like the best bet.

when i needed a walkmove check to move through monsters, i just set all monsters to non-solid. :P 
 
or a flag on the entity you toggle on temporarily that you code into all triggers 'don't activate when this flag is on'. 
Addendum 
The takehome message of the last post is:
During a walkmove or movetogoal the monster will only collide with SOLID_TRIGGER entities.

I want to add that only the trigger's touch function is activated, the engine does not call the monster's touch function with the trigger as other. 
In Error 
So I adapted the code which I wrote last week answer that question about movetogoal. It turns out that even through touch functions, it restores the QC state correctly and doesn't overwrite self. This is admittedly run from a think function - it's possible that you could create a more complex scenario like calling movetogoal from a touch function and then causing another touch function.

But I thought I'd share it with you because it made me practice a few diagnostic tools for QC, some of which you may not be aware of.

The first is just chucking in a load of dprint statements, I'm sure most people have done that before. Don't forget that you can use ftos() and vtos() to put more information into the statements. If you need to confirm the value of an entity variable (as I wanted to with self) you can use the eprint() function instead.

The second tool is a bit of a hack. It allows you to get a stack trace. This was particularly valuable in this case because of the concern that the touch function might be trashing the stack. But it would be of use in any case where you have an interaction between think and touch functions causing a bug, and you want to know what is calling what.

There's no command or QC builtin for a stack trace, but you get one if you manage to perform an invalid operation in QC. The infinite loop is one way to do this, but personally I prefer the null function call, it's a bit quicker. Just throw something like

world.think1();

and unless the map is particularly weird you'll get your error. Of course, this does shut down the entire server, so it's not for general use.

The last trick is pretty powerful but also has the potential to spam the entire console out. The QC builtins traceon() and traceoff() allow you to enable console output of every Qasm instruction that the server reads - including the register values that were employed.

I believe Qasm is a neologism, and if so I demand it be pronounced as "chasm". It refers to the bytecode that a QC compiler creates - effectively Quake Assembly code. You can use one of the options in FTEQCC to output the assembly to a file, which is a good way to learn what the trace output means.

(As an aside FTEQCC also lets you enter segments entirely in Qasm. This is in the same way as c compilers accept asm segments - and indeed the original quake engine did just that in the renderer to get tight loops fast enough for the Pentium 75s to run the game. I believe most modern ports strip the assembly out. Even so, in quake Qasm is a great way to do naughty things that the compiler won't let you...) 
Are You Sure? 
because i distinctly remember after putting in the self restoration bit that it fixed a problem with walkmoves not being called after movetogoal. 
Yeah 
We might have to share some code here to determine why we're getting different results. If you whack an eprint(self) in before you restore self do you get the trigger entity that you touched come up? 
Heh 
yeah, i guess i could have at least tried it out to see.

you're right, self isn't being lost when touching triggers. trying to figure out now would be pointless though, because the code has change by a huge amount since last time i posted about it. 
SV_TouchLinks: Next != L->next 
is it bad to call setorigin(self) in a touch function? does that cause the touchlinks warning in fitz?

i googled the forums, but i don't think it was ever really explained what this error is, only that it used to crash in e2m2. 
Potentially 
I'm not brilliant at following how the engine handles this touch stuff, but here goes:

The following functions could cause problems with sv_touchlinks:

setorigin
setsize
droptofloor
movetogoal
walkmove

All these functions can cause the entity to be relinked, which potentially causes the issue. There are some other conditions that need to be met before the error is encountered: firstly the entity has to be part of the same areanode as the touch is coming from*. My understanding is that there are 32 areanodes in a map to reduce the number of entities considered in the collision code by approx. that factor.

The other important thing is that the entity has to actually break the chain of linked entities at exactly the point that sv_touchlinks is operating. I believe that this translates to applying any of the dangerous functions to other, but again not too sure how all the code fits together.

A final small point is that even if do apply one of the dangerous functions to other (and since we're in a touch function with other it's safe to assume by now that they share an areanode) we might still get away with it. This case would occur when other is the first entity in the areanode - relinking it will reinsert it in the same place as before. There's no safe way to exploit this though, as it's determined entirely engine-side.

The last thing to remember is that sv_touchlinks only looks through the list of triggers, so anything that's not SOLID_TRIGGER* ought to be safe. Best of luck!



*Technically that should read "that wasn't SOLID_TRIGGER last time the entity got linked into the world" but that's a bit of a mouthful. All that means is that you can't quickly set the entity to SOLID_NOT and get away with anything, it's about whether the entity is truly a trigger according to the engine at that time... 
Footnote 
That first asterisk should have been removed, the footnote is about the SOLID_TRIGGER statement - though most of you spotted that anyway I expect... 
 
yeah, this is happening with some of my code. unfortunately, i have the report from a third party and haven't been able to reproduce it myself. apparently, occasionally, it spams the console with sv_touchlinks errors in fitz085.

i noticed this tiny comment in defs.qc for setmodel:

void(entity e, string m) setmodel = #3; // set movetype and solid first

and i checked that entity and found that i was setting self.model before movetype. so i fixed that anyway, but maybe that might also have been the problem? the entity wasn't properly linked in the first place?

geez, i dunno. :P

it's supposed to be a visible solid_trigger entity that changes origin when you touch it.

i was wondering, maybe if i used to bad method of just setting self.origin in the touch function, that way, it's not breaking whatever links? 
Danger 
From what I've gleaned, that is the most dangerous thing you could do in a touch function, although I didn't post it correctly. I was talking about the danger of moving other, but I should have been talking about self. I had the two muddled up and I'm sorry about that.

The standard way to cope with this is to set up a quick think function from the touch function, and have that think reset the origin. You can put a guard into the touch function to prevent multiple touches before it moves - set up a flag on the entity and toggle it in the touch and think. 
Hm Ok 
so what i can do is just set a flag after the touch function to disallow further touches until the think function has been run. that should work i guess. a little roundabout but last thing i want to do is start causing crashes. :P 
Speed-up 
You can also use a little trick to make sure the think function runs as soon as possible (either this frame or the next). Just set self.nextthink = 0.05; - note that we are deliberately omitting time from the assignment. Since this will be in the past for every frame the engine runs (frame 1 runs at time = 0.1) it will execute the think as soon as the entity is checked by the engine.

The flag works a bit like the way that
if (self.nextthink > time) line works for a trigger_multiple, so if you don't want to use another field that approach is an option. My feeling is that the flag is simpler because you don't need the flexibility of a custom delay but it's basically a preference thing. 
Spot The Deliberate Mistake? 
(frame 1 runs at time = 0.1)

Time starts at 1, not 0. This is one of the quirks of the system, that makes very little difference, except for when it trips you up (and when it does, it hurts). For example (from the id1 code):

self.nextthink = self.nextthink + random()*0.5;

This line appears in all of the $foomonster_start functions. The original intention was for monsters to not all do their setup on the same frame, to reduce computer load. However, what actually happens, is that on a roughly 1 in 32000 chance (or, according to LordHavoc, 1 in 2 billion on Linux), self.nextthink is set to 0, which means that it never thinks again (as .nextthink is set to 0 before the think function is called), and the rest of the time, it happens on the next frame. 
Thinking About The Other One 
When a think function is being called, what is other set to? Does it get set to something predictable, or is it just left as whatever it was last time? 
 
so that bit of code doesn't actually do anything then?

might be a good idea to change to it 1 + random() * 0.5 then? because that must mean thinks are being all processed on the same frame.

also, on other, a few think functions steal other for their own uses so even if it is being reset every frame, you might get a semi-random entity if one of those thinks happened during the frame? 
I'd Say 
(1 + random()) * 0.5 
 
won't that still generate nextthinks < 1? 
First | Previous | Next | Last
You must be logged in to post in this thread.
Website copyright © 2002-2024 John Fitzgibbons. All posts are copyright their respective authors.