Black White Red
Black White Red (BWR) is a first-person, action-based, 5v5 PvP game. Get across the map to capture enemy portals in order to score points, all while your abilities change as you traverse to different sections of the map. Defend, fight, evade, and capture.
Product Owner & Systems Programmer
Team Size: 8
Scale: Tech Demo
Development Period: June–August 2022
Tools Used:
C#
Unity
Git
Features
Contributions
Development of Black White Red
BWR was developed by a small team of 8 over the Summer of 2022. Based on a mini-game originally made in Minecraft, we wanted to make this game come to life and see how well it would work as a standalone. It was my first foray into a game project involving network programming, and turned out to be a very fulfilling experience.
Technical information
Archery
// ArrowController.cs Initialization
[ClientRpc(Delivery = RpcDelivery.Reliable)]
public void InitClientRpc(Team _team, ulong _shooterId, int _shadeValue, Vector3 startingPosition, Vector3 startDirection, float amountCharged, float timeShot)
{
Debug.Log("Arrow Inited");
Vector3 startingVelocity = startDirection * Mathf.Lerp(minimumStartingVelocity, maximumStartingVelocity, amountCharged);
float timeSinceShot = NetworkManager.Singleton.LocalTime.TimeAsFloat - timeShot;
gameObject.SetActive(true);
rb.constraints = RigidbodyConstraints.None;
team = _team;
shadeValue = _shadeValue;
shooterId = _shooterId;
transform.position = startingPosition + startingVelocity * timeSinceShot + .5f * Physics.gravity * timeSinceShot * timeSinceShot;
rb.velocity = startingVelocity + Physics.gravity * timeSinceShot;
timer = timeBeforeDespawn;
appearance.forward = rb.velocity;
landed = false;
//When defaultly enabled in NGO, the collider would accidentally act as not a trigger, causing "flying" glitch caught by the testers
sphereCollider.enabled = true;
if (IsServer || IsHost)
{
RaycastHit temp;
Ray ray = new Ray(startingPosition, transform.position);
RaycastHit[] raycastHits = Physics.SphereCastAll(ray, .25f, Vector3.Distance(startingPosition, transform.position), collisionDetectionMask);
for(int i = 0; i < raycastHits.Length; i++)
{
temp = raycastHits[i];
Debug.LogWarning("SphereCastAll index "+ i +": "+ temp.collider.name +" "+temp.distance);
if (
temp.collider.gameObject.layer != 6 || //Isn't a player
temp.collider.gameObject.GetComponent<PlayerController>().CurrentTeam != team //Is on the enemy team
)
{
Debug.LogWarning("PROCESSING SphereCastAll index " + i + ": " + temp.collider.name + " " + temp.distance);
ProcessCollision(temp.collider);
break;
}
}
}
}
// ArrowController.cs Processing Collision
private void ProcessCollision(Collider hit)
{
//Handle ground, bomb, or enemy teammate
switch (hit.gameObject.layer)
{
//Ground
case 0:
Debug.Log("Arrow hit ground " + hit.name);
StickIntoPlaceClientRpc(hit.ClosestPoint(transform.position));
timer = timeBeforeDespawnOnceLanded;
landed = true;
sphereCollider.enabled = false;
break;
//Players
case 6:
Debug.Log("Arrow hit player " + hit.name);
PlayerController playerController = hit.GetComponent<PlayerController>();
if (playerController.CurrentTeam != team)
{
//Knockback
playerController.GetComponent<PlayerKnockbackController>().KnockbackPlayer(rb.velocity, kit.playerStats[shadeValue].bowKnockbackMultiplier);
//Damage
playerController.GetComponent<PlayerHealth>().TakeDamage(
rb.velocity.magnitude * damageMultiplier * kit.playerStats[shadeValue].bowDamageMultiplier,
DamageSource.ARROW,
NetworkManager.Singleton.SpawnManager.SpawnedObjects[shooterId].GetComponent<PlayerController>());
//Unload
sphereCollider.enabled = false;
ArrowPool.Instance.UnloadArrow(gameObject);
}
break;
//Bomb
case 9:
sphereCollider.enabled = false;
ArrowPool.Instance.UnloadArrow(gameObject);
break;
default:
break;
}
}
Music Reacting to the Local Player's Shade
// MusicShadeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MusicShadeController : MonoBehaviour
{
public FMODUnity.EventReference musicEventName;
public FMOD.Studio.EventInstance musicEvent;
[Space]
[SerializeField]
[Tooltip("Parameter name for the shade/layer value")]
string param;
[SerializeField]
int lastSetValue;
[SerializeField]
PlayerController localPlayer;
void Start()
{
musicEvent = FMODUnity.RuntimeManager.CreateInstance(musicEventName);
}
private void OnEnable()
{
MatchManager.onMatchStart += HandleMatchStart;
ListenToPlayer();
}
private void OnDisable()
{
MatchManager.onMatchStart -= HandleMatchStart;
StopListeningToPlayer();
}
private void HandleShadeChange(PlayerStats _value)
{
musicEvent.setParameterByName(param, localPlayer.ShadeValue);
lastSetValue = localPlayer.ShadeValue;
}
private void ListenToPlayer()
{
if (localPlayer != null)
{
localPlayer.onShadeChange += HandleShadeChange;
}
}
private void StopListeningToPlayer()
{
if(localPlayer != null)
{
localPlayer.onShadeChange -= HandleShadeChange;
}
}
private void HandleMatchStart()
{
localPlayer = MatchManager.Instance.localPlayerController;
musicEvent.setParameterByName(param, localPlayer.ShadeValue);
lastSetValue = localPlayer.ShadeValue;
musicEvent.start();
ListenToPlayer();
}
private void OnDestroy()
{
musicEvent.stop(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
}
}