mGm_Lizard

UT2004 Replication used

Aug 12th, 2015
266
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1.  
  2. 
  3. MAY AUG SEP
  4. 10  
  5. 2005    2006    2007
  6. 14 captures
  7. 30 Aug 02 - 10 Aug 06
  8. CloseHelp
  9. A Guided Ripper with a Targeting Laser
  10. So many coders have been inspired by Half Life's rocket launcher. In a nutshell, this weapon has two modes, which are switched between via secondary fire. Secondary fire turns on and off a targeting laser. That laser is seen by other people, so using it to target players provides them a give-away that they are being aimed at. You might think that it's better to just turn off the laser, since you have the targeting reticule. But the laser has another purpose. Any missiles fired while it is turned on gravitate towards that laser. So you can aim missiles mid-flight by aiming the weapon's laser.
  11. Since so many have wanted to make such a weapon in UT, I thought I'd use that as a base for my tutorial. I've seen many people make it, and after days of fiddling with variables to get it to work, they did. Unfortunately, the version they ended up was completely unrecognizable and unmaintainable. So I spent today working on a nice one, one that would be understandable, and I would use as a base for this tutorial. The one I've written here seems to fulfill those goals, and I hope you'll agree.
  12. There are two main parts to this weapon. The first is to create a targeting laser that is visible to both yourself and other clients. The second is to create the homing missiles. When I first started, I realized that using the rocket launcher would not be a good approach. There was so much code already in there in regards to the locking on, and the multiple rockets. Any example I made from it would involve lots of copying and pasting, and would not be readily understandable. Instead, I decided to modify the Ripper, which I found much easier to adapt and use for this tutorial. So, you will soon have a targeting ripper with guided razorblades. :)
  13. I'm placing my files in a project called ReplicationTest, which is where I'll be putting all my Replication Case Studies. It'll make it easy for you to download and play with yourself.
  14. First, set up your class. It should look like the following:
  15. class LaserRipper extends Ripper;
  16. Not much, but a start. :) Now, let's make a toggle-able flag that determines whether the laserdot it available.
  17. var bool bHasLaserDot;
  18. Next, we need to modify the Alt-Fire to toggle this flag. Let's pretend for a moment that we have a function called SetupLaserDot() that looks at the bHasLaserDot flag, and creates or removes the dot as necessary. This only needs to be done serverside, so you'll notice how it is not simulated. We'll add in a beeping sound via PlaySound. Since PlaySound is being called from a non-simulated function, UT will correctly replicate the sound to all relevant clients.
  19. function AltFire( float Value )
  20. {
  21.     //switch the laserdot flag
  22.     bHasLaserDot = !bHasLaserDot;
  23.  
  24.     //set up laserdot to on or off
  25.     SetupLaserDot();
  26.  
  27.     //play the nice click sound
  28.     PlaySound(sound'UnrealShare.Click', SLOT_None, 4.2);
  29.  
  30.     //turn off altfire flag
  31.     Pawn(Owner).bAltFire = 0;
  32. }
  33. There's a few other deals that should be covered here. We don't want to play the alt-fire sound anymore, and nor do we want to display the alt-fire shooting animation anymore. So we'll just dummy-out the function.
  34. simulated function PlayAltFiring()
  35. {
  36. }
  37. Now, lets create the actual laser dot. Create a new class, called LaserDot. :) Set bUnlit to true on this actor, so that it looks like a true laser dot, and is not affected by lighting, (so it appears as bright as ever, even in those dark corners.) Oh, and we want to give it an appropriate texture. It should look like this:
  38. class LaserDot extends Actor;
  39.  
  40. defaultproperties {
  41.     Texture=texture'dot'
  42.     DrawType=DT_Translucent
  43.     bUnlit=True
  44. }
  45. That right there is the final version of LaserDot. There is no logic in there. It's all done in the weapon, as we'll see later. One thing that works right now is that this LaserDot is a ROLE_DumbProxy by default. This means that the location updates will be fed in over the network. Simulation cannot really be performed, since you can't simulate where the dot will be x seconds from now based upon it's velocity. It is entirely up to the whim of the player where the dot goes next, so the DumbProxy replication is the best we can get for other actors. However, for the person holding the weapon, it would be nice to have the laser act accurately for them, since they have an accurate ViewRotation from which to base the laser. We'll do this later on in this tutorial. You'll probably have trouble compiling with that, since you don't have a dot texture. You can always remove the line to see a little horse head laser, or you can download the full ReplicationTest package which includes everything.
  46. Anyway, let's go back to the LaserRipper code and create the variable that will hold a reference to our LaserDot. Just add the following to the code at the top of the script.
  47. var LaserDot LaserDot;
  48. Now, we can write the SetupLaserDot() function. We don't want to have a laser created on the server AND on the client, so this function will execute on the server alone. This is ensured by not using simulated. This code will check if bHasLaserDot is set. If it is on, and the weapon does not yet have a laser dot, then we will spawn a new one at our current location, and set this weapons's laser dot to the newly spawned one. If, on the other hand, we already had a laser dot, (it shouldn't happen, but it can never hurt to be too careful,) then it will simply do nothing at all. In either case, we will tell the HUD not to drawn it's crosshair anymore, since we now have our own crosshair, the LaserDot. And finally, if the user has turned off their laser dot, it lets the HUD draw the crosshair, and it calls a to-be-created function called DestroyLaserDot().
  49. //set up laserdot appropriately, to on or off
  50. function SetupLaserDot () {
  51.     if ( bHasLaserDot )
  52.     {
  53.         //to remove the default crosshair from our screen, since we now have a laserdot
  54.         bOwnsCrossHair=True;
  55.  
  56.         //if we want a laserdot, and we don't already have one, then create one
  57.         if (LaserDot == None)
  58.         {
  59.             LaserDot = Spawn(class'LaserDot', Self, , Location, Rotation);
  60.         }
  61.     }
  62.     else
  63.     {
  64.         //place the default crosshair back on screen
  65.         bOwnsCrossHair=False;
  66.  
  67.         //destroy the laserdot
  68.         DestroyLaserDot();
  69.     }
  70. }
  71. DestroyLaserDot() does a few things for us, and since we will be calling it a bunch of times throughout our weapon code, it's best to isolate it to it's own function. Let's cover this function next, since we'll be needing it more often later on. This should merely destroy the laser dot, and then set the laser dot reference to none, if we currently have a laser dot.
  72. //function for destroying the laserdot
  73. function DestroyLaserDot()
  74. {
  75.     if (LaserDot != None)
  76.     {
  77.         LaserDot.Destroy();
  78.         LaserDot = None;
  79.     }
  80. }
  81. Hopefully, this isn't too complicated yet. This should be relatively simple stuff, as we haven't gotten into the replication details yet. Now, let's do some house keeping. It currently destroys the laser dot when you switch the laser off with the alt fire. However, you need to take care to make it switch off when you switch to another weapon, and to make it turn back on when you switch to this LaserRipper. We'll do that with the following code:
  82. state Active
  83. {
  84.     function BeginState()
  85.     {
  86.         //when they bring up this weapon, initialize the laserdot to on or off
  87.         SetupLaserDot();
  88.         Super.BeginState();
  89.     }
  90. }
  91.  
  92. state DownWeapon
  93. {
  94. ignores Fire, AltFire, Animend;
  95.  
  96.     function BeginState()
  97.     {
  98.         //when they switch to another weapon, get rid of the laserdot
  99.         DestroyLaserDot();
  100.         Super.BeginState();
  101.     }
  102. }
  103. Again, all of the laser dot management is done server-side. We don't want to have to deal with it clientside, since the server is perfectly able to handle this, and no real client-side behavior is needed. Now we can get into the fun details. We'll work on making the laser act correctly. Involved in this, is performing a trace out from the player in the direction they are looking, and finding where it hits the wall. We then move the laser dot to that location, so it looks as if the dot is hitting there.
  104. function UpdateLaserDot ()
  105. {
  106.     local vector X, Y, Z, StartTrace, EndTrace, HitLocation, HitNormal;
  107.     local actor HitActor;
  108.  
  109.     //Role check makes sure that we only do this on the server, since we only have access to it there
  110.     //laserdot check makes sure that we actually have a laser dot to update
  111.  
  112.     if (Role == ROLE_Authority && LaserDot != None)
  113.     {
  114.         //get the X component (the forward component) of the ViewRotation as a normal
  115.         GetAxes(Instigator.ViewRotation,X,Y,Z);
  116.  
  117.         //get the start, just outside ourselves in the direction we are facing
  118.         StartTrace = Instigator.Location + Instigator.Eyeheight * Z + X*Instigator.CollisionRadius;
  119.  
  120.         //get a ridiculous distance
  121.         EndTrace = StartTrace + 10000 * X;
  122.  
  123.         //trace and get the location that the laser hits
  124.         HitActor = Instigator.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
  125.  
  126.         //move our self to that location
  127.         LaserDot.SetLocation(HitLocation);
  128.     }
  129. }
  130. The laser dot is almost complete. We need something that calls UpdateLaserDot() every tick to make sure it follows the gun appropriately. We do that with the following code:
  131. function Tick(float deltatime)
  132. {
  133.     Super.Tick(deltatime);
  134.  
  135.     //remove the laserdot if our owner died
  136.     if ( Owner == None )
  137.     {
  138.         DestroyLaserDot();
  139.     }
  140.  
  141.     //update the laser's location
  142.     UpdateLaserDot();
  143.  
  144. }
  145. Image a player dies. Their weapon still exists, but it is not attached to them. In this case, we want to be sure to remove the laser dot, so the dead player cannot target. When a player dies, they lose their inventory, and the inventory's owner is set to None. The conditional that checks if Owner == None handles that and ensures that the player cannot have a laser once they die.
  146. If you were to try this out, it would work fine in single player. In addition, it would work fine in multiplayer. Because of the nature of the LaserDot, and how it will replicate it's location to every client in the game it's relevant to, every client will see the laser fine. However, when I am controlling my own gun, it still looks laggy for me. Here, we can improve things... Since the client knows the ViewRotation of their own self, they should be able to update the laser dot locally and make it appear great. We'll do that now.
  147. First, we need to make Tick simulated, since we want the tick to run on the client, so that the UpdateLaserDot code can be run on the client. The final version of Tick looks the same as above, but with a simulated just before function Tick.
  148. Now, we need to modify UpdateLaserDot() as well. First, we'll make it simulated so that it will run on all the clients, including the owner client, which we want. We do not want to use replicated functions to get that one client to do the update laser, since these functions would be sent every tick, and would be very expensive, network-wise.
  149. However, that simple change to UpdateLaserDot() does not solve our problems. Every client will be running that code, and every client does not know the ViewRotation of the owner. So, what we'll have to do is limit who runs this function.
  150. One way to do this is to check bNetOwner. On the client, this is set to true if this actor is owned by that player. On the server, it cannot be used, since bNetOwner changes frequently since it replicates variables to many different clients, and so changes the server's bNetOwner many times. Our check in the UpdateLaserDot() function looks like this:
  151. if (Role == ROLE_Authority || bNetOwner)
  152. {
  153. This says: if we are the server, or if we are not the server, but we are a client that is owns this actor (which is true of your own weapon.)
  154. Let's integrate this into the UpdateLaserDot() code so that it will work on both the server and the owning-client.
  155. The new function should look like this:
  156.  
  157. simulated function UpdateLaserDot ()
  158. {
  159.     local vector X, Y, Z, StartTrace, EndTrace, HitLocation, HitNormal;
  160.     local actor HitActor;
  161.  
  162.     /*
  163.     instigator check ensures that we have a valid ViewRotation, (eg: the server, and the playerpawn owning us). this is done:
  164.         on the server, where instigator is available
  165.         on the bNetOwner of this weapon
  166.         if we are not one of those, we are another client on the network, and the DumbProxy behavior of the LaserDot will replicate the location to them
  167.     laserdot check makes sure that we actually have a laser dot to update
  168.     */
  169.  
  170.     if ((Role == ROLE_Authority || bNetOwner) && LaserDot != None)
  171.     {
  172.         //get the X component (the forward component) of the ViewRotation as a normal
  173.         GetAxes(Instigator.ViewRotation,X,Y,Z);
  174.  
  175.         //get the start, just outside ourselves in the direction we are facing
  176.         StartTrace = Instigator.Location + Instigator.Eyeheight * Z + X*Instigator.CollisionRadius;
  177.  
  178.         //get a ridiculous distance
  179.         EndTrace = StartTrace + 10000 * X;
  180.  
  181.         //trace and get the location that the laser hits
  182.         HitActor = Instigator.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
  183.  
  184.         //move our self to that location
  185.         LaserDot.SetLocation(HitLocation);
  186.     }
  187. }
  188. This is almost perfect. One problem is that although the ViewRotation will be available on the client, the Instigator will not. Looking at the Instigator replication check in actor, we see:
  189. unreliable if( bReplicateInstigator && (RemoteRole>=ROLE_SimulatedProxy) && (Role==ROLE_Authority) )
  190.     Instigator;
  191. Our weapon is a ROLE_SimulatedProxy, so that satisfies that part of the criteria. But we'll need to make sure that bReplicateInstigator is set. By default, that is only done for projectiles. So add the following to our LaserRipper class:
  192. defaultproperties {
  193.     bReplicateInstigator=true
  194.     bRecommendAltSplashDamage=False
  195. }
  196. I threw that last part in so the bots didn't decide to play around with the AltFire and try to use it for splash damage. This isn't a bot tutorial, so I can't guarantee any level of work-ability with the UT bots. (Actually, I don't understand bots yet, so I'm not that great at getting things to work with them yet. :)
  197. There is one final thing that needs to be done. The LaserDot is not available client-side either, so the code will be unable to update it's local copy of the laser dot. To fix these, we simply need to replicate the LaserDot variable. We only need to replicate it to the bNetOwner of the weapon though, since no one else should be concerned with it, since only the bNetOwner is manipulating the dot. Add the following code above the top function, and below the last variable:
  198. //because this weapon plays with the location of it's own LaserDot locally on it's own machine
  199. //it needs the LaserDot replicated to the client
  200.  
  201. replication
  202. {
  203.     reliable if (bNetOwner && Role==ROLE_Authority)
  204.         LaserDot;
  205. }
  206. Now that we can be assured that the Instigator will be available, and so our code will work on both the server and on the owning-client.
  207. Congratulations, you've made a laser dot that works in netplay. For reference's sake, the full class looks like this:
  208. class LaserRipper extends Ripper;
  209.  
  210. var bool bHasLaserDot;
  211. var LaserDot LaserDot;
  212.  
  213. //because this weapon plays with the location of it's own LaserDot locally on it's own machine
  214. //it needs the LaserDot replicated to the client
  215.  
  216. replication
  217. {
  218.     reliable if (bNetOwner && Role==ROLE_Authority)
  219.         LaserDot;
  220. }
  221.  
  222. //function for destroying the laserdot
  223. function DestroyLaserDot()
  224. {
  225.     if (LaserDot != None)
  226.     {
  227.         LaserDot.Destroy();
  228.         LaserDot = None;
  229.     }
  230. }
  231.  
  232. //set up laserdot appropriately, to on or off
  233. function SetupLaserDot () {
  234.     if ( bHasLaserDot )
  235.     {
  236.         //if we want a laserdot, and we don't already have one, then create one
  237.         if (LaserDot == None)
  238.         {
  239.             LaserDot = Spawn(class'LaserDot', Self, , Location, Rotation);
  240.         }
  241.     }
  242.     else
  243.     {
  244.         //destroy the laserdot
  245.         DestroyLaserDot();
  246.     }
  247. }
  248.  
  249.  
  250. function Destroyed()
  251. {
  252.     Super.Destroyed();
  253.     //destroy the laserdot when the weapon gets destroyed
  254.     DestroyLaserDot();
  255. }
  256.  
  257. simulated function UpdateLaserDot ()
  258. {
  259.     local vector X, Y, Z, StartTrace, EndTrace, HitLocation, HitNormal;
  260.     local actor HitActor;
  261.  
  262.     /*
  263.     instigator check ensures that we have a valid ViewRotation, (eg: the server, and the playerpawn owning us). this is done:
  264.         on the server, where instigator is available
  265.         on the bNetOwner of this weapon
  266.         if we are not one of those, we are another client on the network, and the DumbProxy behavior of the LaserDot will replicate the location to them
  267.     laserdot check makes sure that we actually have a laser dot to update
  268.     */
  269.  
  270.     if ((Role == ROLE_Authority || bNetOwner) && LaserDot != None)
  271.     {
  272.         //get the X component (the forward component) of the ViewRotation as a normal
  273.         GetAxes(Instigator.ViewRotation,X,Y,Z);
  274.  
  275.         //get the start, just outside ourselves in the direction we are facing
  276.         StartTrace = Instigator.Location + Instigator.Eyeheight * Z + X*Instigator.CollisionRadius;
  277.  
  278.         //get a ridiculous distance
  279.         EndTrace = StartTrace + 10000 * X;
  280.  
  281.         //trace and get the location that the laser hits
  282.         HitActor = Instigator.TraceShot(HitLocation, HitNormal, EndTrace, StartTrace);
  283.  
  284.         //move our self to that location
  285.         LaserDot.SetLocation(HitLocation);
  286.     }
  287. }
  288.  
  289. simulated function Tick(float deltatime)
  290. {
  291.     Super.Tick(deltatime);
  292.  
  293.     //remove the laserdot if our owner died
  294.     if ( Owner == None )
  295.     {
  296.         DestroyLaserDot();
  297.     }
  298.  
  299.     //update the laser's location
  300.     UpdateLaserDot();
  301.  
  302. }
  303.  
  304. function AltFire( float Value )
  305. {
  306.     //switch the laserdot flag
  307.     bHasLaserDot = !bHasLaserDot;
  308.     //set up laserdot to on or off
  309.     SetupLaserDot();
  310.  
  311.     //turn off altfire flag
  312.     Pawn(Owner).bAltFire = 0;
  313. }
  314.  
  315. simulated function PlayAltFiring()
  316. {
  317.     PlayOwnedSound(sound'UnrealShare.Click', SLOT_None, 4.2);
  318. }
  319.  
  320. state Active
  321. {
  322.     function BeginState()
  323.     {
  324.         //when they bring up this weapon, initialize the laserdot to on or off
  325.         SetupLaserDot();
  326.         Super.BeginState();
  327.     }
  328. }
  329.  
  330. state DownWeapon
  331. {
  332. ignores Fire, AltFire, Animend;
  333.  
  334.     function BeginState()
  335.     {
  336.         //when they switch to another weapon, delete the laserdot
  337.         DestroyLaserDot();
  338.         Super.BeginState();
  339.     }
  340. }
  341.  
  342. defaultproperties {
  343.     bReplicateInstigator=true
  344.     bRecommendAltSplashDamage=False
  345. }
  346. After coding that, I felt that it didn't exercise a wide enough variety of networking techniques to teach you anything really useful. And besides, the targeting laser didn't actually do anything. So I decided to continue on and make a ripper razorblade that actually steered itself towards the dot. Here is where we'll be able to get into some of the more nitty-gritty replication details. Let's get started, shall we? :)
  347. First, we'll need to make a new steering razorblade. Create a class called GuidedRazorBlade that extends the regular bouncy razorblade. The code should look like the following;
  348. class GuidedRazorBlade extends Razor2;
  349.  
  350. First, we'll need to get our LaserRipper to shoot our GuidedRazorBlades. We'll modify the Fire() function to do this. If we have a laser dot, we'll want to shoot out our GuidedRazorBlades, while if we do not have a dot, we'll want to shoot out regular razorblades. The following code does this, leaving the actual projectile classes to be defined in the defaultproperties.
  351. function Fire( float Value )
  352. {
  353.     local Projectile proj;
  354.     if (bHasLaserDot)
  355.         ProjectileClass=Default.AltProjectileClass;
  356.     else
  357.         ProjectileClass=Default.ProjectileClass;
  358.  
  359.     if ( (AmmoType == None) && (AmmoName != None) )
  360.     {
  361.         // ammocheck
  362.         GiveAmmo(Pawn(Owner));
  363.     }
  364.     if ( AmmoType.UseAmmo(1) )
  365.     {
  366.         GotoState('NormalFire');
  367.         bPointing=True;
  368.         bCanClientFire = true;
  369.         ClientFire(Value);
  370.         if ( bRapidFire || (FiringSpeed > 0) )
  371.             Pawn(Owner).PlayRecoil(FiringSpeed);
  372.         if ( bInstantHit )
  373.             TraceFire(0.0);
  374.         else
  375.             proj = ProjectileFire(ProjectileClass, ProjectileSpeed, bWarnTarget);
  376.         if (GuidedRazorBlade(proj) != None)
  377.             GuidedRazorBlade.TargetLaserDot = LaserDot;
  378.     }
  379. }
  380. Let's add our ProjectileClass and AltProjectileClass to the defaultproperties now.
  381.     ProjectileClass=Class'Botpack.Razor2'
  382.     AltProjectileClass=Class'GuidedRazorBlade'
  383. Now we have the code set up to spawn one of two different projectiles, depending upon the laser dot. All we need to do now is get the GuidedRazorBlades to home in on their targets. We'll want to modify the velocity to aim towards the dot in the Tick() function. First, we'll start with a non-networking version, and after we get that working, we'll make it network-compatible.
  384. function Tick( float DeltaTime )
  385. {
  386.     local rotator NewRotation;
  387.  
  388.     /*
  389.     if we still have a targetlaserdot, then modify rotation
  390.     otherwise, just continue in a straight path
  391.  
  392.     adjust Rotation to point to new laserdot location, with some restrictions
  393.     (we don't want to have instant velocity change...it can only turn so quickly)
  394.     */
  395.  
  396.     if (TargetLaserDot != None )
  397.     {
  398.  
  399.         NewRotation = rotator( TargetLaserDot.Location - Location );
  400.         //Not perfectly DeltaTime-friendly, but close enough for this tutorial's purposes :)
  401.         SetRotation( Rotation + Normalize( NewRotation - Rotation ) * DeltaTime * 3 );
  402.         Velocity = Speed * vector(Rotation);
  403.     }
  404. }
  405. Walking though the above code, it checks to make sure that there is a dot. The dot will become invalid if the owner changes weapons, dies, or does a variety of things that should cause the GuidedRazorBlades to lose their tracking ability. If there is no dot, then this GuidedRazorBlade will lose it's guided ability, (since there's no dot to home in on,) and so it will continue in a straight line, according to it's velocity.
  406. If however, there is a dot to aim for, we must adjust our rotation to aim towards it. The above implementation is not completely framerate independent, but since it's only a single server running that code, nothing get's out of sync. It may act slightly differently depending upon your framerate. But I figured simplicity here was better than a lot of complex code that made it framerate-independent.
  407. The above code first gets a NewRotation. This is the direction the GuidedRazorBlade would ideally be pointing. It is obtained by subtracting the target location from it's location, and making a rotator that points in that direction. The code the Normalizes the rotator, (so that it's pointing in the most efficient direction,) and tells it to modify it's rotation a little bit in that direction. And finally, after it's rotation has been modified, it resets its velocity based upon it's rotation. Since the razorblades always maintain the same speed, we basically say: go at x mph in this direction.
  408. One problem you may have noticed, if you are testing this code at every step, is that when you turn the laser off, the guided razorblades still head towards the last known location of the laserdot. The reasoning behind this is simple, once you know it. When we delete the laser dot, we are destroy()ing it, and setting the weapon's LaserDot to None. Then any LaserDot != None checks will fail, and it will act like there is no laser dot. However, the GuidedRazorBlades still have a reference to a now-destroyed LaserDot. one approach would be to perform a foreach allactors(class'GuidedRazorBlade',grb) and remove any references to the laser dot. However, there is a simpler approach, in my opinion. When an actor is destroyed, its bDeleteMe property is set to true. So we can handle this in the GuidedRazorBlade itself. Just before it performs the if (TargetLaserDot != None ), we can add a check that looks at the TargetLaserDot's bDeleteMe, and sets the TargetLaserDot property to None if it has been deleted. To do this, we'll just add the following line in Tick() before the if statement:
  409. if (TargetLaserDot != None && TargetLaserDot.bDeleteMe) TargetLaserDot = None;
  410. That's all there really is to making a Guided RazorBlade. Now we get a bit more complex by aiming to make it work in netplay. Right now, the RazorBlades will continue to go in a straight line on the client, no matter how their velocity is adjusted on the server. It took me awhile to realize the cause of this problem. It turns out that Projectile sets bNetTemporary==true by default. Looking at the conditions for relevancy, we see:
  411. 3. If it is a temporary network actor, (as defined by bNetTemporary), and it's been sent to this client already, then it does not pass the first stage.
  412. Since all projectiles are bNetTemporary by default, this means they are spawned, given their initial values, and then forgotten about. Since a regular razorblade travels in the same motion on both the client and the server, it can be predicted with 100% accuracy on the client without any interaction on the part of the server. Some things however, do need to keep a connection open with the server. A Redeemer missile needs to tell the client if it got shot down out of the air, and shock projectiles need to keep a connection open in case they get blown up by a primary shock rifle shot, and explode as a combo. Since our values for our Velocity constantly change, we need to keep the replication connection open for this actor, and so we must turn bNetTemporary off.
  413. Now, we need to figure out an approach to get information replicated to the client. I'll go through a few ideas and examples, and finally end with the one that I finally used. (Yes, I went through a couple iterations in building this. :) My first idea was to have the location and velocity replicated to the client. This sounds like the job of a SimulatedProxy, with the exception of the fact that the location and velocity are not replicated to the client. One approach to force these to replicate is that used by the GuidedWarhead, (the guided redeemer missile,) and the guided razorjacks in the original Unreal. However, we are unable to change the replication statements for Location and Velocity as they are defined in the parent actor class. However, we can get something almost as good. To start, let's define our own location and velocity as follows, and replicate these variables to all clients:
  414. var vector ServerLocation, ServerVelocity;
  415.  
  416.  
  417. replication
  418. {
  419.     // Things the server should send to the client.
  420.     unreliable if( Role==ROLE_Authority )
  421.         ServerLocation, ServerVelocity;
  422. }
  423. This is simple enough to understand. Now let's picture how we'll actually use these variables. On the server, we will set ServerLocation to the actor's real Location each tick, and the same with the velocity. Then we'll let the replication statements handle the replication of these variables. On the client, we'll receive the location and velocity that is being replicated to us, and then plug them into the client's own Location and Velocity. Let's make an initial pass at this using Tick():
  424. if (Role == ROLE_Authority)
  425. {
  426.     ServerLocation = Location;
  427.     ServerVelocity = Velocity;
  428. }
  429. else
  430. {
  431.     SetLocation(ServerLocation);
  432.     Velocity = ServerVelocity;
  433. }
  434. This does what we said, but now what we want. :) Every tick, the location get's set back to the ServerLocation. This means that the velocity will have no effect on anything at all, clientside, since the ServerLocation is the overriding factor. It means that the projectile will stay at ServerLocation until a new ServerLocation update comes down the pipe. Even on my own machine connecting to a server on my own machine, that looks quite choppy. It'd look horrible under real conditions. We need a way to know when a new Location update comes in, and only update our local copy of the location at that point in time. Since the incoming variable will overwrite any local copy of the variable, we can set the ServerLocation to an identifiable value that means: We have already used this location. Whenever we get a ServerLocation, we'll use it, and then set our clientside copy of ServerLocation to be vect(0,0,0). This will be our flag, and we will not update our Location in the subsequent tick unless the replication causes the ServerLocation to be set to a real Location. And the same applies to velocity, of course. The following code does that:
  435. if (Role == ROLE_Authority)
  436. {
  437.     ServerLocation = Location;
  438.     ServerVelocity = Velocity;
  439. }
  440. else
  441. {
  442.     if (ServerLocation != vect(0,0,0) )
  443.     {
  444.         SetLocation(ServerLocation);
  445.         ServerLocation = vect(0,0,0);
  446.     }
  447.     if (ServerVelocity != vect(0,0,0) )
  448.     {
  449.         Velocity = ServerVelocity;
  450.         ServerVelocity = vect(0,0,0);
  451.     }
  452. }
  453. Now, the only remaining thing to be done is to make the function simulated, so that it will actually run on the client. Here's the completed GuidedRazorBlade, using this approach.
  454. class GuidedRazorBlade extends Razor2;
  455.  
  456. var LaserDot TargetLaserDot;
  457.  
  458. //used as a correctional factor for replacing the client-actor
  459. var vector ServerLocation;
  460.  
  461. replication
  462. {
  463.     // Things the server should send to the client.
  464.     unreliable if( Role==ROLE_Authority )
  465.         ServerLocation, ServerVelocity;
  466. }
  467.  
  468. simulated event Tick( float DeltaTime )
  469. {
  470.     local rotator NewRotation;
  471.  
  472.     if (Role == ROLE_Authority)
  473.     {
  474.         ServerLocation = Location;
  475.         ServerVelocity = Velocity;
  476.     }
  477.     else
  478.     {
  479.         if (ServerLocation != vect(0,0,0) )
  480.         {
  481.             SetLocation(ServerLocation);
  482.             ServerLocation = vect(0,0,0);
  483.         }
  484.         if (ServerVelocity != vect(0,0,0) )
  485.         {
  486.             Velocity = ServerVelocity;
  487.             ServerVelocity = vect(0,0,0);
  488.         }
  489.     }
  490.  
  491.     //if the laserdot we are tracking has been deleted, don't track it anymore
  492.     if (TargetLaserDot != None && TargetLaserDot.bDeleteMe) TargetLaserDot = None;
  493.  
  494.     /*
  495.     if we still have a targetlaserdot, then modify rotation
  496.     otherwise, just continue in a straight path
  497.  
  498.     adjust Rotation to point to new laserdot location, with some restrictions
  499.     (we don't want to have instant velocity change...it can only turn so quickly)
  500.     */
  501.     if (TargetLaserDot != None )
  502.     {
  503.         NewRotation = rotator( TargetLaserDot.Location - Location );
  504.         //Not perfectly DeltaTime-friendly, but close enough for this tutorial's purposes :)
  505.         SetRotation( Rotation + Normalize( NewRotation - Rotation ) * DeltaTime * 3 );
  506.         Velocity = Speed * vector(Rotation);
  507.     }
  508. }
  509.  
  510. //bNetTemporary set to false so that we continue to receive updates about this actor
  511. //RemoteRole is a SimulatedProxy by default
  512. defaultproperties {
  513.     bNetTemporary=False
  514. }
  515. If you try this in netplay, it will work fine. However, even though it works, it is not ideal. It is sending the locations and the velocities over the network many times per second. When I performed network usage tests with 'stat net', I found that shooting a large amount of GuidedRazorBlades caused bandwidth usage to increase by 2000 bytes / second compared to non-guided RazorBlades. This is really quite unacceptable. When brooding about how to go about fixing this, I came upon an idea. The TargetLaserDot is a lot easier to replicate than the changing velocities and locations, especially since it does not change. So technically, we should be able to completely simulate the client-side behavior by having the Velocity updated on the client, based upon the client's perception of where the LaserDot is. No location or velocity would be sent over the network, and it would be highly network-efficient. An approach that uses this would need to replicate TargetLaserDot, and would look like the following:
  516. replication
  517. {
  518.     // Things the server should send to the client.
  519.     unreliable if( Role==ROLE_Authority )
  520.         TargetLaserDot;
  521. }
  522.  
  523. simulated event Tick( float DeltaTime )
  524. {
  525.     local rotator NewRotation;
  526.  
  527.     //if the laserdot we are tracking has been deleted, don't track it anymore
  528.     if (TargetLaserDot != None && TargetLaserDot.bDeleteMe) TargetLaserDot = None;
  529.  
  530.     /*
  531.     if we still have a targetlaserdot, then modify rotation
  532.     otherwise, just continue in a straight path
  533.  
  534.     adjust Rotation to point to new laserdot location, with some restrictions
  535.     (we don't want to have instant velocity change...it can only turn so quickly)
  536.     */
  537.     if (TargetLaserDot != None )
  538.     {
  539.         NewRotation = rotator( TargetLaserDot.Location - Location );
  540.         //Not perfectly DeltaTime-friendly, but close enough for this tutorial's purposes :)
  541.         SetRotation( Rotation + Normalize( NewRotation - Rotation ) * DeltaTime * 3 );
  542.         Velocity = Speed * vector(Rotation);
  543.     }
  544. }
  545.  
  546. Since the TargetLaserDot is now replicated, it will now be available for use by the client in adjusting the velocity. There are a variety of little problems with this approach, however. First, there is the possibility that the TargetLaserDot will be replicated, but the LaserDot itself will not be relevant to the client. If you remember from our relevancy discussion, relevancy is not recursive. The simple fact that the TargetLaserDot is replicated does not mean that the LaserDot itself will have it's Location replicated to the client. This situation could arise if you are looking down a hallway into a larger room. The LaserDot could be on a wall somewhere in that room, hidden, and thus not relevant, from the client in the hallway. The client could see the GuidedRazorBlades fly by, however, but they would not be homing correctly, since the client doesn't know where the LaserDot is, and so neither do the client's SimulatedProxy GuidedRazorBlades. That situation is probably very rare, and the chances of the client knowing that their version of the GuidedRazorBlades is off is slim, as well.
  547. However, there is another problem that can manifest itself in many ways, but in a more subtle manner. The client is getting replication updates about the LaserDot via a DumbProxy, without simulation. This means that the client has a choppy version of what the server sees. The client is making it's local versions of the GuidedRazorBlades behave as if the LaserDot were completely accurate. This means that the client is pretending that it knows where the LaserDot is, where in truth, it only has a rough approximation. The small discrepancies in the actual Location can cause small discrepancies in the client's velocity adjustments for the GuidedRazorBlades. When a GuidedRazorBlade then proceeds to fly around for any significant length of time where it's TargetLaserDot changes significantly, the small errors in Velocity will add up into a significant change in the client's Location, and the client's idea of where the GuidedRazorBlade is will differ significantly from the server's. This means the client can think it got hit from GuidedRazorBlades where it actually didn't, or even get hit by ghost GuidedRazorBlades that the client never saw coming. This is really quite unacceptable, especially since it happens quite often.
  548. One approach to fixing this last problem is to get correctional Location updates, but leave the Velocity alone, and calculate the Velocity clientside. It's still possible to get errors in the Velocity, but since the Location is constantly being updated, any small errors in the velocity will only translate to small errors in the Location, which is constantly being corrected. How can we do this. Using a DumbProxy might sound like a good approach, since it will handle the Location updates for us. However, there is an annoying catch. DumbProxies are not Tick()ed clientside, because of the nature of DumbProxy. This means that our code that sets the Velocity clientside will not work, since it will never be executed. Timer won't work either, since that also is not run clientside for DumbProxies, no matter what you do.
  549. The only way to fix this is to use the Location replication approach we discussed above when we replicated both Location and Velocity. This time however, we can make some more optimizations. We don't want to send the Location updates to the client if the GuidedRazorBlade is travelling in a straight line. This is the case when the TargetLaserDot has been destroyed, and the GuidedRazorBlades are travelling unguided, in a straight line. As such, we only want to use the server's correctional updates if we have a valid TargetLaserDot. In addition, we can modify the replication check on the ServerLocation variable so that it will not even be sent without a valid TargetLaserDot. Combining the two approaches above, we come out with what I believe is the best approach. The entire class GuidedRazorBlade class is listed below:
  550. class GuidedRazorBlade extends Razor2;
  551.  
  552. var LaserDot TargetLaserDot;
  553.  
  554. //used as a correctional factor for replacing the client-actor
  555. var vector ServerLocation;
  556.  
  557. replication
  558. {
  559.     // Things the server should send to the client.
  560.     reliable if( Role==ROLE_Authority )
  561.         TargetLaserDot;
  562.  
  563.     unreliable if (Role==ROLE_Authority && TargetLaserDot != None)
  564.         ServerLocation;
  565. }
  566.  
  567. simulated event Tick( float DeltaTime )
  568. {
  569.     local rotator NewRotation;
  570.  
  571.     //if the laserdot we are tracking has been deleted, don't track it anymore
  572.     if (TargetLaserDot != None && TargetLaserDot.bDeleteMe) TargetLaserDot = None;
  573.  
  574.     /*
  575.     if we still have a targetlaserdot, then modify rotation
  576.     otherwise, just continue in a straight path
  577.  
  578.     adjust Rotation to point to new laserdot location, with some restrictions
  579.     (we don't want to have instant velocity change...it can only turn so quickly)
  580.     */
  581.     if (TargetLaserDot != None )
  582.     {
  583.         if (Role == ROLE_Authority)
  584.         {
  585.             ServerLocation = Location;
  586.         }
  587.         else if (ServerLocation != vect(0,0,0) )
  588.         {
  589.             SetLocation(ServerLocation);
  590.             ServerLocation = vect(0,0,0);
  591.         }
  592.  
  593.         NewRotation = rotator( TargetLaserDot.Location - Location );
  594.         //Not perfectly DeltaTime-friendly, but close enough for this tutorial's purposes :)
  595.         SetRotation( Rotation + Normalize( NewRotation - Rotation ) * DeltaTime * 3 );
  596.         Velocity = Speed * vector(Rotation);
  597.     }
  598. }
  599.  
  600.  
  601. //bNetTemporary set to false so that we continue to receive updates about this actor
  602. //RemoteRole is a SimulatedProxy by default
  603. defaultproperties {
  604.     bNetTemporary=False
  605. }
  606. Now, I'll discuss what can still go wrong with this approach. The problem of the TargetLaserDot being replicated, but the LaserDot itself not being relevant is still a problem. However, it should happen quite infrequently, and the client should never really notice the effects of it, anyway. In addition, the client can still have a slightly inaccurate version of the Location of a GuidedRazorBlade, but that was discussed before. No significant error will ever be accumulated because of the ServerLocation updates the GuidedRazorBlades get. The ServerLocation updates are still being sent over the net, and with a lot of GuidedRazorBlades, they can consume bandwidth. It's probably best to decrease the rate of fire for the GuidedRazorBlades, to save on bandwidth. This can be accomplished by playing the Fire animation at a slower rate, but I'll leave that up to the reader. And finally, the Guided RazorBlades are quite powerful, and destroy the balance of firepower in UT. To balance this out, you can reduce the Damage of the GuidedRazorBlades, or reduce the fire rate. I personally would recommend the latter approach, since it would also help with network bandwidth issues.
  607. Congratulations, you've created a complex weapon, and it works in Netplay! Granted, there will probably be a lot more of these types of weapons appearing in mods now that it is easily possible, but I'm not sure if that's a bad thing. There's already a lot of instant-hit weapons that shoot bullets. Variety is good, right? :)
  608. Oh, and as a little present for completing this tutorial, I suggest you try this weapon out on DM-Morbias][. Summon the weapon, and go to the upper level. Then shoot your GuidedRazorBlades at one of the pillars on the opposite side of the map, and leave your corsshair there.
Advertisement
Add Comment
Please, Sign In to add comment