Synchronized Multiplayer Spaceship in C#
This tutorial demonstrates how to create a networked hover-capable space jet with advanced physics, weapon systems, and multi-input support. The system combines atmospheric flight mechanics with space-style controls for a unique hybrid experience.
Key Concepts
- Hybrid Physics: Combines atmospheric hover mechanics with spaceflight controls
- Dual Input Modes: Fully supports both VR and desktop controls
- Weapon Systems: Network-synchronized primary and secondary weapons
- Dynamic Hover: Adaptive ground effect with inverted flight support
The jet blends two flight models: atmospheric-style hover when near surfaces and space-style flight at altitude. This creates intuitive controls where players automatically transition between modes. Weapon systems use event-based networking to minimize bandwidth while maintaining precise synchronization. The hover system dynamically adjusts force based on ground proximity and ship orientation, including special handling for inverted flight.
Constants and Fields
The constants and fields establish the spacecraft's core behavioral parameters and physical relationships. These values define the operational envelope within which all other systems function, from the engine's thrust curve to the network synchronization framework. By codifying these relationships upfront, we create a predictable foundation for the ship's physics, controls, and multiplayer synchronization. The syncObject
serves as a critical network conduit, efficiently packaging control inputs into a single synchronized entity while minimizing bandwidth usage. These definitions represent more than mere values. They embody the spacecraft's fundamental design philosophy and performance characteristics.
// Engine constants
private const float ENGINE_IDLE_THRUST = 0.1f;
private const float ENGINE_MAX_THRUST = 1f;
private const float TARGET_HOVER_HEIGHT = 5f;
private const float HOVER_DAMPING = 0.5f;
// Engine states
public enum EngineState
{
Off,
Startup,
Idle,
Acceleration
}
// Network sync object
public Transform syncObject; // Synchronizes throttle (x), pitch (y), yaw (z), roll (euler z)
// Core components
public Rigidbody Jet_RB;
public MLStation station;
public Transform centerOfMass;
// Weapon systems
public GameObject LeftWeapon;
public GameObject RightWeapon;
public Transform Left_WeaponShootPoint;
public Transform Right_WeaponShootPoint;
public float weaponCooldown = 0.2f;
// Internal state
private float throttle = 0;
private float pitch = 0;
private float yaw = 0;
private float roll = 0;
private bool underLocalPlayerControl = false;
private JetEngine jetEngine = new JetEngine();
The physics system is designed around several key principles:
- Energy Efficiency: The idle thrust (0.1) provides just enough force to maintain stability without wasting energy, while max thrust (1.0) gives a 10:1 power band for dynamic maneuvering.
- State Awareness: The EngineState enum creates clear operational phases that affect both physics and audio systems. This prevents abrupt transitions between power states.
- Network Optimization: The syncObject transform is repurposed as a data container because Unity's native transform synchronization is highly optimized. This packs four control dimensions into one efficiently-synced object.
Initialization
During initialization, we methodically bring all spacecraft systems to an operational state. The physics parameters are carefully calibrated to achieve the desired balance between responsiveness and inertia, with mass distribution and drag coefficients tuned for the intended flight characteristics. Network event handlers are registered to ensure reliable weapon synchronization, while audio sources are pre-initialized to eliminate latency during critical playback moments. This phase transforms the spacecraft from a static asset into a fully functional vehicle, with all subsystems prepared for their respective roles in the simulation loop. The station event listeners established here create the essential linkage between player input and vehicle response.
void Start()
{
InitializePhysics();
// Set up network event handlers
tokenShipShoot = this.AddEventHandler(EVENT_SHIP_SHOOT, OnShipShootEvent);
// Configure station callbacks
station.OnPlayerSeated.AddListener(OnPlayerEnterStation);
station.OnPlayerLeft.AddListener(OnPlayerLeftStation);
// Initialize audio sources
engineIdleAudio.Play();
engineAccelerationAudio.Play();
hoverAudio.Play();
}
private void InitializePhysics()
{
Jet_RB.mass = jetMass;
currentHoverForce = jetMass * hoverForceMultiplier;
maxThrustForce = jetMass * thrustForceMultiplier;
Jet_RB.centerOfMass = centerOfMass.localPosition;
Jet_RB.drag = 0.2f;
Jet_RB.angularDrag = 2f;
}
Key initialization considerations:
- Physics Readiness: The rigidbody is configured with appropriate mass and drag values before any forces are applied, preventing unstable initial conditions.
- Audio Preparation: All audio sources begin playing immediately (but silently) to avoid latency when sounds need to trigger. Unity's audio system has significant startup delays if sources aren't pre-warmed.
- Event Systems: The weapon event handler is registered early to ensure network messages are processed even during the startup sequence.
Network Synchronization
The synchronization system implements a deterministic control scheme that maintains consistency across all clients. By encoding control inputs into the transform of a networked object, we leverage Unity's optimized transform synchronization while maintaining precise control over bandwidth usage. This approach ensures that all clients process identical input sequences, allowing local physics simulations to produce identical results without the need for continuous state synchronization. The separation between local input authority and remote input interpretation prevents network conflicts while providing seamless interaction for all participants.
void Update()
{
if (underLocalPlayerControl)
{
// Local control - write inputs to sync object
syncObject.localPosition = new Vector3(throttle, pitch, yaw);
syncObject.localRotation = Quaternion.Euler(pitch * 45f, yaw * 45f, roll * 45f);
}
else
{
// Remote control - read from sync object
var localPos = syncObject.localPosition;
throttle = localPos.x;
pitch = localPos.y;
yaw = localPos.z;
var localRot = syncObject.localRotation.eulerAngles;
roll = localRot.z / 45f;
}
}
Design rationale:
-
Input Compression: All control axes are normalized to [-1,1] range before encoding into the transform. This provides consistent precision regardless of network conditions.
-
Authority Separation: The underLocalPlayerControl flag ensures only the pilot's inputs are authoritative. Remote clients never write to the sync object.
-
Frame Independence: Using Update() rather than FixedUpdate() for input sync ensures controls remain responsive even if physics frames are delayed.
Input Handling
Input processing translates platform-specific control schemes into normalized vehicle commands. The system accommodates both desktop and VR input paradigms through discrete processing pipelines that maintain consistent vehicle behavior regardless of input method. Desktop controls employ graduated response curves and modifier keys to maximize precision from binary inputs, while VR controls utilize raw analog values for direct manipulation. This layer abstracts hardware differences to present a unified control model to the physics systems, ensuring predictable behavior across all input devices.
Desktop Controls
private void HandleDesktopModeInput(UserStationInput input)
{
// Throttle control with return-to-zero
if (Mathf.Abs(input.KeyboardMove.y) > 0.1f)
{
throttle += input.KeyboardMove.y * throttleSensitivity;
}
else
{
throttle = Mathf.Lerp(throttle, 0, throttleReturnSpeed * Time.deltaTime);
}
// Sprint modifier changes control scheme
if (input.LeftSprint)
{
// Yaw control
yaw = input.KeyboardMove.x * yawSensitivity;
lateralMovementInput = 0;
// Vertical movement
if (input.Jump) ApplyVerticalForce(1f);
if (input.Crouch) ApplyVerticalForce(-1f);
}
else
{
// Lateral movement
lateralMovementInput = input.KeyboardMove.x;
yaw = 0;
}
// View toggle
if (input.RightSprint) ToggleThirdPersonView();
}
Desktop control philosophy:
-
Progressive Input: Throttle changes are cumulative rather than absolute, allowing finer control through repeated key presses.
-
Context-Sensitive Controls: The sprint modifier changes control behavior to suit different flight phases - precision maneuvering when held, general flight when released.
-
Fail-Safe Design: The auto-centering throttle prevents uncontrolled acceleration if input is lost.
VR Controls
private void HandleVRModeInput(UserStationInput input)
{
// Analog throttle control
throttle = Mathf.Lerp(throttle, input.LeftControl.y, throttleSensitivity);
// Lateral movement
lateralMovementInput = input.LeftControl.x;
// Vertical movement
ApplyVerticalForce(input.RightControl.y);
// Rotation controls
pitch = input.RightControl.y * pitchSensitivity;
yaw = input.RightControl.x * yawSensitivity;
// Roll from grab buttons
if (input.LeftGrab > 0.5f) roll -= rollSensitivity;
if (input.RightGrab > 0.5f) roll += rollSensitivity;
}
VR control considerations:
- Ergonomic Mapping: Controls are split between hands - left controls movement, right controls orientation.
- Analog Precision: Raw controller input is used directly where possible, preserving the full range of motion.
- Physical Metaphors: Grip buttons intuitively trigger roll maneuvers, matching real-world expectations.
Physics Simulation
The physics system orchestrates the interaction of multiple force application systems to produce the spacecraft's characteristic flight model. By evaluating altitude and orientation, it automatically blends between atmospheric hover and spaceflight modes, each with distinct force profiles. The stabilization subsystem provides automatic attitude correction while preserving manual control authority. This modular approach allows for targeted tuning of individual flight modes while maintaining overall system cohesion. The fixed timestep execution guarantees consistent simulation results across varying frame rates.
Hybrid Flight System
The hybrid flight system serves as the central coordinator for the spacecraft's dynamic movement capabilities, intelligently blending different flight modes based on environmental context. This core physics routine executes within Unity's FixedUpdate loop to ensure deterministic simulation regardless of rendering frame rate. By continuously evaluating altitude through CalculateAltitude(), the system automatically transitions between high-precision hover mechanics near surfaces and unconstrained spaceflight behavior at higher altitudes.
The force application sequence follows carefully prioritized ordering: hover stabilization (when active) takes precedence, followed by primary thrust, rotational control inputs, stabilization adjustments, and finally lateral movement forces. This specific ordering prevents force competition and ensures predictable vehicle behavior. The engine state calculation operates independently of direct control input, simulating realistic spool-up and spool-down characteristics that affect available thrust. This architecture creates a flight model that responds naturally to both player input and environmental factors while maintaining precise network synchronization through its input-driven design.
void FixedUpdate()
{
CalculateAltitude();
if (underLocalPlayerControl)
{
// Apply appropriate forces based on altitude
if (groundDistance <= maxHoverHeight)
{
ApplyHoverForce();
}
ApplyThrust();
ApplyRotation();
ApplyStabilization();
ApplyLateralMovement();
}
jetEngine.CalculateCurrentEngineState(throttle);
}
Flight model architecture:
- Context-Aware Physics: The system automatically switches between hover and spaceflight modes based on terrain proximity.
- Priority-Based Force Application: Forces are applied in a specific order (hover → thrust → rotation) to prevent system conflicts.
- Deterministic Simulation: All physics calculations use FixedUpdate() to ensure consistent behavior across different frame rates.
Hover Physics : Ground Hovering Module
The hover system simulates surface proximity effects through dynamically calculated force application. Using raycast distance measurements, it generates a non-linear lift profile that diminishes with altitude, creating natural ground effect behavior. The implementation includes specialized handling for inverted flight conditions, applying additional force to compensate for unstable orientations. Velocity-based damping prevents oscillatory behavior while maintaining responsiveness. This system interacts with the core physics engine to blend seamlessly with other force applications during mode transitions.
private void ApplyHoverForce()
{
bool isUpsideDown = Vector3.Dot(transform.up, Vector3.up) < 0;
Vector3 hoverDirection = isUpsideDown ? -transform.up : transform.up;
// Calculate hover strength based on ground distance
float hoverStrength = (1 - (groundDistance / maxHoverHeight)) * bounceReductionFactor;
// Apply force with inverted flight multiplier
float multiplier = isUpsideDown ? invertedHoverForceMultiplier : 1f;
Vector3 hoverForce = hoverDirection * hoverStrength * currentHoverForce * multiplier;
// Add damping
float verticalVelocity = Vector3.Dot(Jet_RB.velocity, hoverDirection);
Jet_RB.AddForce(hoverForce - (hoverDirection * verticalVelocity * HOVER_DAMPING), ForceMode.Force);
// Add upright assist if needed
if (isUpsideDown)
{
Vector3 torqueDirection = Vector3.Cross(transform.up, Vector3.up);
Jet_RB.AddTorque(torqueDirection * uprightAssistTorque);
}
}
Hover system details:
-
Orientation Awareness: The system works equally well when upside down, with increased force to compensate for the unstable position.
-
Ground Effect Simulation: Hover strength follows a inverse-square curve relative to altitude, mimicking real-world ground effect physics.
-
Stability Augmentation: The damping system prevents oscillating "bouncing" while the righting torque helps recover from flips.
Thrust System : Propulsion Management
Thrust vector application follows a gradual alignment algorithm that simulates inertial direction change. Rather than instantaneously matching the spacecraft's orientation, the thrust vector smoothly interpolates toward the current forward axis, simulating realistic propulsion system behavior. The resulting force application accounts for both current throttle position and engine state, creating a nuanced relationship between control input and acceleration. This implementation provides the foundation for the spacecraft's acceleration profile while integrating cleanly with other force systems.
private void ApplyThrust()
{
// Gradually align thrust with ship orientation
currentThrustDirection = Vector3.Slerp(
currentThrustDirection,
transform.forward,
thrustAlignmentSpeed * Time.fixedDeltaTime
);
// Apply force
Jet_RB.AddForce(currentThrustDirection * jetEngine.thrust * maxThrustForce, ForceMode.Force);
}
Thrust physics rationale:
- Vector Alignment: The Slerp-based alignment simulates inertial vectoring - thrust doesn't instantly change direction.
- Engine Responsiveness: The alignment speed creates different handling characteristics - fast for fighters, slow for transports.
- Power Scaling: Thrust scales linearly with throttle input but can be modified by engine state.
Weapon Systems : Combat Integration
Weapon operation combines client-side prediction with event-based network synchronization to create responsive combat mechanics. The aiming system utilizes constrained rotation algorithms to ensure weapons track within their designated firing arcs while following the player's view direction. Firing events propagate through a minimal network message that triggers local effects processing, reducing bandwidth requirements while maintaining visual fidelity. The cooldown management system prevents weapon spam while providing clear player feedback about firing readiness.
Aiming and Firing
private void HandleWeaponAiming()
{
if (playerCamera == null) return;
// Calculate aim direction from camera
Vector3 targetPoint = playerCamera.position + playerCamera.forward * weaponRange;
// Apply constrained aiming to all weapons
ApplyConstrainedWeaponAim(LeftWeapon.transform, targetPoint);
ApplyConstrainedWeaponAim(RightWeapon.transform, targetPoint);
}
private void FireWeapon(int weaponID)
{
// Networked firing using events
this.InvokeNetwork(EVENT_SHIP_SHOOT, EventTarget.All, null, weaponID);
}
void OnShipShootEvent(object[] args)
{
int weaponID = (int)args[0];
switch(weaponID)
{
case 1: FirePrimaryWeapon(); break;
case 2: FireSecondaryWeapon(); break;
}
}
Weapon system design:
- View-Aligned Aiming: Weapons track where the player is looking, not where the ship is pointing.
- Network Efficiency: Only the weapon ID is synced - each client handles their own ballistic calculations.
- Fire Control Separation: The firing logic is separated from input detection, allowing multiple trigger sources.
Player Events
Player interaction events manage the transition between autonomous and piloted vehicle states. The entry sequence initiates a startup procedure that gradually brings systems online, while the exit sequence ensures safe vehicle deactivation. Camera coordination maintains proper view perspectives during control transitions. These handlers maintain the vehicle's operational state while ensuring clean handoff between different control authorities, whether local, remote, or autonomous.
private void OnPlayerEnterStation()
{
// Play startup sequence
foreach (AudioClip clip in ShipSounds)
{
engineStartupAudio.PlayOneShot(clip);
}
jetEngine.state = EngineState.Startup;
jetEngine.startTime = Time.time;
// Find player camera
currentPilot = station.GetPlayer();
if (currentPilot.IsLocal) StartCoroutine(FindPlayerCamera());
}
private void OnPlayerLeftStation()
{
// Reset controls
throttle = pitch = yaw = roll = 0;
jetEngine.state = EngineState.Off;
currentPilot = null;
}
Player interaction flow:
- Startup Sequence: The multi-clip audio playback creates a convincing power-up effect.
- Progressive Activation: The engine takes 3 seconds to reach idle, preventing instant escapes.
- Clean State Transition: Controls reset completely when vacated, preventing phantom inputs.
Audio System
The audio management system dynamically blends multiple sound layers based on operational parameters. Engine sounds crossfade between idle and acceleration profiles proportional to thrust output, while hover effects modulate according to ground proximity. Each audio source's volume and pitch track relevant physical quantities, creating an accurate sonic representation of vehicle state. This implementation provides players with continuous situational awareness through auditory cues while maintaining performance efficiency.
private void HandleEngineAudioEffects()
{
if (jetEngine.state == EngineState.Off)
{
// Silence all audio
engineAccelerationAudio.volume = 0;
engineIdleAudio.volume = 0;
}
else if (jetEngine.state == EngineState.Startup)
{
// Fade in idle sound
engineIdleAudio.volume = Mathf.Lerp(0, 1, (Time.time - jetEngine.startTime) / 3f);
}
else
{
// Blend between idle and acceleration sounds
float thrustFactor = Mathf.InverseLerp(ENGINE_IDLE_THRUST, ENGINE_MAX_THRUST, jetEngine.thrust);
engineAccelerationAudio.volume = thrustFactor;
engineIdleAudio.volume = 1 - thrustFactor;
// Adjust pitch
engineAccelerationAudio.pitch = Mathf.Lerp(0.8f, 1.2f, thrustFactor);
}
// Hover audio
hoverAudio.volume = (groundDistance <= maxHoverHeight) ?
Mathf.Lerp(0.5f, 0.1f, groundDistance/maxHoverHeight) : 0;
}
Audio design principles:
-
Stateful Mixing: Audio layers are weighted based on engine operational state.
-
Physical Modeling: Pitch changes simulate real turbine spool-up characteristics.
-
Environmental Feedback: Hover sounds only play when near surfaces, providing audible terrain cues.
Best Practices
- Network Optimization:
- Sync only inputs, not physics states
- Use single transform for all control synchronization
- Event-based weapon firing
- Physics Tuning:
- Different drag values for local vs remote control
- Separate hover and space flight modes
- Gradual thrust vector alignment
- Input Handling:
- Provide alternative control schemes
- Implement input smoothing
- Support both absolute and relative controls
- Audio Design:
- Layer multiple engine sounds
- Dynamic hover audio based on altitude
- Pitch modulation for acceleration
Complete Example
To view the full example C# script, click here!
This implementation provides a complete networked space jet with:
- Hybrid hover/space flight physics
- Network-synchronized weapons
- Dual control schemes (VR/desktop)
- Dynamic audio feedback
- Smooth network synchronization
The system automatically transitions between flight modes based on ground proximity, creating intuitive controls that work in both planetary and space environments. Weapon systems maintain precision across the network while minimizing bandwidth usage.