Networked Projectiles

hvault
hvault
edited February 2016 in Photon Bolt
A few people have asked how they can make network based projectiles.
The kind where you can't just do a raycast like a bullet, as the projectile is much slower and can be dodged by the players or AI. Similar to what you see in space style shooters like the old Wing Commanders / Freelancer, or something like the slower rockets in some FPS games, missiles, etc....

Full credit for this goes to fholm and someone from the old forums who's name I've long since forgotten.
They had some nice videos explaining the issue and showing the accuracy levels after they had implemented this solution.

I've heavily modified what they had posted to suit the requirements we have, such as object pools, DB look ups, etc....but I wont go into any of that here and will try to keep it close to what was originally posted a few years ago.

This solution should work well in many scenarios, but as always may need to be tweaked for some types of weapons or games.
In tests of over 50 bolt entities over a LAN everything works perfectly and looks great, so its a good starting base if you want to achieve the slower moving networked projectiles.

So getting into the details.....


Set-up a bool input on the player command for firing the weapon called fireSelectedWeapon.
And another bool called fireSelectedWeaponAction.

Create an event called PlayerFireProjectile.
Set the global senders to server only, and entity senders to none.
Set the following entity values (name / type)
origin / Vector
rotation / Quaternion
direction / Vector
frame / Integer
entity / Entity

You may also like to add some ID numbers to identify what the type of projectile is, or the prefab, or something similar.
The AI projectile event is very similar, which has the same values, but different IDs to support the different AI weapon types, and has the global senders to server only.

For the movement side of things, you need to set-up the commands like so. The event listener can be entity or global (I think) as it not needed for this at all.
public class PlayerMovementController : Bolt.EntityEventListener<IPlayerState>
{
	// normal movement variables
	bool _fireSelectedWeapon;
	bool _fireSelectedWeaponAction;
	
	void PollKeys()
	{
		// other key polls
		_fireSelectedWeapon = Input.GetKey("Fire Weapon");
	}
	
	public override void SimulateController()
	{
		PollKeys();
		IPlayerCommandInput input = PlayerCommand.Create();

		//other movement commands
         	
		 if ((this._fireSelectedWeapon == true) && (this.CanFire() == true))
		 {
		    input.fireSelectedWeaponAction = true;
		 }
		 else
		 {
		    input.fireSelectedWeaponAction = false;
		 }
		 
		 entity.QueueInput(input);
	}
	
	public override void ExecuteCommand(Bolt.Command cmd, bool resetState)
	{
		PlayerShipCommand playerCmd = (PlayerShipCommand)cmd;

		if (resetState)
		{
			//reset state code
		}
		else
		{
			//movement code
			
			if (playerCmd.IsFirstExecution)
			{
				if (playerCmd.Input.fireSelectedWeaponAction)
				{
					this.FireWeapon(playerCmd);
				}
			}
		}
	}
	
	void FireWeapon(PlayerShipCommand cmd)
	{
		if (BoltNetwork.isServer == true)
		{
			var ev = PlayerFireProjectile.Create();
			
			ev.entity = entity;
			ev.origin = this.transform.position;
               		ev.rotation = this.transform.rotation;
               		ev.direction = // the the velocity from your player motor to add the players velocity to the projectile, if you want to do this
               		ev.frame = cmd.ServerFrame;
               		ev.Send();
		}
	}
	
	private bool CanFire()
	{
		return true; // if the weapon can be fired based on cooldowns, energy, etc...
	}
}

Next up you need something to listen to the events and create the projectiles.
I have one of these on each player game object.
For the AI there is just an overall controller class sitting on a game object in the scene.
This is actually split over several classes, but I've condensed it into one below to show the fire frame bit easier.
public class PlayerWeaponsController : Bolt.GlobalEventListener
{
	private BoltEntity _entity;

	public int fireFrame
	{
		get;
		set;
	}

	public override void OnEvent(PlayerFireProjectile evnt)
	{
		if(evnt.entity == _entity)
			this.FireEvent(evnt);
	}
	
	private void FireEvent(PlayerFireProjectile evnt)
	{
		this.FireWeapon(evnt.weaponsGroup, evnt.mountPointID, evnt.frame, evnt.origin, evnt.rotation, evnt.direction);
	}
	
	private void FireWeapon(int frame, Vector3 position, Quaternion rotation, Vector3 direction)
	{
		//Set the weapon cool down fame
         	this.fireFrame = frame;
         	
         	// Get the projectile from an object pool
         	GameObject obj = ObjectPool.instance.GetObjectForType(_ammoPrefabName);
         	
		obj.GetComponent<AmmoBase>().Init(frame, position, rotation, direction);
		obj.GetComponent<AmmoBase>().IgnoreHitbox(transform.gameObject.GetComponents<BoltHitbox>());

		obj.SetActive(true);
	}
}

