Optimizing Multiplayer Shooting Games with Unity's PUN (Section 3)
Table of Contents
I. Preparation
II. Projectile Instantiation Script
III. Fire and Damage Synchronization
Adding a rocket launcher alongside the grenade seems reasonable. The GIF animation shows single-player and multiplayer results.
Adding the rocket launcher follows similar principles to grenades. I noticed that the rocket launcher model already included explosion script assets. Implementing grenade synchronization made rocket launcher development easier.
I. Preparation
Locate the rocket launcher model named Rocket_Launcher_01 in your assets.
The projectile model is named Rocket_Launcher_01_Projectile. Add collider and rigidbody components, and set both projectile and explosion effect tags to "Boom" for damage detection, sharing the same logic as grenades.
Attach the rocket launcher to the player Player model using weapon component activation for weapon switching.
This is individual weapon switching - for multiple weapons, store them in an array and use indices for selection.
II. Projectile Instantiation Script
I extracted the grenade script into handGrenade, placing handheld weapons in the same script for organization.
public class handGrenade : MonoBehaviour
{
//Grenade
public GameObject handGrenadePrefab;
//Grenade spawn point
public Transform handGrenadeTf;
void Start()
{
}
public void AttGrenade()
{
//Instantiate a grenade
GameObject handGrenadeObj = Instantiate(handGrenadePrefab,handGrenadeTf.transform.position,handGrenadeTf.transform.rotation);
handGrenadeObj.GetComponent<Rigidbody>().AddForce(transform.forward * 1,ForceMode.Impulse); //Throw velocity Adjust camera offset for center point alignment
}
Gun Script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Weapon script
public class Gun : MonoBehaviour
{
public int BulletCount = 15;
public GameObject bulletPrefab;
public GameObject casingPreafab;
public Transform bulletTf;
public Transform casingTf;
//Rocket launcher ammo count
public int RocketBulletCount = 1;
//Projectile prefab
public GameObject rocketBulletPrefab;
//Rocket launcher firing point
public Transform RocketBulletTf;
void Start()
{
}
public void Attack()
{
//Instantiate a bullet
GameObject bulletObj = Instantiate(bulletPrefab);
bulletObj.transform.position = bulletTf.transform.position;
bulletObj.GetComponent<Rigidbody>().AddForce(transform.forward * 500, ForceMode.Impulse); //Bullet speed Adjust camera offset for muzzle position
GameObject casingObj = Instantiate(casingPreafab);
casingObj.transform.position = casingTf.transform.position;
}
public void RocketAttack()
{
//Instantiate a rocket projectile
GameObject bulletObj = Instantiate(rocketBulletPrefab);
bulletObj.transform.position = RocketBulletTf.transform.position;
bulletObj.GetComponent<Rigidbody>().AddForce(transform.forward * 300, ForceMode.Impulse); //Projectile speed Adjust camera offset for muzzle position
}
}
III. Fire and Damage Synchronization
In the PlayerController script we need to:
- Implement weapon switching
- Synchronize projectile instantiation
- Implement damage synchronization
- Reload mechanics (for both gun and rocket launcher)
Weapon switching animation uses reload animation for now.
Some code repeats previous sections, but rocket launcher uses these functions too.
Damage, score, and explosion radius (adjust explosion effect scale) can be customized. Both rocket launcher and grenades use the same damage detection and scoring logic - explosion-based damage.
//Character controller
public class PlayerController : MonoBehaviourPun,IPunObservable
{
public Gun gun; //Weapon script
public handGrenade HandGrenade; //Grenade script
//Rocket launcher and gun prefabs
public GameObject RocketObject;
public GameObject GunObject;
//Target variable for weapon switching, 1 for gun 0 for rocket launcher
int RocketFlag = 1;
void Start()
{
gun = GetComponentInChildren();
HandGrenade = GetComponentInChildren();
}
void Update()
{
//Debug.Log(photonView.Owner.NickName);
//Check if this is local player only
if (photonView.IsMine)
{
if (isDie == true)
{
return;
}
UpdatePosition();
UpdateRotation();
InputCtl();
}
else
{
UpdateLogic();
}
}
//Player controls
public void InputCtl()
{
switch (RocketFlag)
{
case 0:
//Rocket launcher
if (Input.GetMouseButtonDown(0))
{
//Check ammo count
if (gun.RocketBulletCount > 0)
{
//Cannot shoot during reload
if (ani.GetCurrentAnimatorStateInfo(1).IsName("Reload"))
{
return;
}
RocketShell.SetActive(false);
gun.RocketBulletCount--;
Game.uiManager.GetUI("FightUI").UpdateBulletCount(gun.RocketBulletCount);
//Play fire animation
ani.Play("Fire", 1, 0);
StopAllCoroutines();
StartCoroutine(RocketAttack());
}
}
//Rocket launcher
if (Input.GetKeyDown(KeyCode.R))
{
//Reload ammo
AudioSource.PlayClipAtPoint(reloadClip, transform.position); //Play reload sound
ani.Play("Reload");
RocketShell.SetActive(true);
gun.RocketBulletCount = 1;
Game.uiManager.GetUI("FightUI").UpdateBulletCount(gun.RocketBulletCount);
}
break;
case 1:
if (Input.GetMouseButtonDown(0))
{
//Check ammo count
if (gun.BulletCount > 0)
{
//Cannot shoot during reload
if (ani.GetCurrentAnimatorStateInfo(1).IsName("Reload"))
{
return;
}
gun.BulletCount--;
Game.uiManager.GetUI("FightUI").UpdateBulletCount(gun.BulletCount);
//Play fire animation
ani.Play("Fire", 1, 0);
StopAllCoroutines();
StartCoroutine(AttackCo());
}
}
if (Input.GetKeyDown(KeyCode.R))
{
//Reload ammo
AudioSource.PlayClipAtPoint(reloadClip, transform.position); //Play reload sound
ani.Play("Reload");
gun.BulletCount = 15;
Game.uiManager.GetUI("FightUI").UpdateBulletCount(gun.BulletCount);
}
break;
default:
Debug.Log("------------error");
break;
}
//Press ESC to exit game
// if (Input.GetKeyDown(KeyCode.Escape))
// {
// Application.Quit();
// }
//Hold Tab to view scoreboard
if (Input.GetKey(KeyCode.Tab))
{
Game.uiManager.ShowUI("ScoreboardUI");
Game.uiManager.ShowUI("ScoreboardUI").UpDateScore();
}
else if(Input.GetKeyUp(KeyCode.Tab))
{
Game.uiManager.CloseUI("ScoreboardUI");
}
if (Input.GetKeyDown(KeyCode.Q))
{
if (boolHandGrenade == false)
{
boolHandGrenade = true;
//Throw every 5 seconds
Invoke("boolThrowHandGrenade", 5f);
//Play grenade throw animation
ani.Play("Grenade_Throw");
StopAllCoroutines();
StartCoroutine(AttHandGrenade());
}
}
//Press E to switch between gun and rocket launcher
if (Input.GetKeyDown(KeyCode.E))
{
ani.Play("Reload");
if (RocketFlag == 1)
{
RocketObject.SetActive(true);
GunObject.SetActive(false);
Game.uiManager.GetUI("FightUI").UpdateBulletCount(gun.RocketBulletCount);
RocketFlag = 0;
}
else
{
RocketObject.SetActive(false);
GunObject.SetActive(true);
Game.uiManager.GetUI("FightUI").UpdateBulletCount(gun.BulletCount);
RocketFlag = 1;
}
}
}
//Rocket launcher attack coroutine
IEnumerator RocketAttack()
{
//Delay 0.1 seconds before firing
yield return new WaitForSeconds(0.1f);
//Play shooting sound
AudioSource.PlayClipAtPoint(shootClip, transform.position);
photonView.RPC("AttackRocketRpc", RpcTarget.All); //All players execute AttackRpc function
}
//Execute rocket launcher sync
[PunRPC]
public void AttackRocketRpc()
{
gun.RocketAttack();
}
//Collision detection: Only detect collision moment —— Shared by rocket launcher and grenades
private void OnCollisionEnter(Collision collision) //Parameter collision contains collision information
{
//Set ground layer to "Ground" collision.collider.tag == "Ground" Check if object is on ground
if (collision.collider.tag == "Boom")
{
GetHit(localPlayer,3,2);
}
}
//Synchronize all character damage: p local player, addScore score gained, AttackHp different weapons have different damage values
public void GetHit(Player p,int addScore,int AttackHp)
{
if (isDie == true)
{
return;
}
switch (AttackHp)
{
case 1:
//Synchronize all character damage: Gun damage
photonView.RPC("GetGunHitRPC", RpcTarget.All);
break;
case 2:
//Synchronize all character damage: Explosion damage —— Shared by rocket launcher and grenades
photonView.RPC("GetBoomHitRPC", RpcTarget.All);
break;
default:
Debug.Log("------------ERROR");
break;
}
Score += addScore;
p.SetScore(Score);
}
}