Featured Projects

Viticulture

Viticulture

Summary

I ported the digital version of Viticulture, a board game originally created by Jamey Stegmaier & Alan Stone, for the Last Gameboard's tabletop console platform.

Gameplay

In this worker-placement board game, players have access to different tasks during each season. By strategically using workers and visitor cards, players expand their vineyards and fulfill client orders by constructing buildings, planting vines, and producing wine.

Development

After receiving the source code from the original developers of the digital Viticulture version, DIGIDICED, I examined the project architecture and established a plan for extracting all of the unnecessary scripting and logic related to other platforms such as integrations with Google Play and Steamworks, while still maintaining the structural integrity of the project and ensuring that future source control merges from their main branch could be sorted out smoothly.

Much of the work involved in the porting process involved changes to the UI and UX of the game. In addition to changing the overall HUD to support showing the stats for up to six players at the same time (up from one), I changed the aspect ratio of the entire experience from the 1920x1080 desktop environment to the 1920x1920 environment of the Gameboard. This involved designing and implementing a custom board rotation solution that dynamically turned to the active player or the player who had to perform an action at the moment.

Play

Access Viticulture through the Last Gameboard website here.

Screenshots

viticulture gameplay imageviticulture gameplay

Narrative

Set in rustic, pre-modern Tuscany, each player starts with a few plots of land, an old crushpad, a small cellar, and three workers. Their ultimate aspiration is to be the first to achieve success and establish their winery.

Roles

Lead Porter / Co-Developer, Interaction Design Consultant, Development Team Liaison

Creature Feature

Creature Feature

Summary

Creature Feature is a game of hand management and bluffing designed by Richard Garfield, mastermind behind the design of games such as Magic: the Gathering, RoboRally, and KeyForge. I developed a digital port of this project as part of Last Gameboard, Inc. for their eponymous tabletop console platform.

Gameplay

During each round, each player selects a Co-Star and a Star (cards with numeric values and, in some cases, special abilities) to compete for a role (a tile worth points). After revealing their Co-Stars, players have the option to switch their audition and aim for lower-rated films that offer fewer points. Winning a role earns points, but here's the catch: if your Star has a lower value than your Co-Star, you cannot win unless you are the only player competing for that film. However, the risk you take may pay off in the form of additional points!

Development

My team and I worked closely with the publisher of the original Creature Feature card game to develop comprehensive wireframes that accommodated all possible gameplay scenarios based on predefined rules.

Once we defined the project scope and established a cohesive visual style for the assets and animations, we focused on constructing a project architecture that integrated the unique features of the hardware we were developing for. This involved implementing multi-touch support to facilitate simultaneous interaction among multiple players, ensuring stable connections with multiple handheld companion applications, and optimizing the game's performance to prioritize speed even when the maximum number of players were connected.

An essential aspect of our development process was the creation of artificially intelligent bots that could join play sessions with insufficient players. Given that bluffing is a central element of this game, I programmed the bots with various strategies, randomly assigning their risk-taking approaches from reckless to chaotic to apprehensive.

Play

Access Creature Feature through the Last Gameboard website here.

Screenshots

creature feature gameplay imagecreature feature gameplay

Narrative

Play as a movie agent representing actors who excel in portraying particularly monstrous roles. Your goal is to secure them the leading role in a feature film or, if that proves unsuccessful, at least a part in any film.

Roles

Lead Gameplay Programmer, Quality Assurance Tester

Tools Used

Unity Game Engine, Visual Studio Code, GIMP, Github Desktop, The Last Gameboard proprietary tabletop console and its software development kit

Hibotchi

Hibotchi

Inspiration

This game was born of a desire to make Tetris into a dungeon-crawling deck-building game with roguelike elements like Slay the Spire.

Gameplay

After prototyping my first version of the game, where I restricted tetromino movement to match held cards, I realized that completing lines with this system in place became much harder. To adapt, I combined this idea with a cooking game I had in mind. I transformed the falling tetrominos into food and changed the goal to fulfilling customer orders by merging nearby food blocks.

Roles

Lead Game Designer, Core Programmer, Art Director, Pixel Artist, and Sound Designer

Design & Development

The main tasks involved were developing a robust ScriptableObject and GameEvent-based project architecture in Unity, creating unique gameplay mechanics for each chef class, optimizing the code for small file size and fast loading times, and designing a player progression curve to encourage daily engagement. To improve player retention, I introduced unlockable cards that rewarded users for completing games. Additionally, I implemented optional video reward ads to expedite card unlocks, with a 24-hour waiting period between each unlock using this method.

