TayLee Young|

Systems Programmer and Game Designer


								

Featured Work|

Here is some of my most recent work! Some are still in production, some are finished. This work showcases my skills in programming, game design, and systems. If you would to take a peek at my resume, you can find it here, or over in my about me page.

I will be adding a blog section soon, which covers some other projects I have worked on, as well as my thoughts on other related technological topics, so stay tuned for that!

SHUTTLEFALL
Aliens, corrupt business practices, and a massive drill.
Moth 2: The Mothening
From fluttering wings to fortune — grow your moth empire!
Refraction
A custom engine running CPU-based pixel-by-pixel light calculations? How hard can that be?
Procedurally Generated Dungeon
What happens when you try to design a playable level using entirely procedurally generated layouts?

Moth 2: The Mothening

On an alien planet, moths grow to be 5x the size of the ones we find on Earth. They are cute, cuddly, and worth quite the price. You set up a base on this planet, creating a greenhouse as your base of operations. Alone on the planet, you seek to catch, breed, and sell moths all to become a monopoly.
It's hard to sell these guys with how cute they are though...

Overview

This student project was one of the first major scale projects I had been a part of. It was the first time I worked on such a large team consisting of different disciplines, the first time I was able to work on a GAM project using Unreal Engine 5, let alone any commercial engine. The project was a massive learning experience for me, yet one of my favorite projects to date. I've been in leadership positions before, but not quite of this magnitude. I was the Technical Advocate (or Technical Lead, or the "manager of the programmers"), and I was also the main systems designer. Considering this project was heavily systems-focused, I had a lot of work to do.

My Focus

My main goal was to design the Market, a massive computer sitting in the greenhouse. The player could catch or breed moths and sell them here, where the main point was to get as much money and monopolize the moth market. On the outside, it seemed simple — take the moth, look up a price, and sell it for said price. However, we wanted to be unique. Since this game initially started as an economy simulator, we decided to put a little more thought into it. The breakdown of how a single moth's value gets calculated is as follows

  1. Check if the moth is wild-caught or bred. Wild-caught moths sell for slightly less.
  2. We need both the species and that specific moth's quality next — the most important factors in determining a base price.
  3. Taking the species, we can add certain modifiers to get the proper ratio of quality to sell price. This is important as rarely does a species have a simple exponential path — most are logarithmic, sine, or even a little bit linear. This gives the player a reason to find what species are worth more at different qualities.
  4. Of course, this means we also must check the quality. From 1-100, we group the qualities into subcategories, some rarer than others. Breeding or being lucky while outside can get you a higher quality — which of course, typically means more money as well.
It gets a little more in-depth than all that, but that's the overview. While I worked on this nearly entirely by myself, I still got some great help from our designers and other programmers to help me test and try new methods to get it just right.
While working on this major mechanic, it was my job to ensure that the rest of the programmers had clear goals. I worked with each one personally, making sure that each one had the resources they needed, knew what they had to do, and whenever possible, worked on tasks that they were most compatible with. We had a programmer fitting for most tasks — UI/UX, tools, AI, gameplay, etc. I had to ensure that they were comfortable both in their tasks themselves, and their respective workloads. Communication was key to everything I did. Using a multitude of tools, such as Trello, Discord, When2Meet, and Teams, I could ensure that everyone had all the resources to communicate. Fleshed out documentation for the game itself aided in this as well.

Issues

Of course, nothing was all perfect, even beyond bugs and crashes. Conflicts between teammates, especially when it came to design choices, were common. This often meant that I had to help handle the situation or guide the conversation to a productive end. If the programmers felt like the deadlines being requested of them from the producer were too tight or unrealistic, I mediated that conversation as well, pushing to come to a compromise that doesn't overwhelm the programmers, yet ensures we were still on track for our milestones.
Additionally, as is the nature of programming, both my pride and joy in the market and the other systems I had a hand in, were not without their issues either. The market went through several iterations, causing me to delay and rework time and time again. Since the three core mechanics of catching, breeding, and selling were so interconnected, this also meant a lot of back-and-forth handling. Reworking or changing one mechanic often leads to a cascade of changes in the other two. While we often warned the designers and producers, we ultimately had many issues with a constantly changing game — to the point that the very market I put so much time into was nearly entirely scrapped, being reworked into a much different system. The bane of every software developer.

End Result

In the end, the project has been an incredible experience. While every project that I have worked on — big or small — always lets me expand my knowledge and experiment, this project is arguably the most relevant to me. It was the first time I was able to work on a project with such important roles, and it was also the definitive reason I enjoyed systems programming. I had an interest in it before but had yet to dive into it. This project was the perfect opportunity to do so, and I ran with it. With how interconnected and complex every system was, I was able to learn so much about how to design, implement, and maintain systems while balancing my other tasks as well. My leadership skills have also improved — while it wasn't the first time I had taken a role similar to it, this pushed what I already knew and proved there is always time for improvement.

Refraction

