I'm a doctor, not a mechanic
Legacy:Dma/JetPack
I was just writing this to see how hard it would be...
Known bugs:
- No HUD notification of the fuel status.
- Expire message doesn't disappear.
- You can't see your own trails in first-person mode.
- There isn't any physical jetpack attached to shoulder or spine bones.
- Client-side simulation isn't right.
Dma: I just realized that PostNetBeginPlay is used to catch variable replications. And I was the one who posted it at Creating_Actors_And_Objects. Looks like I forgot. :-)
If you have any hints about the proper method of replicating the acceleration caused by the jet pack so as to avoid ClientAdjustPosition calls (the jerkiness that results when the authoritative server tells the client where the pawn should be), I would be very grateful. The calls seem to have changed since UT1.
Also, feel free to make comments on this page. Feel free to chastise me for my overuse of the simulated keyword. Perhaps I should use an autonomous proxy or something.
This whole "project" is actually my excercise in understanding the player movement commands under lag and packet-loss conditions, not to mention the pitfalls of the visibility of attached objects and changing relevance of actors.
//============================================================================= // JetPack // http://www.coe.uncc.edu/~danderse/ //============================================================================= class JetPack extends Powerups; replication { // Send these variables to the client. reliable if (Role==ROLE_Authority) Strength, AmbJetSound, JetTrailClass, JetTrail, SoundActor; // Server calls these functions on the client. reliable if (Role==ROLE_Authority) ClientGotoState; } var xEmitter JetTrail; // The Jet Trail. var JetPackInteraction OurInteraction; // Our hud stuff! var bool bGotOwner; var() class<xEmitter> JetTrailClass< SEMI > // This isn't used yet. var() sound AmbJetSound; var() float Strength; var JetPackSound SoundActor; var int TickCount; //============================================================================= // This little piece of garbage waits until the owner is successfully // replicated to the client. This is a bizarre way to fix accessed nones. //============================================================================= state ActivateWhenGetOwner { simulated function BeginState() { if (Owner != None) { log("ActivateWhenGetOwner. Owner=" $ Owner); bNetNotify=false; GotoState('Activated'); } } simulated function PostNetReceive() { Super.PostNetReceive(); log("Got PostNetReceive. Owner=" $ Owner); if (Owner != None) { bNetNotify=false; GotoState('Activated'); } } } //============================================================================= // I hate this. 'Activate' is not simulated so it must manually // instruct the client to ALSO go into the 'Activated' state. // // However, Owner (and probably some other properties) are not // valid for the first few ticks. Therefore, I must instruct the // client to go to the intermediate state of 'ActivateWhenGetOwner', // which uses the new PostNetReceive() notification function that // is enabled when bNetNotify=true. Since the 'Activated' state must // have access to Owner, I make the client wait until it gets an Owner // before activating. // This usually only takes a few ticks. //============================================================================= simulated function ClientGotoState(name NewState) { log("ClientGotoState: " $ String(NewState)); GotoState(NewState); } function PerformMove(PlayerController PC, vector Accel, float DeltaTime) { local SavedMove sm; sm = PC.getFreeMove(); sm.SetMoveFor(PC, DeltaTime, Accel, PC.DoubleClickDir); sm.SavedLocation = PC.Pawn.Location; sm.SavedVelocity = PC.Pawn.Velocity; } state Activated { simulated function BeginState() { local Pawn P; local PlayerController PC; // Be sure that we are no longer being distracted by // notifications of replication events. bNetNotify=false; P = Pawn(Owner); if (Role == ROLE_Authority) { // Give the player's spinebone an effect. Make sure this works. JetTrail = spawn(class'SpeedTrail', P,, P.Location, P.Rotation); JetTrail.SetPhysics(PHYS_None); JetTrail.bSuspendWhenNotVisible=false; if (P != None) { P.AttachToBone(JetTrail, 'Bip01 Spine2'); JetTrail.SetRelativeLocation(Vect(-10,50,-50)); } } if (Role == ROLE_Authority) { log("Calling spawn..."); SoundActor = Spawn(class'JetPackSound',P,,P.Location, P.Rotation); log("Spawn got: " $ SoundActor); } // Add some stuff to the HUD. if (Level.NetMode != NM_DedicatedServer) { PC = PlayerController(P.Controller); if (PC != None && Viewport(PC.Player) != None) { // Player is being controlled locally at the time of activation! // Pre-existing Global Interactions: // GUIController'Package.InteractionMaster0.GUIController0' // ExtendedConsole'Package.InteractionMaster0.ExtendedConsole0' // Create a local interaction. OurInteraction = JetPackInteraction( PC.Player.InteractionMaster.AddInteraction("JetPack.JetPackInteraction", PC.Player)); } } Super.BeginState(); } simulated function EndState() { if (Role == ROLE_Authority && JetTrail != None) { JetTrail.Destroy(); } if (SoundActor != None) { SoundActor.SetSound(None); SoundActor.Destroy(); } // Get rid of our HUD interaction. if (OurInteraction != None) { OurInteraction.DieAlready(); OurInteraction = None; } Super.EndState(); } simulated function Tick(float DeltaTime) { local PlayerController PC; local Pawn P; local vector X,Y,Z; local vector Accel; P = Pawn(Owner); PC = PlayerController(P.Controller); if ((PC != None) && (P != None)) { if ((PC.bDuck != 0) && (P.Physics == PHYS_Falling)) { // Turn on the Jet Trail. Unpause the xEmitter. if (JetTrail != None) { JetTrail.mRegenPause=False; } // Thrust the player. GetAxes(PC.Rotation, X, Y, Z); // Z = Normal(Z); Accel = Z*Strength; /* // Try to replicate client movement to server. if (Role < ROLE_Authority) { PerformMove(PC, Accel, DeltaTime); } */ P.AddVelocity(Z*Strength*DeltaTime); if (Role < ROLE_Authority) { PC.ShortServerMove(Level.TimeSeconds, P.Location, PC.bRun!=0, PC.bDuck!=0, PC.bJumpStatus, P.Rotation.Roll / 256, 0); } // log("Would replicate move - accel = " $ String(-4*Accel)); if (Level.NetMode != NM_DedicatedServer) { // log("Replicating move - accel = " $ String(-4*Accel)); // PC.ReplicateMove(DeltaTime, -4*Accel, PC.DoubleClickDir, rot(0,0,0)); } // Turn on the jetpack sound. if (SoundActor != None) { SoundActor.SetSound(AmbJetSound); } // Make it burn fuel. Charge -= (DeltaTime * 100); if (Charge <= 0) { UsedUp(); } } else { // Turn off the Jet Trail. Pause the xEmitter. if (JetTrail != None) { JetTrail.mRegenPause=True; } // Turn off the jetpack sound. if (SoundActor != None) { SoundActor.SetSound(None); } } // if (not) thrusting // ... } // Global.Tick(DeltaTime); } } // We are no longer being held. function Destroyed() { GotoState(''); // Get rid of our HUD interaction. if (OurInteraction != None) { OurInteraction.DieAlready(); OurInteraction = None; } // Remove from player's inventory. Super.Destroyed(); } function Activate() { if( bActivatable ) { GoToState('Activated'); // I suppose the first part is redundant below. if ((Role == ROLE_Authority) && (Level.NetMode != NM_ListenServer)) { // The client is being told to activate once it gets an owner. ClientGotoState('ActivateWhenGetOwner'); } } } simulated function PostBeginPlay() { Enable('Tick'); Super.PostBeginPlay(); } defaultproperties { RemoteRole=ROLE_SimulatedProxy ExpireMessage="Your jetpack ran out of gas." bAutoActivate=True bActivatable=True bNetNotify=True PickupClass=class'JetPackPickup' Charge=10000 Strength=3000 AmbJetSound=Sound'WeaponSounds.BaseGunTech.BTranslocatorHoverModule' JetTrailClass=class'RocketTrailSmoke'; }
class JetPackInteraction extends Interaction; var int Fuel; function GetFuelFromViewTarget() { local Pawn VT; local JetPack JP; VT = Pawn(ViewportOwner.Actor.ViewTarget); if (VT != None) { // Viewport is looking at a pawn. JP = JetPack(VT.FindInventoryType(class'JetPack')); if (JP != None) { Fuel = JP.Charge; } } } function Initialized() { log(self@"I'm alive"); } function PostRender( canvas Canvas ) { local Pawn VT; VT = Pawn(ViewportOwner.Actor.ViewTarget); GetFuelFromViewTarget(); Canvas.SetPos(20,300); Canvas.Style = 1; Canvas.SetDrawColor(255,255,255); Canvas.DrawText("Fuel remaining: " $ String(Fuel)); } // Stops the magic. function DieAlready() { log("JPI: Dying!"); // Dunno why. bVisible=False; bActive=False; bNativeEvents=False; bRequiresTick=False; // Remove ourself. Master.RemoveInteraction(Self); } // Called when level changes. function NotifyLevelChange() { DieAlready(); } defaultproperties { bVisible=true bActive=True }
//============================================================================= // JetPackSound. //============================================================================= class JetPackSound extends Actor; replication { reliable if (Role < ROLE_Authority) ServerSetSound; } function ServerSetSound(Sound S) { AmbientSound=S; } function SetSound(Sound S) { AmbientSound=S; ServerSetSound(S); } defaultproperties { RemoteRole=ROLE_None Physics=PHYS_Trailer SoundVolume=200 SoundPitch=56 bAlwaysRelevant=true bHidden=true }
Category:Legacy Custom Class (UT)//Category:Legacy Custom Class (UT2004)
Comments[edit]
SocratesJohnson: WRT ClientAdjustPosition() jerkiness, I experienced the same thing while working on my Freefall mutator. It seems to happen when a client explicitly tries to set the velocity of the Pawn. Try just working with Acceleration i.e. change the P.AddVelocity(Z*Strength*DeltaTime) line to adjust acceleration instead, for a start, perhaps make it decay over time. I don't have the Pawn source in front of me but just modifying P.Acceleration seemed to work in my case and that gets replicated back to the server when the physics engine does its thing. I'm pretty new to UnrealScript so your milage may vary. :D I'd love to see this in action!