During development, I noticed that there were usually very few customers in the mid-to-late game, resulting in an accumulation of unused food at the bottom of the player's grid. To mitigate this issue, I implemented a solution where playing a card would deduct one second from the customer arrival timer. This straightforward adjustment ensured that more customers arrived when the player's hand was full and their restaurant was empty.

Based on playtesting and the principle of "following the fun," I made the significant design decision to alter the timing of card drawing. Instead of drawing a card when a food landed at the bottom of the grid, I changed it to when a food was instantiated at the top. I implemented this change to address situations where a food failed to reach the bottom or got destroyed, resulting in the player not receiving a card to guide the next spawned food. This could be frustrating, especially when the player had an empty hand and a new food appeared, leaving them with no actions to perform. The new approach ensured that players could take at least one action on each falling food, even if their hand was empty when it spawned.

Read the full Game Design Document here.

Tools Used

Unity Game Engine, Visual Studio Code, Adobe Photoshop, GIMP, Shotcut, Adobe Premiere, Adobe Audition

Get it on Google Play

Screenshots

Hibotchi Screenshot



Narrative

You play as a hibachi chef desiring to feed as many customers as possible in order to gain more experience to become a better chef.

Code Sample

The most common type of ScriptableObject I used in this project was the "Card".
										
	public class Card : ScriptableObject
	{
		// Each asset has a unique resource ID so it can be saved and loaded correctly.
		[SerializeField] private string uniqueResourceID;
		public string UniqueResourceID { get => uniqueResourceID; }
	
		// Effects are ScriptableObjects and are assigned here to determine what the card will do when played (and when played with a doubling effect, and when a new effect is added)
		private Effect[] effects;
		public Effect[] Effects { get => effects; }
	
		private Effect[] doubleEffects;
		public Effect[] DoubleEffects { get => doubleEffects; }
	
		private Effect[] effectsToAdd;
		public Effect[] EffectsToAdd { get => effectsToAdd; }
	
		private string cardName;
		public string CardName { get => cardName; }
	
		private string cardDescription;
		public string CardDescription { get => cardDescription; }
	
		private string effectDescription;
		public string EffectDescription { get => effectDescription; }
	
		// Drag and drop these from the Inspector in order to assign them correctly
		[Header("Scene References")]
		public TextMeshProUGUI cardText;
		public Canvas cardCanvas;
		public Color imageColor;
	
		public enum ChefClass
		{
			Balanced,
			Aggressive,
			Methodical,
			Quirky
		}
	
		public enum Rarity
		{
			Common,
			Uncommon,
			Rare
		}
		
		// Each card is assigned one or more Chef Classes to ensure it only can be used by a player of that class
		public List<ChefClass> chefClasses;
		public Rarity rarity;
	
		// Set these to tell it which animation and sound to play when it is played
		[SerializeField] private string animationTrigger;
		public string AnimationTrigger { get => animationTrigger; }
	
		[SerializeField] private string soundName;
		public string SoundName { get => soundName; }
	
		// Index is to determine the sprite index of its art from the main effect sprite sheet
		public int spriteIndex;
	
		public bool isShuffle, isPersistent, hasTrail, performEffectsOnce, isMovement, isUnlocked;
				
		public GameObject card;
		CardScript cs;
	
		public GameObject CardObject()
		{
			GameObject cardWithValues = Instantiate(Resources.Load("Prefabs/Card Prefab", typeof(GameObject))) as GameObject;
			
			// Load card art from public library of resource.loaded sprites based on index on sprite sheet
			Sprite image;
			
			if (PublicLibrary.Instance && spriteIndex < PublicLibrary.Instance.effectSprites.Length)
			{
				image = PublicLibrary.Instance.effectSprites[index];
			}
			else
			{
				image = Resources.Load("Sprites/Placeholder Sprite", typeof(Sprite)) as Sprite;
			}
	
			// Set values for CardScript
			cs = cardWithValues.GetComponent<CardScript>();
			cs.cardName = cardName;
			cs.card = this;
			cs.cardNameText.text = cardDescription;
			cs.effects = effects.Clone() as Effect[];
			cs.doubleEffects = doubleEffects.Clone() as Effect[];
			cs.cardImageRenderer.sprite = image;
			cs.cardImageRenderer.color = imageColor;
			cs.animTrigger = animTrigger;
			cs.hasTrail = hasTrail;
			cs.performEffectOnce = performEffectsOnce;
			cs.glow.SetActive(false);
	
			if (string.IsNullOrEmpty(SoundName))
			{
				cs.soundName = "Whoosh";
			}
			else
			{
				cs.soundName = soundName;
			}
	
			LerpAlpha lerpAlpha = cardWithValues.GetComponentInChildren<LerpAlpha>();
			
			if (isPersistent)
			{
				cs.canvasColor = new Color32(217, 206, 186, 255);
				lerpAlpha.enabled = false;
			}
			else
			{
				cs.canvasColor = new Color32(117, 117, 117, 255);
				lerpAlpha.enabled = true;
			}
	
			return cardWithValues;
		}
	}
										
									