You find yourself lost in the depths of a dungeon. It's dark, damp, and cramped. Equipped with a solitary light, you venture forward, discovering large crystals that can shift and redirect light beams. Soon, it dawns on you — manipulating these beams unlocks pathways, each puzzle leading closer to an escape.
This student project was one of the first of my serious team-based projects. Having worked on some smaller projects before with fewer people, I had an idea of how it would go, ready for the challenges ahead, but it was a pivotal point in my learning.

Building the Engine


This project required us to build an engine from scratch — no commercial engines. We had specific tools and software we could use — OpenGL, SDL2, and ImGui for example — but the rest was up to us to create. We had to get a functional engine prototype up and running within a tight time constraint. I was in charge of most of this development. I would focus on building, updating, and maintaining the engine, adapting it to what the other programmers and designers needed. While the initial steps of creating a custom engine were daunting, I managed to produce a basic framework in just two weeks. This framework included input handling, scene management, level loading, basic rendering, debugging tools, and anything else I could think of that would be useful for the team.
Working on the engine gave me a newfound appreciation for aspects of game development that are often invisible to end users. Creating features like scene transitions and level management from scratch revealed the intricacies involved and the importance of a solid foundation. In addition, there were countless design choices I had to weigh in on. What engine design pattern should I choose? Observer? Composite? Singleton? There was so much to consider, but I opted for the singleton pattern due to its simplicity and ease of use. This was just one of many decisions that would shape the engine's architecture and the game's development. As someone who previously took these systems for granted when using professional engines, this was an eye-opening experience.

Code Snippets: Simple Engine Implementation Dropdown Icon


// singleton instance
Engine* Engine::instance = new Engine();

Engine::EngineCode Engine::Start()
{
	// time instance
	time = new Time();

	AudioManager.LoadMusicFromJSON("./Data/music.json");
	AudioManager.LoadSFXFromJSON("./Data/SFX.json");
	// initalize all systems
	for (int i = 0; i < systemCount; ++i)
	{
		try
		{
			systems[i]->Init();
		}
		catch (EngineCode engineFailure)
		{
			assert(engineFailure && "Engine failed to initialize. Location: Engine::Start(), Init()");
			return engineFailure;
		}
	}

	// main game loop
	while (isRunning && !closeRequested)
	{
		EngineCode code = NothingBad;

		try
		{
			code = Update();
		}
		catch (EngineCode updateFailure)
		{
			assert(updateFailure && "Engine failed during update. Location: Engine::Start(), Update()");
			return updateFailure;
		}
		if (code == CloseWindow)
			break;

		try
		{
			code = Render();
		}
		catch (EngineCode renderFailure)
		{
			assert(renderFailure && "Engine failed during render. Location: Engine::Start(), Render()");
			return renderFailure;
		}
		if (code == CloseWindow)
			break;
	}

	// stop engine
	return Stop();
}

Engine::EngineCode Engine::Stop()
{
	try
	{
		ShutDown();
	}
	catch (EngineCode shutdown)
	{
		assert(shutdown && "Engine failed to shut down. Location: Engine::Stop(), ShutDown()");
		return shutdown;
	}

	return EngineExit;
}

void Engine::EngineAddSystem(BaseSystem* sys)
{
	systems[systemCount++] = sys;
}

bool Engine::Paused()
{
	return paused;
}

void Engine::SetPause(bool pause)
{
	paused = pause;
}

// get the singleton instance
Engine* Engine::GetInstance()
{
	if (instance == nullptr)
	{
		instance = new Engine();
	}
	return instance;
}

Engine::Engine() : isRunning(true), systemCount(0), systems(), paused(false), time(NULL), closeRequested(false)
{
}

Engine::~Engine()
{
	if (instance != NULL)
	{
		delete instance;
	}
}

Engine::EngineCode Engine::Update()
{
	float dt = Time::Instance().Delta();

	// update all systems
	for (int i = 0; i < systemCount; ++i)
	{
		try {
			systems[i]->Update(dt);
		}
		catch (EngineCode updateFailure)
		{
			switch (updateFailure)
			{
			case CloseWindow:
				return updateFailure;
			default:
				assert(updateFailure && "Engine failed during system update. Location: Engine::Update()");
				throw(updateFailure);
			}
		}
	}
	return NothingBad;
}

Engine::EngineCode Engine::Render()
{
	// render all systems
	for (int i = systemCount; ++i)
	{
		systems[i]->Render();
	}
	return NothingBad;
}

Engine::EngineCode Engine::ShutDown()
{
	// close all systems
	for (int i = systemCount - 1; i >= 0; --i)
	{
		systems[i]->Close();
	}

	delete time;
	AudioManager.Free();
	return EngineExit;
}

// set flag to close game engine for whatever reasons you want
void Engine::SetCloseRequest(bool close)
{
	closeRequested = close;
}
										

Optimizing Performance


