Scripting Sample: Gamify Inc

As part of my work for Gamify Inc, I prototyped various types of gameplay in Unity using C#.

For rapid iteration prototyping of different gameplay features and minigames, I wrote a set of modular scripts that could be combined in different ways to re-use particular mechanics. With permission from Gamify Inc, this is a screenshot of one such script. It also shows tabs of other modular scripts that I wrote.

This is a working patrol drone prototype using my scripts.

 

Over the course of creating 3D gameplay prototypes for measuring brain function, I also conceived of a way of proceduralizing variations of an entire style of such 3D gameplay.

With permission from Gamify Inc, here is one of the C# scripts that I wrote in order to create this procedural content.

 

</pre>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

// Used by procedural obstacle scripts (security cameras, drones, etc) to define which rooms get obstacles.
// Pure randomness creates bad content. Instead, these are different styles of procedural camera & drone placement to choose from.
public enum E_PROCEDURAL_OBSTACLE_PLACEMENT
{
IN_PICKUP_ROOMS,
IN_OPEN_ROOMS,
IN_CLOSED_ROOMS,
}

// Used by procedural obstacle scripts (security cameras, drones, etc) to define obstacles get placed.
// Procedural rooms only allow one obstacle in each room, so this helps determine which obstacle spawns across the list of rooms.
// Pure randomness creates bad content. Instead, these are different styles of procedural camera & drone placement to choose from.
public enum E_PROCEDURAL_OBSTACLE_TYPE
{
CAMERAS_ONLY,
DRONES_PREFERRED,
CAMERAS_THEN_DRONES, // Placing cameras first usually means there are less rooms left for drones, though not always.
DRONES_THEN_CAMERAS, // Placing drones first usually means there are less rooms left for cameras, though not always.
NO_OBSTACLES, // This is only procedurally chosen when no obstacles are enabled.
}

// Used by procedural scripts to define what gives out pickups.
// Pure randomness creates bad content. Instead, these are different styles of procedural placement to choose from.
public enum E_PROCEDURAL_OBJECTIVE_TYPE
{
PICKUPS_ONLY,
DRONES_PREFERRED, // In case the system can't place a drone (1 per room limit), it'll still default to placing Pickups
PICKUPS_DRONES_SEPARATE_CHAINS, // Pickups and drones are on separate locked-door chains.
PICKUPS_DRONES_MIX, // Pickups and drones are randomly mixed together.
}

