|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.|
So, here's what I'm trying to do right now, and probably nobody knows the answer but here goes. What I'm trying to set up is an entity that emits a constant sound, but is also toggleable. So when you turn it off, the sound goes silent, and when you turn it on, the sound starts up again.
Problem 1: if you are out of hearing range when it turns on, the object will be silent when you get close.
Problem 2: if you save and reload the savegame, the object will be silent.
The basic cause is that sound() calls are fire-and-forget and quake assumes the sounds are short-lived so the above situations won't be much of a problem.
I've tried various solutions, the latest is various versions of "restart the sound every 1 second or so." The problems with that are first, when you approach the object its sound starts up suddenly instead of fading in, and second, you can hear obvious repetition when standing next to the object.
Increasing the frequency of sound() calls reduces the sudden start, but worsens the repetition. I'm going to try and improve this hack by having 3 or four different sounds, and playing them at random, so that you don't hear any obvious repetition of sounds.
it is possible, but it's slightly hacky.
unfortunaly, i don't remember who told me about this method... it may have been lordhavoc, but i'm not sure.
anyway, the engine can only load ambient sounds (true ambient sounds that the engine keeps track of regardless of player position) properly when the map is loading up.
you can trick it by using an unused SVC (29). unfortunatly, you can't specify a filename for this, you need to actually feed in the # of the sound in the order as it was loaded into cache. �_�
the best way to do this is to precache your ambient sounds first, this way you don't have to worry about anything being precached before it and mucking up your ambient sounds.
the first sound precaches are located in weapons.qc (W_Precache). load any new ambient sounds in there at the top of the function.
float SVC_SPAWNSTATICSOUND = 29;
Define the constant for convenience, and write a little function to use it:
void(float soundnum, vector org, float ambvolume, float atten) spawnambient =
WriteByte(MSG_ALL, ambvolume * 255); //translate this into a value between 0 and 255...
WriteByte(MSG_ALL, atten * 64);
now just call your new function with whatever sound # you want.
this will spawn a true ambient sound into the map after it was loaded, and won't stop playing when you move out of range.
you'll notice the lack of any channel specification... i'm not sure if it's possible to turn it off again after it's been turned on. i can't check right now, but i'd guess using a non-looping sound will give you that 'non-looping sound for ambient sound' error, since it's not just a normal sound call.
but you will at least be able to turn it on. :x
maybe someone else can build on this?
Interesting, it would only work if the it could be turned off again somehow. Hmm....
my hack with the multiple random 0.1 sec sound effects works fairly well, but the problem is you can hear a sort of distortion/clip sound when the new sound overrides the old (the same sound you hear when a sound isn't looped correctly, meaning the waveforms don't line up and there's a sort of audio seam.) I may have to live with it. The sound effect for this has a lot of white noise in it (it's a steam jet) but I also wanted to do this for forcefields, and i'm worried that sound won't hide the clipping artifact as well.
As it happens I was writing some code yesterday that might help you out. What it does is provides you with a function that is run once when you load a saved game, but not when the game starts first time round. Replace the StartFrame code in world.qc with the following:
nosave float loadflag;
void() LoadGame =
//runs if player has just loaded a game
dprint("*****new game loaded******\n");
void() StartFrame =
teamplay = cvar("teamplay");
skill = cvar("skill");
framecount = framecount + 1;
if ( framecount == 3)
loadflag = 1;//started a new game, not loaded a savegame
else if(!loadflag && framecount > 3)
loadflag = 1;
You'll need a compiler which supports the nosave keyword for variables, frikqcc and fteqcc are the two I've tried. The idea is that the loadflag isn't saved in the savegame, and so gets reset to 0 when you load a game. The reason you have to wait until the third frame before you set it originally is because when quake loads a savegame, it runs the whole worldspawn procedure to rebuild the precache lists. The worldspawn procedure includes running two game frames, to give things a chance to droptofloor etc. So if you change a nosave variable during those frames, it'll have that value when you load savegames - which might be different to what value it had when you saved - but it's not what we want here.
So that gives you a way to restart the sound after someone loads a game, replace the dprint line in LoadGame with a function that searches for all the sounds that should play and restarts them.
As for the other problem, one possible solution would be to calculate the distance at which the sound can just be heard, assuming you started in range of the sound. Then have the entities search for a player within this radius regularly and start playing when they just enter it. Might be more difficult to make it work in coop.
If you do that, then you don't really want to use findradius for the job, it's a bit excessive to have 10 findradius calls every second per entity if you have a few of these things about. Better to loop through the first maxplayers worth of entities with the nextent command, since the players are always the first entities on the server. Then just test the distance for each of those that turns out to be a player. Whether
vlen(vec) < d
is faster than
vec_x*vec_x + vec_y*vec_y + vec_z*vec_z < d*d
is something I've been meaning to test. One is trying to calculate the square root of a quantity, but it is a builtin, so it's faster than qc code. Probably not necessary to get that kind of saving here if you're testing at most 16 entities, and most of the time just 1, it's just an interesting question.
yeah, this findradius idea is the direction i'm thinking of going next. One challenge is, the code to play a sound will not bother playing a sound if both right and left stereo channels can't hear it, and the calculation to determine the volume at each "ear" (channel) is somewhat complicated. Not sure if i'll need to recreate that in quakec or if it will suffice to estimate the radius in some other way.
The music in FMB_bdg is toggleable and plays everywhere. Being a looped sound, it would play forever if not switched off by the player (by operating the music button) or when the player unwittingly moves through an off-trigger.
It doesn't play from a savegame even if it was playing when the game was saved. But the savegame must be recording the state of the entity because you have to operate the music trigger twice after a savegame if you want the music to play - the first time turns the music off even though you can't hear it, and the second time turns it back on, and you then hear it.
I created the entity following an idea by Preach.
// if the player has not touched the 'toggle-music' button then this must be
// a triggered event, which means....
if (mechanism.classname != "func_button") etc...
Could this be developed to check the state of the entity when opening a saved game and play the sound if TRUE? Or am I completely off-track understanding what you want to do?
well, it sounds like preach's save game code would fix your problem, but I have a second problem that you don't have, which is that the sound won't play at all if you are too far away at the time it is first triggered. Your music is always audible, so there's no worry about being too far from the emitter at the time that it starts up.
I started messing with the quake model gen source to make it parse ascii files instead of .TRI, so that I could export to text from my modeler of choice (ie maya). I got all that working fine as far as I can tell, but the .mdl that gets written out crashes Fitzquake and every model editor I've found to try. Stepping through the exporter in debug doesn't really seem to show anything wrong, as the data is all seemingly correct before it gets written.
I took a break bashing my head against this a long time ago (like the end of 2006 now that I think about it :( ), so if I started poking at it now it might become obvious to me, but does anyone have any tips or suggestions? Are there any quake ports or other bits of software that I can try to load a model in that will actually tell me what part of the file is horked rather than just pooping?
A good starting place would be:
This gives you a big chunk of c code that will read a mdl file. You can skip the chunk about rendering it and just write a quick function that prints out the header data once it's loaded it, to check whether that's been written correct or not.
You might also want to look at
it's an export script for blender written in python. I wish I could remember what I did to make it work though, I had the same problems as you are and spent a few hours staring at a hex editor to find out what went wrong. If anything jogs my memory I'll let you know. Shame I never got round to writing the corresponding import script, it'd probably be good for diagnosing the problem...
I've looked over all that in the quake and modelgen sources, but I'm not very confident in writing a new piece of software to check why the last piece of software I wrote doesn't work, because clearly there's no guarantees the new one's not going to be fucked in some tiny significant way also, especially if they're both doing the same things to the same data structures. I'm just a simian level designer.
Part of the problem is that I had to do so much to get the thing to a testable state - .bmp import for the skin and then my ascii reader thing, because there's no program left on earth to write TRI files and there's no program left on earth that'll write whatever silly-ass image format modelgen looks for.
Well, upload an example model that the tool exports and I'll take a look at it, try and figure out what's wrong with it.
The steam jet sound problems seem to be solved.
First, I play the looping sound whenever the object is turned on, and the "fade out" sound whenever it is turned off. This was the original naive implementation.
Second, to deal with the problem of steam jets turning on when you're far away, I re-trigger the sound every 0.1 seconds when you are between 300 and 350 units away. The clipping sound artifact is there, but since it's so quiet at that distance, you barely hear it. When you get closer, it stops re-triggering it so you can hear a perfectly looped sound.
Third, I used Preach's loadgame callback idea to retrigger all sounds in case you are standing within 300 units when you save and reload the game.
Problem solved! Thanks for the help preach.
thanks Preach! It might take me a bit to gather all the code and stuff back up and actually produce one, but I'll hit that this weekend.
So I thought I would try Preach's code from above and simply put a call to my music_play_tune routine for any savegame but got this super message from aguirRe's engine.
CDAudio: drive not ready
INDIRECT 28(self)entity 0 481(distance).distance 21507(?]
STORE_V 28(self)entity 0 4(?]
STORE_V 323(CHAN_VOICE) 2.0 7(?]
STORE_V 21505(?] 10(?]
STORE_V 21506(?] 13(?]
STORE_V 21507(?] 16(?]
ADDRESS 28(self)entity 0 147(use).use 21508(?]
play_music.qc : music_play_tune : statement 51
world.qc : LoadGame : statement 0
world.qc : StartFrame : statement 16
PR_ExecuteProgram: assignment to world entity
Host_Error: Program error
The idea was to play whichever tune was currently set. Clearly, I am not understanding qc here.
function music_play_tune in file play_music.qc, there's an entity propery ("use" I think) that you're assigning some value at statement 51.
This entity is world (= 0) at the reported occasion and this is not allowed, you may not change the world's properties.
It's probably an un-initialized entity variable that causes this issue. If I saw the corresponding QC code, it'd be easier to explain.
Does this mean you're working on "This Onion II - The Sequel"? ;)
Does that mean I would have to intialise my music sounds in world.qc instead of calling the function play_music where they are usually precached? I can get around the "use" statement.
Oh, and according to my publicist, I have to give a "no comment" to your last question :-)
debug output above, it's a
self.use = something
statement that's run when self == world which is not allowed. It's not the precaching that's at fault here.
Since the function is called from StartFrame initially, I'd assume that the world is indeed self at that point.
Music_play_tune is normally the think function for the play_music entity, you can't call that directly from another entity's (here: world) think code. The sound calls are also invalid then.
I'd guess you'll have to trigger the play_music entity's think function somehow from StartFrame.
I'll play a little more with this to see if I can figure out a workaround.
No, I Can't Figure It Out And I'm Going Round In Circles
I thought I could set up a flag to say whether or not the music was playing at the time of the savegame (state = TRUE/FALSE). I know the piece of music playing by tracking it with a variable (cnt = 1/3/5/7, for 4 set pieces of music). I know both of these are saved in the savegame file (actually FALSE appears not to be saved), so presumably are loaded with loadgame.
But world.qc baulks at everything I try, usually with a 'types function and field not allowed'. So I cannot figure out how to either read the entitie's 'state' and 'cnt' and make use of them; or how to call the 'music_play_tune' function from world.qc.
Do you think I am trying to do the impossible or is it just that the logic is beyond me?
This Might Not
be a very good idea, but just to make the music_play_tune "callable" from StartFrame, you could assign a global entity var music_entity the value of self in play_music.
In StartFrame, you'd then just set music_entity.nextthink = time to make it execute as soon as possible. You should probably add a check for music_entity being not-world, i.e.:
music_entity.nextthink = time;
Maybe someone else can suggest a better way to actually achieve whatever you're trying to do.
savegame files don't contain vars that are 0 (= FALSE), so that might be why it's "not saved".
I Don't Think It's Impossible
are you waiting long enough for all the entities to be spawned before you go searching for the music entity?
it should just be a matter of using a while loop to .chain through your entities until you find your music ent, checking the entity fields and then giving it a .nextthink and .think to call it's function.
music_play_tune likely has stuff with self in it, so that's why you'd need to do musicent.think = music_play_tune; instead of directly calling music_play_tune();.
or am i misunderstanding the problem?
savegame files don't contain vars that are 0 (= FALSE), so that might be why it's "not saved".
false = 0, and i'm pretty sure variables in quake are automatically initialized to 0 if they have no value, so technically, they are 'saved' whether it's actually in the save file or not.
Website copyright © 2002-2023 John Fitzgibbons. All posts are copyright their respective authors.