One of the most significant challenges we faced was performance optimization. Our game initially struggled to maintain even 30 FPS. Given my familiarity with the engine, I took the lead in diagnosing bottlenecks. This required diving into the graphics programmer's code, experimenting with Visual Studio's Profiler, and creating ImGui tools to track performance metrics in real-time.
A key decision was made right before this optimization phase: our graphics programmer wanted to perform lighting calculations on the CPU rather than the GPU. The lighting system relied on pixel-by-pixel math calculations, and using the CPU allowed the graphics programmer to maintain precise control over each individual pixel. While this method offered flexibility and allowed for the unique lighting effect we sought to create, it came at the cost of significant performance overhead. However, at the time, our team prioritized the control and simplicity of the CPU-based approach. We wanted to test the boundaries of what we could do, and although this decision contributed to the challenges we faced in maintaining stability and performance, it also allowed us to create a unique and visually striking game.


Developing an Editor


I took on the task of developing a level editor to streamline the designers' workflow. Using ImGui, I built an interface that allowed designers to add and remove entities, import layouts from their planning tools, and adjust properties dynamically. This task required me to dive deep into ImGui's documentation and address compatibility challenges with our engine's OpenGL and SDL2 setup. The process was slow at first, but the final editor significantly improved the team's efficiency.
Beyond the core editor functionality, I added debugging tools to track entities and their statistics, tracking and allowing dynamic modification of particles, dynamic adjustments of lights, display lighting properties, level importing and saving from JSON, etc. These tools weren't just useful; they were essential for diagnosing issues during the final stages of development. This process was one of the most difficult, as the documentation was loaded with information I had to sort through, and the process of creating the editor was a trial-and-error process. The other programmers were essential to get this task done in time, and I am grateful for their help!

Code Snippets: Entity Creation in Editor Dropdown Icon


int LevelCreatorScene::CreateCircleEntity()
{
	int circles_existing = 0;
	std::string number = "./Data/GameObjects/Circle";
	std::string filename = "./Data/GameObjects/Circle.json";

	Entity* temp = FileIO::GetInstance()->ReadEntity(filename);

	temp->addKey = "Circle";

	temp->key = "Circle" + std::to_string(circleCount);
	tempEntities.push_back(temp);
	++circleCount;
	return 0;
}

int LevelCreatorScene::CreateDoorEntity()
{
	int door_existing = 0;
	std::string number = "./Data/GameObjects/Door";
	std::string filename = "./Data/GameObjects/Door.json";

	Entity* temp = FileIO::GetInstance()->ReadEntity(filename);

	temp->addKey = "Door"; 

	temp->key = "Door" + std::to_string(doorCount);

	tempEntities.push_back(temp);
	++doorCount;
	return 0;
}

int LevelCreatorScene::CreateMirrorEntity(MirrorData mirror)
{
	int door_existing = 0;

	UNREFERENCED_PARAMETER(door_existing);

	std::string number = "./Data/GameObjects/Mirror";

	std::string filename;
	switch (mirror.spriteDirection) {
	case 1: filename = "./Data/GameObjects/MirrorTopLeft.json"; break;
	case 2: filename = "./Data/GameObjects/MirrorTopRight.json"; break;
	case 3: filename = "./Data/GameObjects/MirrorBottomLeft.json"; break;
	case 4: filename = "./Data/GameObjects/MirrorBottomRight.json"; break;
	default: return 0;
	}

	//classic naming conventions
	Entity* temp = FileIO::GetInstance()->ReadEntity(filename);
	temp->GetComponent()->SetReflection(mirror.direction);

	temp->addKey = "Mirror"; // this is for the map holding functions and gives access to function for circle

	temp->key = "Mirror" + std::to_string(mirrorCount);

	tempEntities.push_back(temp);
	++mirrorCount;
	return 0;
}

int LevelCreatorScene::CreateEmitterEntity(EmitterData emit)
{
	std::string number = "./Data/GameObjects/Emitter";
	std::string filename = "./Data/GameObjects/tempEmitter.json";

	Entity* temp = FileIO::GetInstance()->ReadEntity(filename);

	switch (emit.spriteDirection) {
	case 1:
	{
		BehaviorEmitter* mine = temp->GetComponent();
		mine->SetDirection({ 0.0f, -1.0f });
		mine->SetPosition(*temp->GetComponent()->GetTranslation());
		break;
	}
	case 2:
	{
		BehaviorEmitter* mine = temp->GetComponent();
		mine->SetPosition(*temp->GetComponent()->GetTranslation());
		mine->SetDirection({ 0.0f, 1.0f });
		break;
	}
	case 3:
	{
		BehaviorEmitter* mine = temp->GetComponent();
		mine->SetDirection({ 1.0f, 0.0f });
		mine->SetPosition(*temp->GetComponent()->GetTranslation());
		break;
	}
	case 4:
	{
		BehaviorEmitter* mine = temp->GetComponent();
		mine->SetDirection({ -1.0f, 0.0f });
		mine->SetPosition(*temp->GetComponent()->GetTranslation());
		break;
	}
	default: return 0;
	}
	temp->addKey = "Emitter"; // this is for the map holding functions and gives access to function for circle

	temp->key = "Emitter" + std::to_string(emitterCount);

	tempEntities.push_back(temp);
	++emitterCount;
	return 0;
}
									