public class ProceduralScavManager : MonoBehaviour {

[Header("OBJECTIVE SETTINGS")]
[Tooltip("Number of evidence pickups needed to complete the scavenger gameflow. The procedural system will spawn this number of Bio pickups.")]
public int numEvidence = 1;
[Tooltip("Set this to the pickup that you're supposed to return to the safehouse with. Usually, this should be the Bio pickup.")]
public Item finalPickupType;

[Header("OTHER SETTINGS")]
[Tooltip("Set this to the ProceduralRoom that the player starts the scene in.")]
public ProceduralRoom startingRoom;
[Header("ProceduralScav_LockedDoors has more lock settings")]
[Tooltip("If true, use locked doors with key pickups. The only reason to set this to false is if locks haven't been unlocked/tutorialized yet on the crit path.")]
public bool useLockedDoors = true;
[Tooltip("If true, use drones that drop pickups (final objective & keys) after a proximity hack. The only reason to set this to false is if drones haven't been unlocked/tutorialized yet on the crit path.")]
public bool useProximityDrones = true;
[Tooltip("Whether the procedural system will sometimes decide to use security cameras. The only reason to set this to false is if security cameras haven't been unlocked/tutorialized yet on the crit path.")]
public bool useSecurityCameras = true;
[Tooltip("Whether the procedural system will sometimes decide to use security drones. The only reason to set this to false is if security drones haven't been unlocked/tutorialized yet on the crit path.")]
public bool useSecurityDrones = true;

// Hidden in inspector because these settings should never change.
// Overrides are manually referenced because they start disabled.
//[Header("Override objects for closing/opening doors that don't use keys.")]
[HideInInspector]
public DoorOverride openDoorOverride;
[HideInInspector]
public DoorOverride closeDoorOverride;

// Script with functions for locked door & key placement. Defined on Awake.
[HideInInspector]
public ProceduralScav_LockedDoors lockedDoorManager;
// Script with functions for controlling & selecting security cameras. Defined on Awake.
[HideInInspector]
public ProceduralScav_SecurityCams procedCameraManager;
// Script with functions for controlling & selecting security drones. Defined on Awake.
[HideInInspector]
public ProceduralScav_SecurityDrones procedSecDroneManager;
// Script with functions for controlling & selecting proxmity drones that drop pickup items. Defined on Awake.
[HideInInspector]
public ProceduralScav_ObjectiveDrones procedObjectiveDroneManager;
// Initializes to a list of procedural rooms in this scene.
[HideInInspector]
public ProceduralRoom[] proceduralRooms;
// Tracks which rooms have unblocked (open doors or no doors) access to the starting room. This is dynamically updated as doors get locked.
[HideInInspector]
public List<ProceduralRoom> roomsOpenToStartingRoom;
// Tracks how pickups are dropped when procedural scavenger is initialized. This value is procedurally determined, and not manually set.
[HideInInspector]
public E_PROCEDURAL_OBJECTIVE_TYPE procedDropType;
// Tracks which obstacles should be placed when procedural scavenger is initialized. This value is procedurally determined, and not manually set.
[HideInInspector]
public E_PROCEDURAL_OBSTACLE_TYPE procedObstacleType;

// Tracks which rooms the final evidence objectives have spawned in
[HideInInspector]
public List<ProceduralRoom> evidenceRooms = new List<ProceduralRoom>();

// Initializes to a list of all doors in this scene.
private DoorManager[] procedDoors;

// True when this script is in the process of initializing locked doors. Functions check this to make sure they only continue when locked doors have finished initializing.
private bool isDoorsInitializing = false;
// True when this script is in the process of initializing security cameras.
private bool isSecurityCamerasInitializing = false;
// True when this script is in the process of initializing security drones.
private bool isSecurityDronesInitializing = false;

// Used for functions that search through the rooms for something, to track when they've already searched a room.
private List<ProceduralRoom> tempCheckedRooms;
// Used for tracking whether a locked-door chain is using pickups or proximimty drones.
private int chainSeed = 0;

// Return a random enum from enum set T
// E.g. GetRandomEnum<E_PROCEDURAL_OBSTACLE_PLACEMENT>();
public T GetRandomEnum<T>()
{
System.Array A = System.Enum.GetValues(typeof(T));
T V = (T)A.GetValue(UnityEngine.Random.Range(0, A.Length));
return V;
}

// Returns a list of room with unblocked access (open doors or no doors) to the starting room.
// If inaccessibleRoom is set, any room that requires going through that room is considered blocked.
public void UpdateRoomsOpenToStartingRoom(ProceduralRoom blockedRoom)
{
// First, clear the old list.
roomsOpenToStartingRoom = new List<ProceduralRoom>();
roomsOpenToStartingRoom.Add(startingRoom);

tempCheckedRooms = new List<ProceduralRoom>();
tempCheckedRooms.Add(startingRoom);
if (blockedRoom)
tempCheckedRooms.Add(blockedRoom);

// Rebuild the list of rooms that have unblocked access to the starting room.
AddOpenAdjacentConnectionsRecursive(startingRoom);

}

// Returns a list of room with unblocked access (open doors or no doors) to the starting room.
public void UpdateRoomsOpenToStartingRoom()
{
UpdateRoomsOpenToStartingRoom(null);
}

// Open targetDoor. This also removes any key requirement from the door.
public void SetRoomDoorOpen(DoorManager targetDoor)
{
openDoorOverride.door = targetDoor;
openDoorOverride.requiredKey = null;
openDoorOverride.enabled = true;
}

// Close targetDoor, requiring key to open.
// If key is null, then the door can't be opened.
public void SetRoomDoorClose(DoorManager targetDoor)
{
closeDoorOverride.door = targetDoor;
closeDoorOverride.requiredKey = null;
closeDoorOverride.enabled = true;
}

// returns true if the room is valid to put a pickup objective in
// - If the room hasn't already spawned objectives of any type.
// - If the room hasn't already spawned pickups
// - If the room has any pickups to spawn
// - The room is not the starting room
public bool IsValidForPickupObjective(ProceduralRoom room)
{
return (!room.initializedObjectives && !room.initializedPickups && (room.pickupsList.Count > 0) && (room != startingRoom));
}

// Returns true if the current initialized procedural scavenger setting allows for drone objectives
public bool IsProceduralDroneObjectiveAllowed()
{
return ((procedDropType == E_PROCEDURAL_OBJECTIVE_TYPE.DRONES_PREFERRED) || (procedDropType == E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_MIX) || (procedDropType == E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_SEPARATE_CHAINS));
}

// Returns true if the current initialized procedural scavenger setting allows for pickup objectives
public bool IsProceduralPickupObjectiveAllowed()
{
return ((procedDropType == E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_ONLY) || (procedDropType == E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_MIX) || (procedDropType == E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_SEPARATE_CHAINS));
}

// Returns the objective type that the current chain of locked doors is supposed to use.
// 0 = pickups
// 1 = proximity drones
public int GetChainObjectiveType()
{
return chainSeed;
}

// Called when a chain finishes initializing, to increment to the objective type that should be used for the next chain of locked doors.
public void IncrementChainObjectiveType()
{
if (chainSeed >= 1)
chainSeed = 0;
else
chainSeed++;
}

// Adds to the List "roomsOpenToStartingRoom" any adjacent rooms that have an open connection to targetRoom (open door or no door)
// If inaccessibleRoom is set, any room that requires going through that room is considered blocked.
// Recurses through adjacent rooms to do the same.
private void AddOpenAdjacentConnectionsRecursive(ProceduralRoom targetRoom)
{
foreach (ProceduralRoom room in targetRoom.adjacentRoomsList)
{
// Make sure we haven't already checked the room
// If blockedRoom isn't null, make sure this room isn't the blocked room.
if (!tempCheckedRooms.Contains(room) && !roomsOpenToStartingRoom.Contains(room))
{
tempCheckedRooms.Add(room);
if (room.HasOpenAdjacentConnection(targetRoom))
{
roomsOpenToStartingRoom.Add(room);
// Since "room" is connected, search its adjacent rooms to see if they're connected too.
AddOpenAdjacentConnectionsRecursive(room);
}

}
}
}

private void Awake()
{
if (startingRoom == null)
Debug.LogError("startingRoom in ProceduralScavManager has not been set.");

// Populate the array of procedural rooms in this scene
proceduralRooms = GameObject.FindObjectsOfType<ProceduralRoom>();

procedDoors = FindAllRoomDoors().ToArray();

// Set door overrides to disable themselves after each use. This allows them to be used repeatedly by ProceduralScavManager.
openDoorOverride.isSelfDisabling = true;
closeDoorOverride.isSelfDisabling = true;

procedCameraManager = GetComponent<ProceduralScav_SecurityCams>();
procedCameraManager.scavManager = this;
lockedDoorManager = GetComponent<ProceduralScav_LockedDoors>();
lockedDoorManager.scavManager = this;
procedSecDroneManager = GetComponent<ProceduralScav_SecurityDrones>();
procedSecDroneManager.scavManager = this;
procedObjectiveDroneManager = GetComponent<ProceduralScav_ObjectiveDrones>();
procedObjectiveDroneManager.scavManager = this;
}

private void Start()
{
InitializeProceduralScavenger();
}
// Sets up a new procedural scavenger
private void InitializeProceduralScavenger()
{
// Choose what gives out keys or final objective (evidence) pickups
InitializeRandomObjectiveTypes();
// Choose which types of obstacles to use for this procedural instance.
InitializeRandomObstacleTypes();

// Set all procedurally managed doors to start open.
SetAllRoomDoorsOpen();

// First chart out which rooms have open door access to the starting room. This list will be dynamically updated as rooms get locked off.
UpdateRoomsOpenToStartingRoom();

// Place the evidence pickups needed to complete the scavenger. This should always come before any other doors in the layout get locked.
for (int objNum = 0; objNum < numEvidence; objNum++)
{
InitializeFinalObjectiveLocation();
}

if (useLockedDoors)
{
// Track that we've started initializing locked doors.
isDoorsInitializing = true;

// Doors can't be opened and closed within the same frame.
// Since SetAllRoomDoorsOpen was called earlier, we need to wait a frame before locking doors.
StartCoroutine(InitializeLockedDoors());
}

switch (procedObstacleType)
{
case E_PROCEDURAL_OBSTACLE_TYPE.CAMERAS_ONLY:
{
isSecurityCamerasInitializing = true;
StartCoroutine(InitializeSecurityCameras());
break;
}
case E_PROCEDURAL_OBSTACLE_TYPE.DRONES_PREFERRED:
{
isSecurityDronesInitializing = true;
StartCoroutine(InitializeSecurityDrones());
break;
}
case E_PROCEDURAL_OBSTACLE_TYPE.CAMERAS_THEN_DRONES:
{
isSecurityCamerasInitializing = true;
isSecurityDronesInitializing = true;
StartCoroutine(InitializeSecurityCameras());
StartCoroutine(InitializeSecurityDronesAfterCameras());
break;
}
case E_PROCEDURAL_OBSTACLE_TYPE.DRONES_THEN_CAMERAS:
{
isSecurityCamerasInitializing = true;
isSecurityDronesInitializing = true;
StartCoroutine(InitializeSecurityDrones());
StartCoroutine(InitializeSecurityCamerasAfterDrones());
break;
}
}
}

// Return a list of all doors (opened or closed) attached to ProceduralRooms
// This list does not include loose DoorManager doors that are not referenced by a ProceduralRoom.
private List<DoorManager> FindAllRoomDoors()
{
List<DoorManager> doorsList = new List<DoorManager>();
foreach (ProceduralRoom room in proceduralRooms)
{
doorsList = doorsList.Union(room.doorsList).ToList();
}
return doorsList;
}

// Set all procedurally managed doors to start open.
private void SetAllRoomDoorsOpen()
{
foreach (DoorManager door in procedDoors)
{
SetRoomDoorOpen(door);
}
}

// Decide where the final objective is located. This should always be done first when initializing a new procedural scavenger.
private void InitializeFinalObjectiveLocation()
{
switch (procedDropType)
{
case E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_ONLY:
{
InitializeFinalObjectiveAsPickup();
break;
}
case E_PROCEDURAL_OBJECTIVE_TYPE.DRONES_PREFERRED:
{
procedObjectiveDroneManager.InitializeFinalObjectiveAsDrone();
break;
}
case E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_SEPARATE_CHAINS:
{
if (chainSeed == 0)
InitializeFinalObjectiveAsPickup();
else
procedObjectiveDroneManager.InitializeFinalObjectiveAsDrone();
break;
}
case E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_MIX:
{
if (Random.Range(0, 1) == 0)
InitializeFinalObjectiveAsPickup();
else
procedObjectiveDroneManager.InitializeFinalObjectiveAsDrone();
break;
}
}

}

private void InitializeFinalObjectiveAsPickup()
{
// Search for a list of candidate rooms to place the final objective at.
List<ProceduralRoom> candidateRooms = GetFinalObjectiveRoomPickupCandidates();

// Randomly choose a room from the candidates list.
ProceduralRoom finalObjectiveRoom = candidateRooms[Random.Range(0, candidateRooms.Count)];

// Tell the room to spawn a pickup as the final objective.
if (finalObjectiveRoom.EnablePickupOfType(finalPickupType))
{
// Track rooms where we've added an evidence pickup.
evidenceRooms.Add(finalObjectiveRoom);
}
}

// Search for a list of candidate rooms to place the final objective at.
// If possible, use preferred rooms first.
private List<ProceduralRoom> GetFinalObjectiveRoomPickupCandidates()
{
// first search for rooms where preferredForFinalObjective == true;
List<ProceduralRoom> candidateRooms = GetFinalObjectiveRoomPickupCandidates(true);

// If at least one preferred room was found, then return the list of preferred rooms.
if (candidateRooms.Count > 0)
return candidateRooms;
else
{
// Search for non-preferred rooms
candidateRooms = GetFinalObjectiveRoomPickupCandidates(false);

if (candidateRooms.Count == 0)
Debug.LogError("No valid room found to place final scavenger objective in.");

return candidateRooms;

}
}

// Search for a list of candidate rooms to place the final objective at.
// if limitToPreferred is true, then this search will only return rooms where "preferredForFinalObjective" is true
private List<ProceduralRoom> GetFinalObjectiveRoomPickupCandidates(bool limitToPreferred)
{
List<ProceduralRoom> candidateRooms = new List<ProceduralRoom>();

foreach (ProceduralRoom room in proceduralRooms)
{
// Check if this is a preferred room, or if limitToPreferred is false
if (room.preferredForFinalObjective || !limitToPreferred)
{
// Check to make sure this room is valid for the final objective
if (IsValidForPickupObjective(room))
candidateRooms.Add(room);
}
}

return candidateRooms;
}

// Sets up the sequence of locked doors and keys through the scavenger layout.
private IEnumerator InitializeLockedDoors()
{
// Doors can't be opened and closed within the same frame.
// Since SetAllRoomDoorsOpen was called earlier during initialization, we need to wait a frame before locking doors.
yield return new WaitForEndOfFrame();

lockedDoorManager.InitializeLockedDoors();

// Track that we've finished initializing locked doors.
isDoorsInitializing = false;
}

// Sets up security cameras
private IEnumerator InitializeSecurityCameras()
{
// Only proceed once locked doors have finished initializing.
yield return new WaitWhile(() => isDoorsInitializing);

procedCameraManager.InitializeSecurityCameras();
isSecurityCamerasInitializing = false;
}

// Sets up security cameras after drones finish initializing
private IEnumerator InitializeSecurityCamerasAfterDrones()
{
// Only proceed once locked doors have finished initializing.
yield return new WaitWhile(() => (isDoorsInitializing || isSecurityDronesInitializing));

procedCameraManager.InitializeSecurityCameras();
isSecurityCamerasInitializing = false;
}

// Sets up security drones
private IEnumerator InitializeSecurityDrones()
{
// Only proceed once locked doors have finished initializing.
yield return new WaitWhile(() => isDoorsInitializing);

procedSecDroneManager.InitializeSecurityDrones();
isSecurityDronesInitializing = false;
}

// Sets up security drones after cameras finish initializing
private IEnumerator InitializeSecurityDronesAfterCameras()
{
// Only proceed once locked doors have finished initializing.
yield return new WaitWhile(() => (isDoorsInitializing || isSecurityCamerasInitializing));

procedSecDroneManager.InitializeSecurityDrones();
isSecurityDronesInitializing = false;
}

// Initialize which obstacle types to use
private void InitializeRandomObstacleTypes()
{
if (useSecurityCameras && !useSecurityDrones)
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.CAMERAS_ONLY;
else if (!useSecurityCameras && useSecurityDrones)
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.DRONES_PREFERRED;
else if (!useSecurityCameras && useSecurityDrones)
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.NO_OBSTACLES;
else
{
// Randomly determine which type to use for a new instance of procedural scavenger.
int randomInt = Random.Range(1, 4);
switch (randomInt)
{
case 1:
{
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.CAMERAS_ONLY;
break;
}
case 2:
{
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.DRONES_PREFERRED;
break;
}
case 3:
{
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.CAMERAS_THEN_DRONES;
break;
}
case 4:
{
procedObstacleType = E_PROCEDURAL_OBSTACLE_TYPE.DRONES_THEN_CAMERAS;
break;
}
}
}
}

// Initialize which objectives / pickup drop types are valid when initializing a new instance of procedural scavenger
private void InitializeRandomObjectiveTypes()
{
if (!useProximityDrones)
{
procedDropType = E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_ONLY;
}
else
{
int randomInt = Random.Range(1, 4);
switch (randomInt)
{
case 1:
{
procedDropType = E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_ONLY;
break;
}
case 2:
{
procedDropType = E_PROCEDURAL_OBJECTIVE_TYPE.DRONES_PREFERRED;
break;
}
case 3:
{
// Randomize which chain gets to use pickup drops.
chainSeed = Random.Range(0, 1);
procedDropType = E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_SEPARATE_CHAINS;
break;
}
case 4:
{
procedDropType = E_PROCEDURAL_OBJECTIVE_TYPE.PICKUPS_DRONES_MIX;
break;
}
}
}
}

}

&nbsp;
<pre>