Caldera

Caldera

Inspiration

Caldera is a puzzle game in which the goal is to defeat enemies by moving tiles around. One crucial pillar of game design was to ensure that each level could be solved with multiple solutions. Into The Breach was the primary inspiration.

Gameplay

Tap adjacent tiles to move on a grid, moving tiles in your row or column the chosen direction, similar to 2048. The catch was that there were enemies, and to defeat them, the player had to push them off of the edge of the grid.

Roles

Lead Level Designer, Sound Designer, and Programmer

Development

The first part of the process was designing enemies. We had one enemy that moved one space toward the player each turn (using the A* pathfinding algorithm) and attacked one space in front of it, and we immediately knew that we could take that one step further by creating an enemy that shot a beam to each tile in front of it rather than just one.

The next step involved creating the terrain, starting with movable sandy land and then progressing to non-movable mountainous land. We also introduced chasms, which acted like the ocean at the grid edges but were located within the land. The levels are intended to showcase each enemy and land type individually, gradually integrating new mechanics with previously introduced ones. We thoroughly tested each level to ensure validity and multiple potential solutions.

Tools Used

Unity Game Engine, Visual Studio Code, Aseprite, Adobe Creative Suite, XCode

Play

Get it on Google Play

Media

Caldera Screenshot

Code Sample

This script dynamically spawns a button onto a Canvas for each Level in the build, then restricts access to only unlocked levels.
											
	public class LevelSelectionManagerScript : MonoBehaviour
	{
		public GameObject levelButton;
		public GameObject portraitContent;
		public GameObject landscapeContent;
		public MainMenuScript mms;
	
		public void LoadLevels()
		{
			if (DataSaver.instance != null)
			{
				DataSaver.instance.LoadData();
	
				for (int i = 0; i < DataSaver.instance.completedLevels.Length; i++)
				{
					GameObject b = Instantiate(levelButton) as GameObject;
	
					if (Camera.main.aspect > 1)
					{
						b.transform.SetParent(landscapeContent.transform);
					}
					else
					{
						b.transform.SetParent(portraitContent.transform);
					}
	
					int levelToLoad = i + 1;
					
					string path = SceneUtility.GetScenePathByBuildIndex(levelToLoad);
					string sceneName = path.Substring(0, path.Length - 6).Substring(path.LastIndexOf('/') + 1);
					string loadLevel = sceneName;
					
					b.transform.localScale = Vector3.one;
	
						Button bButton = b.GetComponent<Button>();
						TextMeshProUGUI bTMP = b.GetComponentInChildren<TextMeshProUGUI>();
					bButton.onClick.AddListener(() => mms.LoadSelectedScene(levelToLoad));
					bButton.interactable = DataSaver.instance.completedLevels[i];
	
					bTMP.text = loadLevel;
					bTMP.fontSize = 14;
				}
			}
			else if (PlayerPrefsSave.instance != null)
			{
	
				PlayerPrefsSave.instance.Load();
	
				for (int i = 0; i < PlayerPrefsSave.instance.totalLevels; i++)
				{
					GameObject b = Instantiate(levelButton) as GameObject;
	
					if (Camera.main.aspect > 1)
					{
						b.transform.SetParent(landscapeContent.transform);
					}
					else
					{
						b.transform.SetParent(portraitContent.transform);
					}
	
					int levelToLoad = i + 1;
					string path = SceneUtility.GetScenePathByBuildIndex(levelToLoad);
					string sceneName = path.Substring(0, path.Length - 6).Substring(path.LastIndexOf('/') + 1);
					string loadLevel = sceneName;
	
					b.transform.localScale = new Vector3(1, 1, 1);
	
						Button bButton = b.GetComponent<Button>();
						TextMeshProUGUI bTMP = b.GetComponentInChildren<TextMeshProUGUI>();
					bButton.onClick.AddListener(() => mms.LoadSelectedScene(levelToLoad));
					if (i <= PlayerPrefsSave.instance.levelsCompleted)
					{
						bButton.interactable = true;
					}
					else
					{
						bButton.interactable = false;
					}
	
					bTMP.text = loadLevel;
					bTMP.fontSize = 14;
				}
			}
		}
	}