Code Snippets: Scene Settings Dropdown Icon


ImGui::Text("Scene Settings");
ImGui::Text("./Data/Scenes/''LevelNameHere''");
if (ImGui::TreeNode("Export:"))
{
	ImGui::Text("TileMapOnly");
	ImGui::InputText(".json", myTextBuffer, sizeof(myTextBuffer));
	if (ImGui::Button("ExportTileMap"))
	{
		std::string exportFileName = std::string(myTextBuffer);
		if (exportFileName.empty())
		{
			ImGui::OpenPopup("EmptyExportFileNamePopup");
		}
		else if (!IsFileNameValid(exportFileName))
		{
			ImGui::OpenPopup("InvalidExportFileNamePopup");
		}
		else
		{
			FileIO::GetInstance()->ExportTileMap(myTextBuffer);
			ImGui::OpenPopup("SuccessfulExport");
		}
	}

	if (ImGui::Button("ExportFullScene"))
	{
		std::string exportFileName = std::string(myTextBuffer);
		if (exportFileName.empty())
		{
			ImGui::OpenPopup("EmptyExportFileNamePopup");
		}
		else if (!IsFileNameValid(exportFileName))
		{
			ImGui::OpenPopup("InvalidExportFileNamePopup");
		}
		else
		{
			ExportScene(myTextBuffer);
			ImGui::OpenPopup("SuccessfulExport");
		}
	}

	ImGuiManager::RenderOKPopup("SuccessfulExport", "Exported!");
	ImGuiManager::RenderOKPopup("EmptyExportFileNamePopup", "Please provide a file name for export!");
	ImGuiManager::RenderOKPopup("InvalidExportFileNamePopup", "Illegal characters in the tilemap name GOOBER!");

	ImGui::TreePop();
}

if (ImGui::TreeNode("Import:"))
{
	static char filenameBuffer[256];
	ImGui::InputText(".json", filenameBuffer, sizeof(filenameBuffer));

	ImGui::Text("NOTE: this will reset the current level.");
	{
		if (ImGui::Button("Submit"))
		{
			std::string filename = "./Data/Scenes/" + std::string(filenameBuffer) + ".json";

			if (std::string(filenameBuffer).empty())
			{
				ImGui::OpenPopup("EmptyImportFileNamePopup");
			}
			else if (!IsFileNameValid(filenameBuffer))
			{
				ImGui::OpenPopup("InvalidImportFileNamePopup");
			}
			else
			{
				std::ifstream file(filename);
				sceneName = std::string(filenameBuffer);
				if (file.is_open())
				{
					file.close();
					Renderer::GetInstance()->CleanRenderer();
					LevelBuilder::GetInstance()->LoadTileMap(filename);
					if (EntityContainer::GetInstance()->CountEntities() > 0)
					{
						int size = EntityContainer::GetInstance()->CountEntities();
						for (int i = 0; i < size; ++i)
						{
							if ((*EntityContainer::GetInstance())[i])
							{
								tempEntities.push_back((*EntityContainer::GetInstance())[i]);
								tempEntities[i]->addKey = tempEntities[i]->GetRealName();
								if (tempEntities[i]->GetRealName().compare("Player") == 0)
								{
									playerExists = true;
								}
							}
						}
					}
					json counts = FileIO::GetInstance()->OpenJSON("./Data/Scenes/" + std::string(filenameBuffer) + "OBJECTS.json");

					if (counts["Circle"].is_object())
					{
						circleCount = counts["Circle"]["count"];
					}
					if (counts["Door"].is_object())
					{
						doorCount = counts["Door"]["count"];
					}
					if (counts["Mirror"].is_object())
					{
						mirrorCount = counts["Mirror"]["count"];
					}
					if (counts["Emitter"].is_object())
					{
						emitterCount = counts["Emitter"]["count"];
					}
					if (counts["Receiver"].is_object())
					{
						ReceiverCount = counts["Receiver"]["count"];
					}

					if (!entityManager->InitializeProperties(filename + "OBJECTS.json"))
					{
						ImGui::OpenPopup("NoObjectsInScene");
					}

					ImGui::OpenPopup("SuccessfulImport");
				}
				else
				{
					ImGui::OpenPopup("FileNotFoundPopup");
				}
			}
		}

		ImGuiManager::RenderOKPopup("SuccessfulImport", "Successfully imported the scene!");

		ImGuiManager::RenderOKPopup("NoObjectsInScene", "Objects do not exist in this scene.");

		ImGuiManager::RenderOKPopup("EmptyImportFileNamePopup", "Please provide a file name for import!");

		ImGuiManager::RenderOKPopup("InvalidImportFileNamePopup", "Please provide a valid file name without illegal characters for import!");

		ImGuiManager::RenderOKPopup("InvalidImportFileNamePopup", "Please provide a valid file name without illegal characters for import!");

		ImGuiManager::RenderOKPopup("FileNotFoundPopup", "File not found!");
	}
	ImGui::TreePop();
}
									
