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; } } } } } <pre>