<li><aclass="backlink"title="Click to do a full-text search for this title"href="./MakeAMod-DMSP?action=fullsearch&value=linkto%3A%22MakeAMod-DMSP%22&context=180">MakeAMod-DMSP</a></li>
<spanclass="anchor"id="line-2"></span><spanclass="anchor"id="line-3"></span><pclass="line867"><em>Special thanks to Professor Wouter "Aardappel" van Oortmerssen for <aclass="http"href="http://wouter.fov120.com/maps/dmsp/index.html">original DMSP mod/concept</a> and awesome sounding name.</em><spanclass="anchor"id="line-4"></span><spanclass="anchor"id="line-5"></span><pclass="line867"><br/>
<br/>
<spanclass="anchor"id="line-6"></span><spanclass="anchor"id="line-7"></span><pclass="line874">Before we begin, if you haven't downloaded it already you'll need this file: <spanclass="anchor"id="line-8"></span><br/>
<spanclass="anchor"id="line-12"></span><spanclass="anchor"id="line-13"></span><pclass="line874">DMSP for Q4 was originally an internal experiment to see if a new gametype was possible entirely within script. The basic premise is to provide the player an arcade-style limitless supply of monsters and pickups. A deathmatch map is the perfect place for it, because it has a closed connected layout the player can run around in as long as he can stay alive, henceforth the term "deathmatch single player." <spanclass="anchor"id="line-14"></span><br/>
<br/>
<spanclass="anchor"id="line-15"></span><spanclass="anchor"id="line-16"></span><pclass="line874">Q4DM4 was determined to be the best map for the job, because almost the entire layout is navigable without the use of jumppads or teleporters (which the AI doesn't pay attention to). Maps like Q4DM5 had too many isolated platforms that would fill up with monsters that couldn't go anywhere. <spanclass="anchor"id="line-17"></span><br/>
<br/>
<spanclass="anchor"id="line-18"></span>The experiment was a success aside from a few small issues. The main one is that the player spawns in a multiplayer map with the default unstroggified marine stats and physics if the gametype isn't specifically set to multiplayer (and the game isn't a lot of fun if you're stuck moving slowly), but in multiplayer AI doesn't evaluate. Thus, besides including .aas files for the deathmatch map, the "player" keyvalue had to be set on the worldspawn in the .map itself to "player_marine_mp". <spanclass="anchor"id="line-19"></span><br/>
<br/>
<spanclass="anchor"id="line-20"></span><spanclass="anchor"id="line-21"></span><pclass="line874">q4dm4.script is loaded by default when the map is loaded, because they share a name and a directory. q4dm4.script points to dmsp_base.script, which starts the fun. <spanclass="anchor"id="line-22"></span><br/>
<br/>
<spanclass="anchor"id="line-23"></span>The first thing we do is set up variables for the values we're going to have to keep track of persistently: <spanclass="anchor"id="line-24"></span><spanclass="anchor"id="line-25"></span><pre>float dmsp_skill = 0;
<spanclass="anchor"id="line-29"></span></pre><spanclass="anchor"id="line-30"></span><pclass="line874">We stash the value for the g_skill cvar in dmsp_skill, so we can use it in various functions later without having to run sys.getcvar() every time we want it. dmsp_score is pretty self explanatory, and the two combo variables are used to keep track of how many kills the player has chained together in a short span of time. <spanclass="anchor"id="line-31"></span><br/>
<br/>
<spanclass="anchor"id="line-32"></span>There's a lot of stuff following this in the script, but the thing that kicks it off is at the end in <strong>main()</strong>, which runs by default when a map loads. Since the game runs about a dozen frames of any map before actually spawning the player and starting the show, anything we sneak into the beginning of main() will happen at the loading screen. Since the map has no monsters in it, the game would pause to cache their models and sounds every time a new one was spawned if we didn't do this. <spanclass="anchor"id="line-33"></span><spanclass="anchor"id="line-34"></span><pclass="line867"><spanclass="anchor"id="line-35"></span><pre>void main()
<spanclass="anchor"id="line-58"></span> sys.println("The invasion has begun!");
<spanclass="anchor"id="line-59"></span>}
<spanclass="anchor"id="line-60"></span></pre><spanclass="anchor"id="line-61"></span><spanclass="anchor"id="line-62"></span><pclass="line862">After that there's a dorky countdown to give the player a couple seconds to run for some guns, and then <strong>dmsp_createSpawner()</strong> is called. This function has to do things in a somewhat peculiar order because everything relies on spawnArguments. The gameplay is driven by a func_spawner, which has to be spawned with its keyvalues already set. If we spawn it and then set keys on it they won't have any effect. <spanclass="anchor"id="line-63"></span><spanclass="anchor"id="line-64"></span><pre>void dmsp_createSpawner()
<spanclass="anchor"id="line-65"></span>{
<spanclass="anchor"id="line-66"></span> float j = dmsp_createSpawns(); // make nulls for spawn points
<spanclass="anchor"id="line-67"></span> ...
<spanclass="anchor"id="line-68"></span></pre><spanclass="anchor"id="line-69"></span><pclass="line862">The function starts by going out to <strong>dmsp_createSpawns()</strong>, which in turn searches the entity list for every entity whose name starts with "info_player_deathmatch_", and spawns a target_null (func_spawners can only be targetted at target_nulls for whatever reason). We do this early so we can set spawnArgs on the nulls we create without interfering with the spawnArgs we set on the func_spawner. This function returns the number of spawns it found/created, which gets put in <strong>j</strong>. <spanclass="anchor"id="line-70"></span><br/>
<br/>
<spanclass="anchor"id="line-71"></span>At this point we begin setting spawnArgs for the spawner itself. def_spawns are listed to set up our monster loadout - we skip the tactical transfers because they don't move fast enough for the ravenous horde combat we're looking to achieve. After those are done, we add extra keys for functionality: <spanclass="anchor"id="line-72"></span><spanclass="anchor"id="line-73"></span><pclass="line867"><spanclass="anchor"id="line-74"></span><pre> ...
<spanclass="anchor"id="line-75"></span> sys.setSpawnArg("max_active", j); // assume # of spawns in map is a good monster limit
<spanclass="anchor"id="line-83"></span> sys.setSpawnArg("spawn_script_death", "dmsp_killMonster"); // for spawning loots
<spanclass="anchor"id="line-84"></span> sys.setSpawnArg("spawn_script_init", "dmsp_whoosh"); // for spawning zoots
<spanclass="anchor"id="line-85"></span> ...
<spanclass="anchor"id="line-86"></span></pre><spanclass="anchor"id="line-87"></span><spanclass="anchor"id="line-88"></span><pclass="line874">At the end of this function, the func_spawner is finally spawned and triggered, which commences the invasion. <spanclass="anchor"id="line-89"></span><br/>
<br/>
<spanclass="anchor"id="line-90"></span>Assuming that the # of spawns is a good monster limit isn't entirely safe - while designers seem to commonly stick to an unspoken standard of deathmatch spawn count vs map size, there is the occasional aberration. (q4dm6, for example, has 64 spawns!) spawn_neverdormant ensures that if the player leaves the PVS of a group of monsters they won't suddenly stop and stand there blinking - this way the player is always beset on all sides by the horde. <spanclass="anchor"id="line-91"></span><br/>
<br/>
<spanclass="anchor"id="line-92"></span>The rest of the gameplay functionality comes from the last two keys. spawn_script_init will make the monster call <strong>dmsp_whoosh()</strong> when it spawns, which simply adds some effects to try and mask their sudden appearance. spawn_script_death is the important one, which makes the monster call <strong>dmsp_killMonster()</strong> when it dies. This is where all of the item drops and scoring is handled. <spanclass="anchor"id="line-93"></span><spanclass="anchor"id="line-94"></span><pclass="line867"><spanclass="anchor"id="line-95"></span><pre>void dmsp_killMonster( entity victim )
<spanclass="anchor"id="line-96"></span>{
<spanclass="anchor"id="line-97"></span> string vicType = victim.getKey( "classname" ); // what kind of monster is it?
<spanclass="anchor"id="line-107"></span> vicPip_z = vicPip_z + vicSize_z * 0.75; // print score pip about head high
<spanclass="anchor"id="line-108"></span> ...
<spanclass="anchor"id="line-109"></span></pre><spanclass="anchor"id="line-110"></span><spanclass="anchor"id="line-111"></span><pclass="line862">What makes it all work is that when an entity calls any of its script_ functions, <strong>it passes itself in as the first parameter.</strong> This means we can have one function that has different output based on what kind of monster calls it. This works great for us, because now we can have puny monsters add appropriately puny amounts to the player's score and drop simple items like armor shards, but have much better rewards for the beefy opponents. We set up a couple of variables early: <spanclass="anchor"id="line-112"></span><ul><li><pclass="line891"><strong>vicType</strong> is the monster's classname. We use this shortly to differ the drops and score based on monster size. <spanclass="anchor"id="line-113"></span></li><li><pclass="line891"><strong>score</strong> starts out as our base score multiplier. 25 is the base, and another 25 is added for every level of skill above Easy we're playing at, and for our current combo level. More on that later. <spanclass="anchor"id="line-114"></span></li><li><pclass="line891"><strong>vicOrg</strong> is the origin of the monster at the time of death (when the function is called). <spanclass="anchor"id="line-115"></span></li><li><pclass="line891"><strong>vicSize</strong> returns a vector for the extends of the monster's bounding box at the time of death. We use this to set ... <spanclass="anchor"id="line-116"></span></li><li><pclass="line891"><strong>vicDrop</strong> to a point halfway up the height of the monster, so items come out of the body, and ... <spanclass="anchor"id="line-117"></span></li><li><pclass="line891"><strong>vicPip</strong>, so we can print the floating score number above the monster's head. <spanclass="anchor"id="line-118"></span></li></ul><pclass="line867"><br/>
<br/>
<spanclass="anchor"id="line-119"></span>After that comes a block like this for every monster type that was specified in the func_spawners args: <spanclass="anchor"id="line-120"></span><spanclass="anchor"id="line-121"></span><pre> ... if ( vicType == "monster_iron_maiden" ) {
<spanclass="anchor"id="line-122"></span> if ( val > 0.6 ) dmsp_throwLoots( vicDrop, "ammo_rocketlauncher_mp", 1, 0 );
<spanclass="anchor"id="line-123"></span> else if ( val > 0.3 ) dmsp_throwLoots( vicDrop, "ammo_hyperblaster_mp", 1, 0 );
<spanclass="anchor"id="line-130"></span></pre><spanclass="anchor"id="line-131"></span><spanclass="anchor"id="line-132"></span><pclass="line862">We put a random number in <strong>val</strong>, and each monster gets its own custom bracket of what items it drops, and how frequently. This is done by calling <strong>dmsp_throwLoots()</strong>. Score is multiplied again by some other value based arbitrarily on the monster's strength, and scoring is then handled by calling <strong>dmsp_scorePip()</strong>. We'll look at both of them to see how they work. <spanclass="anchor"id="line-133"></span><br/>
<br/>
<spanclass="anchor"id="line-134"></span>dmsp_throwLoots is relatively simple - it takes parameters for where the drop occurs (taken from the monster's place of death), what to drop, how many of them to drop, and for fun an extra flag to allow an occasional bonus "double drop." This, once again, is done through spawnArgs. The <strong>setLinearVelocity()</strong> function is used to give all the items a random parabolic arc, so the items "pop" out of the monster instead of just appearing on the floor. <spanclass="anchor"id="line-135"></span><spanclass="anchor"id="line-136"></span><pre>void dmsp_throwLoots ( vector org, string lootClass, float cnt, float nodoubles )
<spanclass="anchor"id="line-159"></span></pre><spanclass="anchor"id="line-160"></span><spanclass="anchor"id="line-161"></span><pclass="line874">dmsp_scorePip() is what puts the floaty text on screen above the monster's head when it dies. It sets up timing for the text shrinking and fading out, and the combo functionality was lumped in here as well. <spanclass="anchor"id="line-162"></span><spanclass="anchor"id="line-163"></span><pre>void dmsp_scorePip ( string text, vector origin, float pipTime ) {
<spanclass="anchor"id="line-177"></span></pre><spanclass="anchor"id="line-178"></span><spanclass="anchor"id="line-179"></span><pclass="line874">We'll get to how the text works in a moment, but the function can only display static text in space. The way this function works is to only display text for one frame, and slowly change the size and color of the text in a loop. startTime is set to the time when the function is called (when the text first appears), and we then change our text's properties depending on how many frames we've waited since that time. <spanclass="anchor"id="line-180"></span><br/>
<br/>
<spanclass="anchor"id="line-181"></span>The way the combo system works is that every time this is called, dmsp_nextComboTime is set to about 2 seconds into the future (a little more if it's a tougher monster). Before doing that, if the current time is still earlier than the last time it was set to, then we're still within the combo delay, and we increase the modifier. That gets multiplied to the score we were passed from dmsp_killMonster(), as well as the lifetime of the floating text. <spanclass="anchor"id="line-182"></span><br/>
<br/>
<spanclass="anchor"id="line-183"></span>We start the loop, continually setting <strong>d</strong> to a value from 0 to 1, where 0 is the time we started, and 1 is the time the text is completely finished. <spanclass="anchor"id="line-184"></span><spanclass="anchor"id="line-185"></span><pre> ...
<spanclass="anchor"id="line-186"></span> for ( i = startTime; i < (startTime + pipTime); i = i + frameTime ) {
<spanclass="anchor"id="line-187"></span> d = (i - startTime) / pipTime;
<spanclass="anchor"id="line-188"></span> ...
<spanclass="anchor"id="line-189"></span></pre><spanclass="anchor"id="line-190"></span><pclass="line862">After that we pick a color based on what combo level we're at, using <strong>d</strong> in all of them to make them fade to black over the text's lifetime. <spanclass="anchor"id="line-191"></span><spanclass="anchor"id="line-192"></span><pre> ...
<spanclass="anchor"id="line-193"></span> if (dmsp_combo == 1) {
<spanclass="anchor"id="line-194"></span> color_x = 1 - d; // red
<spanclass="anchor"id="line-195"></span> } else if (dmsp_combo == 2) {
<spanclass="anchor"id="line-201"></span></pre><spanclass="anchor"id="line-202"></span><pclass="line862">The meat of the function comes at the end. The origin in space for the text is also changed using <strong>d</strong>, so that it floats upward over time. <spanclass="anchor"id="line-203"></span><spanclass="anchor"id="line-204"></span><pre> ...
<spanclass="anchor"id="line-205"></span> org_z = origin_z + d * 96;
<spanclass="anchor"id="line-213"></span></pre><spanclass="anchor"id="line-214"></span><pclass="line867"><strong>sys.drawText()</strong> is the function that puts the text on screen. It's usually just for placeholder stuff in development, to do something like put "Big cutscene here where the player gets his next mission" on the screen before the cutscene's been made, but it'll work well enough for our purposes. We print twice, once for the score and then again slightly higher with the <strong>comboType</strong> string we've picked. <spanclass="anchor"id="line-215"></span><br/>
<br/>
<spanclass="anchor"id="line-216"></span>After all that, we add the final value to the player's score and show him in the console. <spanclass="anchor"id="line-217"></span><spanclass="anchor"id="line-218"></span><pre> ...
<spanclass="anchor"id="line-219"></span> dmsp_score = dmsp_score + score; // The only way to show the player his score is to update it in the
<spanclass="anchor"id="line-220"></span> sys.println( dmsp_score ); // console since there's no way to display it on the player hud
<spanclass="anchor"id="line-221"></span>}
<spanclass="anchor"id="line-222"></span></pre><spanclass="anchor"id="line-223"></span><spanclass="anchor"id="line-224"></span><pclass="line862">At this point our neverending train of functions is complete. If you'd like to try it out for yourself, just put <strong>dmsp_v1.pk4</strong> in your q4base folder, set 'g_skill' to taste, and type 'map dmsp/q4dm4' at the console. <spanclass="anchor"id="line-225"></span><spanclass="anchor"id="line-226"></span><pclass="line867"><hr/><pclass="line874"><spanclass="anchor"id="line-227"></span><spanclass="anchor"id="line-228"></span><pclass="line874">It's not perfect, of course. It's lacking the polish necessary to make it feel finished and professional, but the limitations of what we can and can't do in script eventually make themselves known. It is, however, pretty remarkable that we're able to do as much as we can. <spanclass="anchor"id="line-229"></span><spanclass="anchor"id="line-230"></span><pclass="line874">The following are the known issues (all of them easily fixable if this were implemented in code, including the player spawnclass fix that required a custom version of the map): <spanclass="anchor"id="line-231"></span><spanclass="anchor"id="line-232"></span><ul><li><pclass="line862">Monster radius isn't taken into consideration when placing or choosing spawn points, so larger monsters can sometimes get stuck to walls if the spawnpoint is too close to a surface. This <em>could</em> be fixed in script, albeit in a rather ugly fashion. The cheap solution would be to just move all the nulls 32 units in the direction that the matching DM spawn point faces - it's a safe assumption that no DM spawns face walls. <spanclass="anchor"id="line-233"></span></li><li>The pre-placed items show up with their single player models, which in cases like the 5+ health means ugly temp artwork nobody was meant to see. A brute force solution in script would be to manually find and replace all of these entities with their MP equivalents. <spanclass="anchor"id="line-234"></span></li><li>There's no HUD GUI interaction at all, so things like score and combos are either dumped into the console or popped on screen with the debug text, whcih can't be read very well at long distances. A proper implementation would have to add new HUD events for updates and some place in a modified single player HUD gui for the new numbers. <spanclass="anchor"id="line-235"></span></li><li>This script cares nothing about system performance, and only stops spawning monsters at the coded limit. In q4dm4 this is 24, which is isn't that friendly to slower machines. <spanclass="anchor"id="line-236"></span><spanclass="anchor"id="line-237"></span></li></ul><pclass="line867"><br/>
<li><ahref="http://moinmoin.wikiwikiweb.de/">MoinMoin Powered</a></li><li><ahref="http://www.python.org/">Python Powered</a></li><li><ahref="http://validator.w3.org/check?uri=referer">Valid HTML 4.01</a></li>