In the end, our team produced a functional, engaging puzzle game with dynamic lighting, handcrafted visuals, and satisfying gameplay. The experience taught me as much about teamwork and project management as it did about technical skills. From handling conflicts in code and within the team to finding creative solutions under pressure, this project laid the foundation for my future growth as a developer. To this day, I'm grateful for the challenges and lessons of this project. It wasn't perfect — far from it — but it was a pivotal experience in my journey as a programmer and game designer. The experience reinforced my passion for game development and systems programming, and I carry the lessons with every project!

Procedurally Generated Dungeon

What happens, as a game designer, when you attempt to create a level whose layout is completely procedurally generated? This project set out to test this concept by designing a dungeon crawler where no two playthroughs were the same. The player would navigate a maze-like environment, defeat enemies, collect powerups, and kill the big-bad boss -- all while traversing a dynamically generated level.

Designing the Level


This was a student project for a level design class, where we were given a few basic assets, few scripts for enemy AI and camera logic, and a blank state to work with. The goal was not only to create a level with a clear beginning, middle, and end but also to ensure that it was still a fun and engaging experience despite being largely procedurally generated. Most other students opted for a design where the player had to traverse from one side of the level to another, as it was much easier to create a difficulty curve and clear player progression. But I wanted to challenge myself.
Instead of starting from one side to another, I wanted the player to start in the center of the level. The player could choose any of the four corners of the dungeon, each with a unique biome, distinct enemies, powerups, and a powerful mini-boss. This design added another layer of complexity: the level had to allow the player freedom throughout the entire level while still maintaining overall structure, difficulty and progression. The procedural generation needed to make sure that each path was navigable, balanced and engaging -- no matter what the procedural generation spat out.

Corridor Generation


Generating the pathways was one of the most technically challenging aspects of the project. The corridors had to provide a clear flow for the player to follow, allowing them to reach all areas while preserving the feel of well-designed level. Making randomized paths would result in either excessive dead ends or a chaotic, nonsensical layout. I needed to strike a balance between randomness and structure. So, I developed an algorithm that helped in path creation.
The corridor generation system started from the center room where the player spawns from. Each side of the room had the chance to have a corridor starting tile, which will begin the generation. The tile picks a random direction and starts creating a path. It checks if another tile or the world border is in the way for each placement. If an obstacle was found, it would try another direction. If that too fails, it backtracks and attempts another path using "branches" -- flags certain tiles have that allow the path to stem from, similar to a tree. This was somewhat inspired from other procedural generation algorithms such as cellular automata and recursive subdivision. The central spawn placements acts as a "seed", similar to what is used in cellular automata, making sure that the paths organically grow from a specific point. The branching corridors are also similar to recursive subdivision, where the paths were segmented into smaller interconnected paths. I had to ensure none of the paths didn't overlap or connect oddly, such as touching parallel paths. This took extensive tweaking and testing to get right. I never did get it exactly the way I wanted, but the challenge alone was an accomplishment.

Note: this is only a small portion of the code!

There is a lot of other checking and boring variable tweaking elsewhere, but we can at least see some basic construction with these snippets.

Code Snippets: Corridor Generation Dropdown Icon


// Spawns a corridor
void GenerateCorridor(int startX, int startY)
{
    // get a starting tile to start a corridor
    SpawnTile(startX, startY);

    // directions for movement (up, down, left, right)
    List directions = new List
    {
        new Vector2Int(0, 1),  // up
        new Vector2Int(0, -1), // down
        new Vector2Int(-1, 0), // left
        new Vector2Int(1, 0)   // right
    };

    // store x and y values
    int currentX = startX;
    int currentY = startY;

    // store directions
    Vector2Int currentDirection = directions[RNG.Next(directions.Count)];

    // store branch points
    Stack branchPoints = new Stack();

    bool canPlaceTile = true;
    int attempts = 0; // number of retries when finding a valid direction
    int maxAttempts = 60; // maximum retries before stopping corridor generation

    while (canPlaceTile)
    {
        if (IsValidTilePlacement(currentX, currentY))
        {
            TrySpawnPowerup(currentX, currentY);
            TrySpawnEnemy(currentX, currentY);

            // 80% chance to mark this tile as a branch point
            if (RNG.Next(100) < 80)
                branchPoints.Push(new Vector2Int(currentX, currentY));

            // 0.5% chance to generate a trident-shaped branch
            if (RNG.Next(100) < 0.5 && GenerateTridentBranch(ref currentX, ref currentY, ref currentDirection, branchPoints))
                continue;

            // 50% chance to generate a staircase pattern
            if (RNG.Next(100) < 50 && GenerateStaircasePattern(ref currentX, ref currentY, currentDirection))
                continue;

            // 10% chance to generate a room
            if (RNG.Next(100) < 10 && GetDistanceFromCenter(currentX, currentY) > 15)
            {
                int[] possibleSizes = { 3, 5, 7 };
                PlaceRoom(currentX, currentY, possibleSizes[RNG.Next(possibleSizes.Length)], possibleSizes[RNG.Next(possibleSizes.Length)]);
                continue;
            }

            // 20% chance to change direction
            if (RNG.Next(100) < 20)
            {
                currentDirection = directions[RNG.Next(directions.Count)];

                // 50% chance to generate a spiral
                if (RNG.Next(100) < 50 && GenerateSpiralPattern(ref currentX, ref currentY, ref currentDirection))
                    continue;
            }

            // move in a valid direction or backtrack if stuck
            if (!TryChangeDirection(ref currentX, ref currentY, ref currentDirection, directions, branchPoints, ref attempts, maxAttempts))
                canPlaceTile = false;
        }
        else if (!TryBacktrack(ref currentX, ref currentY, branchPoints, ref currentDirection, directions))
        {
            canPlaceTile = false;
        }
    }
}
									