Comments

  • Now onto the juicy bit, the projectile code itself.
    I check for network collisions via bolt hitboxes and non network based ones for static objects that use mesh colliders. There is probably a better way to do this, but it seems to work fine.
    public class AmmoBase : MonoBehaviour
    {
    	private int spawnFrame;
    	private int killFrame;
    	private int currentFrame;
    	private Vector3 origin;
    	private Quaternion rotation;
    	private Vector3 velocity;
    	private HashSet<BoltHitbox> ignoreHitbox;
    	private int frameLifetime;
    	
    	void Awake()
    	{
    		// Setup the projectile
    		this.ignoreHitbox = new HashSet<BoltHitbox>();
    	}
    	
    	void OnEnable()
    	{
    		this.IgnoreHitbox(gameObject.GetComponents<BoltHitbox>());
    	}
    
    	void OnDisable()
    	{
    		this.ignoreHitbox.Clear();
    	}
    	
    	public void Init(int spawnFrame, Vector3 origin, Quaternion rotation, Vector3 direction)
    	{
    		this.origin = origin;
    		this.rotation = rotation;
    		transform.position = this.origin;
    		transform.rotation = this.rotation;
    		this.velocity = (this.transform.forward * ammoData.moveSpeed) + direction;
    		this.spawnFrame = spawnFrame;
    		this.currentFrame = spawnFrame;
    
    
    		// Set the kill frame
    		killFrame = spawnFrame + TimerHelper.instance.GetFrameCount(ammoData.lifeTime);
    	}
    	
    	public void IgnoreHitbox(BoltHitbox[] hitbox)
    	{
    		for (int i = 0; i < hitbox.Length; i++)
    		{
    			this.ignoreHitbox.Add(hitbox[i]);
    		}
    	}
    	
    	private void OnHit(GameObject go)
    	{
    		// Do damage stuff
    		// Send projectile back to pool
    	}
    	
    	// Check for collisions via 2 methods
    	// 1 - Bolt Network collisions via bolt hit boxes
    	// 2 - Non network collisions (static non moving objects, i.e. mesh collider)
    	private void ResolveCollisions(int frame, float frameDeltaTime)
    	{
    	 	Vector3 origin = this.GetPositionAtFrame(frame);
    	 	float distance = (this.velocity * frameDeltaTime).magnitude;
    
    	 	Ray ray = new Ray(origin, this.velocity.normalized);
    
    	 	// 1 - Bolt Network collision
    	 	BoltPhysicsHits hits = BoltNetwork.RaycastAll(ray, frame);
    
    	 	for (int i = 0; i < hits.count; i++)
    	 	{
    	    		BoltPhysicsHit hit = hits.GetHit(i);
    	    		if (this.ignoreHitbox.Contains(hit.hitbox) == false)
    	    		{
    	       			if (hit.distance < distance)
    	       			{
    		  			Debug.Log(string.Format("[Bolt Network HIT] Target={0}, Distance={1}, HitArea={2}", hit.body.gameObject.transform.parent.gameObject.name, hit.distance, hit.hitbox.hitboxType));
    		  			this.OnHit(hit.body.gameObject.transform.parent.gameObject);
    	       			}
    	    		}
    	 	}
    
    		// 2 - Non network collision (only process if bolt hits hit nothing)
    		if (hits.count == 0)
    		{
    			RaycastHit rayHit;
    			if (Physics.Raycast(ray, out rayHit))
    			{
    				if (rayHit.distance < distance)
    				{
    				  	Debug.Log(string.Format("[Non Network HIT] Target={0}, Distance={1}", rayHit.transform.gameObject.name, rayHit.distance));
    				  	this.OnHit(rayHit.transform.gameObject);
    				}
    			}
    		}
    	}
    	
    	private Vector3 GetPositionAtFrame(int frame)
    	{
    		int totalDelta = frame - this.spawnFrame;
    		return this.origin + (this.velocity * BoltNetwork.frameDeltaTime * totalDelta);
    	}
    	
    	void FixedUpdate()
    	{
    		int serverFrame = BoltNetwork.serverFrame;
    		if (serverFrame > this.killFrame)
    		{
    			gameObject.SetActive(false);
    		}
    
    
    		for (; this.currentFrame < serverFrame; this.currentFrame++)
    		{
    			this.ResolveCollisions(this.currentFrame, BoltNetwork.frameDeltaTime);
    		}
    
    		this.transform.position = this.GetPositionAtFrame(BoltNetwork.serverFrame);
    	}
    }


    The TimerHelper mentioned above just calculates the number of frames per second.
    Which can probably be done now via BoltNetwork.framesPerSecond.
    I.e. frameCount = duration * BoltNetwork.framesPerSecond;


    And thats pretty much it.
    There is probably a mistake or two in there as I've condensed code and copy / pasted, but if you get stuck I'll try to help.
    I hope it helps you work out how to handle network projectiles, and big thanks to the guys who originally devised how to do this :)
  • Thanks for sharing this, hvault. It's been a huge help to me as well.

    I've got one question for people who are using a similar methods or who are more experienced with networking games.

    How are people handling despawning these kinds of projectiles on the client side?

    Do you do the collision detection against moving networked objects on the client as well or handle it on the server only and then send events from the server providing the clients with the info about the hit position, the effects and whether the projectile needs to be despawned etc?

    I'm wondering if Bolt Raycasts provide the exact same result on all the clients as well as the server when doing projectiles this way and a collision happens with relatively fast moving player?

    I would like to prevent cases where projectiles get despawned on a client but are really still moving on the server and hitting another player - at least if it happens relatively often. Then on the other hand, if you just send events to despawn the projectiles on clients (in the case of collisions with moving objects), the faster moving projectiles might end up flying past the character before getting despawned. Collisions with the static environment should be safe to do on the client side in any case - so the projectiles never fly past walls at least.
  • I know this post is quite old but really interesting!
    I was wondering if someone could help me with an issue.

    I try to instanciate a prefab with the method FireEvent()
    When I play in the editor (with Play as Server), I can see the projectiles.

    But when I play with one or more clients I don't see the projectile anymore (not even on the server).
    When I modify the health (as a test), I can see that the event is received by everyone.

    How come the Instanciated prefab doesn't spawn? (I don't use pooling for now).

    Thanks!
  • If you send your project to support@boltengine.com we can take a look. You can also look at the tutorial, the bazooka works pretty much like that but doesn't use events.
  • Thanks Stanchion,

    I usually try to find by myself the issues, I don't like bothering pple for mistakes or similar things (even if I do right now).

    So, I was able to localize the issue once I got it working, but the issue appears only when the player is moving, the projectile is actually not spawned (I use a rigidbody). But when I don't move, I don't have any issue.

    The server always spawns the projectile though. it's only the client that doesn't, But!

    In Both situation, FireEvent Debug Log is executed
    When I don't move on the client, the log in #1 is executed.
    When I run, the log in // #2 is executed.
    I also have a DebugLog to tell when the projectile is spawned (OnStart), and the Log doesn't show up when I try to spawn the projectile while moving the character on the client.

    Here is the script part where I spawn the RigidBody (I use an event to spawn it, similar to the scripts above):

    public override void OnEvent(PlayerFireProjectile evnt)
    {
    FireEvent(evnt);
    inc += 1;
    }

    private void FireEvent(PlayerFireProjectile evnt)
    {
    Debug.LogWarning("Firing Event! No: " + inc);
    Rigidbody go = Instantiate(throwItem, evnt.origin + Vector3.up * 1.5f + evnt.direction * 1.25f, evnt.localRotation) as Rigidbody;
    go.velocity = evnt.transformDirection;
    StartCoroutine( DebugPos(go));
    }

    private IEnumerator DebugPos(Rigidbody go)
    {
    yield return new WaitForSeconds(0.2f);

    if (go != null)
    {
    Debug.LogWarning("go.transform.position : " + go.transform.position); // #1
    StartCoroutine(DebugPos(go));
    yield return null;
    }
    else {
    Debug.LogWarning("Object has been destroyed ! Cancel Log"); // #2
    yield return null;
    }
    }
  • Hey laurel

    You mention it only happens when you are moving. Does the problem occur when you are moving in all directions, including backwards ?

    It might be worth while disabling all the scripts / components on the projectile and see if one of them is causing the issue as I noticed that inside FireEvent there is nothing to ignore the hit boxes from the spawner.
    It could be that the projectile is spawning then being destroyed straight away.
  • laurel
    laurel
    edited July 2017
    Hi hvault,

    I was not using hitboxes because I didn't want to use raycast. But you're right, the object was destroyed on spawn. And I'm not sure why, but I'm pretty sure is was not happening when I was instantiating the projectile.

    Anyway (shame on me), and while I was waiting for an answer :), I decided to implement a pool system (the one from the Unity video). And with the pooling system, the arrow was disabled at spawn.

    So I've added a check to ignore the player who initiates the launch. Everything seems to work fine now.
    Thanks for your help.

    EDIT:
    I tried it a bit more and sometimes I still have missing input on the client but not on the server. I don't think it can be related to the lag (I use the default values), but when I trigger the projectile, it doesn't show up on the client, but shows up on the server (which is way less than before). I'll double check my script.

    Btw, I didn't use your implementation of FixedUpdate. Doing that was slowing down a lot my FPS.
    Not sure exactly what was the issue and If I actually need it. Could you explain me why you had it?
  • FixedUpdate is used because of the use of Rigidbody on the GameObjects firing the projectiles and blowing up :smile:
    Not sure why you were seeing and FPS reduction though, as I thought it ran less often the Update.

    For the projectiles still missing on the clients, maybe check the bolt console (set it to output to a file) and see if the events are reaching it ok. Perhaps align them with the entries from the server log file by the frame value so you can see which ones are missing if you are firing lots of projectiles.
  • Hey Hvault, just to let you know I fixed the issue. Thanks for your help.
  • Hello everyone! Thanks for the suggested solution, it works. But before I can continue with my game I would like to know, is this decision still valid in 2021? Or are there better solutions for slower projectiles?