Code Snippets: Unique Corridor Generation Dropdown Icon


// generates a trident branch
bool GenerateTridentBranch(ref int x, ref int y, ref Vector2Int direction, Stack branches)
{
	Vector2Int originalBranch = new Vector2Int(x, y);
	Vector2Int initialDirection = direction;

	for (int i = 0; i < 7; i++)
	{
		x += direction.x;
		y += direction.y;
		if (!IsValidTilePlacement(x, y)) return false;
		SpawnTile(x, y);
	}

	branches.Push(originalBranch);
	return true;
}

// generates a staircase pattern
bool GenerateStaircasePattern(ref int x, ref int y, Vector2Int direction)
{
	for (int i = 0; i < 50; i++)
	{
		x += direction.x;
		y += direction.y;
		if (!IsValidTilePlacement(x, y)) return false;
		SpawnTile(x, y);
	}
	return true;
}

// generates a spiral pattern
bool GenerateSpiralPattern(ref int x, ref int y, ref Vector2Int direction)
{
	int[] spiralSteps = { 7, 6, 6, 4, 4, 3, 2 };

	foreach (int steps in spiralSteps)
	{
		for (int i = 0; i < steps; i++)
		{
			x += direction.x;
			y += direction.y;
			if (!IsValidTilePlacement(x, y)) return false;
			SpawnTile(x, y);
		}
		direction = new Vector2Int(-direction.y, direction.x);
	}

	return true;
}

Overall design


To make each corner of the dungeon feel distinct, I designed them as unique biomes, each with its own enemy types, powerups, and different kinds of corridor designs. The central area in-between the biomes, which is largely meant to be both a gentle beginning and a break in-between bosses, was relatively neutral. Weaker enemies with little to no power-ups. This was to prepare the player for their adventure -- getting used to combat, learning how enemies act, the controls, etc. It was meant to encourage exploration as well, prodding the curiosity of the player to keep going in whatever direction they choose. Additionally, since the player could choose any corner to explore first, I designed it so that for every boss killed, the remaining bosses and enemies would get stronger. This helps defeat any linearity, but also ensures that the player is always challenged.
With the little resources I had, I attempted to make each biome have some sort of theme. I attempted to assign enemy types and certain pattern generation within the corridors to try and convey this. The black biome, which was home to enemies with a shotgun like spread, had certain corridor patterns that allowed for more open combat. The green biome, which had slow tanky enemies had spiral-like patterns, which allow the player to lose the enemy through line of sight if the player needs to recover. I also attempted to make the power-ups reflect this too! Fast enemies were more likely to drop speed-boosts, while tanks were more likely to drop health-boosts. This allowed players to adapt their strategy based on the biome they were in. Tank enemies too strong? Go kill some spread enemies for some extra damage! This approached helped ensure the procedural generation didn't feel too static; every playthrough felt fresh yet still maintained some sort of consistency.
Another important aspect of this level design was the gameplay arc -- the pacing and progression of challenges throughout the level. I wanted to implement a false sense of failure for the hook. Players start with a single heart, no powerups, and enemies that were stronger than they are. Balanced so delicately, this largely increased their chance of "dying" at this point, but still allowed the player to experiment with controls and combat. When they died, they would respawn, keeping any powerups they may have managed to find, and gaining some extra boosts. This was to reinforce the idea that they had to grow stronger to progress, a theme carried out further into the level. This safety net of course only lasted for this beginning phase. Once the player first "dies", the deaths are permanent, increasing difficulty. This created an engaging difficulty curve.

Code Snippets: Enemy Buffs Dropdown Icon


void SetWeaponStats()
{
	WeaponLogic weapon = GetComponentInChildren();
	if (weapon == null) return; // No weapon attached, exit early

	// Set base stats per enemy type
	SetBaseWeaponStats(weapon);

	// Apply buffs based on which bosses have been killed
	ApplyBossBuffs(weapon);
}

// Sets the base weapon stats per enemy type
void SetBaseWeaponStats(WeaponLogic weapon)
{
	switch (enemyType)
	{
		case EnemyType.BaseEnemy:
			weapon.ShotCooldown = 3f;
			weapon.BulletsPerShot = 1;
			weapon.BulletSpreadAngle = 0.0f;
			weapon.BulletRange = 5.0f;
			weapon.BulletSpeed = 3.0f;
			break;

		case EnemyType.FastEnemy:
			weapon.ShotCooldown = 3f;
			weapon.BulletsPerShot = 1;
			weapon.BulletSpreadAngle = 0.0f;
			weapon.BulletRange = 5.0f;
			weapon.BulletSpeed = 6.0f;
			break;

		case EnemyType.SpreadEnemy:
			weapon.ShotCooldown = 6f;
			weapon.BulletsPerShot = 4;
			weapon.BulletSpreadAngle = 25f;
			weapon.BulletRange = 5.0f;
			weapon.BulletSpeed = 5.0f;
			break;

		case EnemyType.TankEnemy:
			weapon.ShotCooldown = 3f;
			weapon.BulletsPerShot = 3;
			weapon.BulletSpreadAngle = 0.0f;
			weapon.BulletRange = 6.0f;
			weapon.BulletSpeed = 2.0f;
			break;

		case EnemyType.UltraEnemy:
			weapon.ShotCooldown = 5f;
			weapon.BulletsPerShot = 5;
			weapon.BulletSpreadAngle = 50f;
			weapon.BulletRange = 20.0f;
			weapon.BulletSpeed = 5.0f;
			break;

		case EnemyType.BossEnemy:
			weapon.ShotCooldown = 3f;
			weapon.BulletsPerShot = 8;
			weapon.BulletSpreadAngle = 40.0f;
			weapon.BulletRange = 100.0f;
			weapon.BulletSpeed = 4.0f;
			break;
	}
}

// Applies boss kill buffs to the weapon and enemy stats
void ApplyBossBuffs(WeaponLogic weapon)
{
	if (!PCGObject.boss1Killed && !PCGObject.boss2Killed && !PCGObject.boss3Killed && !PCGObject.boss4Killed)
		return; // No buffs to apply, exit early

	// List of boss buffs to apply
	(bool bossKilled, ref bool buffApplied, float shotCooldownReduction, int healthIncrease, float speedIncrease,
		float bulletRangeIncrease, float bulletSpeedIncrease, int bulletsPerShotIncrease, float bulletSpreadIncrease, int aggroRangeIncrease)[] bossBuffs =
	{
		(PCGObject.boss1Killed, ref boss1BuffApplied, -0.3f, 1, 0.25f,  0f,   0f,   2,  0f,  0),
		(PCGObject.boss2Killed, ref boss2BuffApplied, -0.3f, 1, 0.25f,  5.0f, 2f,   0,  0f,  0),
		(PCGObject.boss3Killed, ref boss3BuffApplied, -0.3f, 1, 0.25f,  0f,   0f,   0,  5f,  0),
		(PCGObject.boss4Killed, ref boss4BuffApplied, -0.3f, 1, 0.25f,  0f,   0f,   0,  0f,  5)
	};

	foreach (var (bossKilled, buffApplied, cooldownReduction, healthIncrease, speedIncrease, rangeIncrease, speedBoost, bulletsPerShot, spreadIncrease, aggroIncrease) in bossBuffs)
	{
		if (bossKilled && !buffApplied)
		{
			weapon.ShotCooldown += cooldownReduction;
			Health += healthIncrease;
			Speed += speedIncrease;
			weapon.BulletRange += rangeIncrease;
			weapon.BulletSpeed += speedBoost;
			weapon.BulletsPerShot += bulletsPerShot;
			weapon.BulletSpreadAngle += spreadIncrease;
			AggroRange += aggroIncrease;

			buffApplied = true;
		}
	}

	// TankEnemy and UltraEnemy get extra bonuses
	if (enemyType == EnemyType.TankEnemy || enemyType == EnemyType.UltraEnemy)
	{
		foreach (var (bossKilled, buffApplied, cooldownReduction, healthIncrease, speedIncrease, rangeIncrease, speedBoost, bulletsPerShot, spreadIncrease, aggroIncrease) in bossBuffs)
		{
			if (bossKilled && !buffApplied)
			{
				weapon.ShotCooldown += cooldownReduction;
				Health += (enemyType == EnemyType.TankEnemy ? 2 : healthIncrease);
				Speed += speedIncrease;
				weapon.BulletRange += rangeIncrease;
				weapon.BulletSpeed += speedBoost;
				weapon.BulletsPerShot += bulletsPerShot + (enemyType == EnemyType.UltraEnemy ? 1 : 0);
				weapon.BulletSpreadAngle += spreadIncrease;
				AggroRange += (enemyType == EnemyType.TankEnemy ? 7 : aggroIncrease);

				buffApplied = true;
			}
		}
	}
}
	

Final Thoughts


This project was a massive learning experience, beyond simply coding procedural generation. It forced me to think critically about level design principles, player agency, and balancing procedural content with structured gameplay. I had to guide the player without any explicity directions, avoid repitition and predictability, and ensure that the level was both challenging and engaging. Through extensive playtesting and iteration, I learned firsthand how procedural generation isn't just about randomness; it is about controlled variation. Level design is not simply building a static world, its preparing for every player to experience and see things differently.
Overall this project gave me valuable insights into game systems design, algorithmic level generation, and gameplay balancing, skills that will, as always, be invaluable in my future projects!

SHUTTLEFALL

With the grip of a powerful corporation around your neck, demanding you mine resources for their profit and to relieve your debt, you must land your ship on an alien planet and drill deep down, careful to not make too much noise. Will you ever, truly, be free?


This is one of my more recent student projects that I am currently apart of. I joined fairly recently, yet I already am working towards goals that push me as a programmer. I am immensely grateful that my name is becoming known amongst my peers, as it didn't take long for me to find a team and be hired, let alone be given an opportunity to design and immediately start work on this project. Thanks to my work on the previous projects, I had an idea on how to navigate and integrate some new systems into the game.
As I am still fairly new to this team, I don't have much work. However, I have some plans I can share!
To begin, even prior to officially being hired, I had pitched some ideas to the team, which we worked off of. My first task was to create a sort of "level" system. Now, to be clear -- levels don't actually exist. As the player will always be on the ship, there isn't too much of a change of scenery in the way one would expect with "levels". Rather, there are days. The player must reach a specific quota amount to advance to the next day.
My goal was to make this mechanic more interesting. Each day, a weather event has a chance to occur. This weather is more of a wrapper for a set of modifiers that affect the player's day. For example, when it is rainy, the alien is more aggressive, the drill overheats at a lower rate, and the quota may be slightly higher. Or, alternatively, if it is a heatwave, the alien is less aggressive, the drill overheats much faster, and the quota is also higher. Depending on the "difficulty" of these conditions, the player can get a higher paycheck at the end of the day, which can be used to upgrade the ship, buy new addons, or even pay off the debt.
A sunny day.

A rainy day.
This largely has been implemented, though with playtesting, we will be experimenting on more dynamics. I purposely programmed the system to be easily modifiable, as well as incredibly simple to add more modifications to nearly anything we want. While there are really only three factors that get modified, it could be expanded to include more, such as faster/slower repair speed, slower/faster drill speed, a faster/slower alien, etc. We also are considering allowing the player to choose where they will land on the planet, with each location having different weather events, encouraging the player to pick and choose where they want to go while also allowing them to play at their own difficulty.
We plan on expanding this with time, but also are seeking to add more systems that could integrate this more. Maybe upgrades that allow the player to "encourage" certain weather events, or cassettes (tools that the player can buy that will add more permanent modifiers) that could reuse these modifications.
I am deeply excited to continue working on this project, as not only has this been a great learning and teaching experience, but it also is simply so much fun to work on. The team values my input, upholds my wishes of creating a game that is inclusive for most players, and is always open to new ideas. I am grateful for this opportunity and I am excited to see where this project goes!

Contact Me!|

About Me|


"The last frontier is the human mind and we are its pioneers." - Prey 2017

Hello!

I am TayLee Young, a third year college student at DigiPen Institute of Technology. I am currently studying Computer Science and Game Design. I focus in systems programming, data analysis, and game development. I am proficient in C, C++, C#, and I am learning Python, CSS and HTML. I am also familiar with Unity, Unreal Engine, and Visual Studio.
I have worked on several projects, some of which you can find in the Work section. I have worked teams, big and small, and I have experience in project management and team leadership. Of course, I am always looking to expand my knowledge and experience.

I am deeply motivated to create games that are more than just entertainment. I want to create works of art -- art that inspires people to see things in a new light, to create deeply personal and provocative worlds, and to challenge the status quo. I want to not only create games, but to create experiences that change people's lives.
I am heavily inspired by games like Prey, Night in the Woods, Journey, Silent Hill, and Minecraft. These have impacted my life in ways that I never thought possible. Some inspired my path for game design, others have inspired my deep love and appreciation for the art of creation. I will always remember with fondness the first time I played these games, as they have made me who I am today!

With every project I am in and every team I am apart of, I push strongly for inclusivity, diversity, and accessibility. I believe that games are for everyone. All people, regardless of their background, should experience the joy of this art. As a member of the LGBT+ community, I know firsthand the importance of representation, inclusion, and acceptance. No one should feel isolated in their experience. This is my mission and goal. To create games that are for everyone, that inspire everyone, and that change everyone.

Currently, I am apart of team ADMIS, working on a student project about a corrupt corporation, aliens, and a big beefy drill. Feel free to read about that too! I am looking for internships and job opportunities, so feel free to contact me anytime!