top of page

Blog: Full-Tilt Game

Writer: Jack Stevenson
Jack Stevenson
Aug 25
20 min read

Updated: 6 days ago


ONGOING



Full-Tilt: Sprint 4

October 22, 2026


For sprint 4, I continued my work on our game's audience system, primarily fixing bugs with the code and working to further integrate its functionality into the rest of the game. Most of my time was spent integrating UI functionality into the audience system and consolidating UI responsiblities away from other classes in order to maintain a cleaner code base. Beyond the audience manager, I also improved the modularity of the game's camera and widgets by putting their movement logic into dedicated scene components that can be easily reused.


To start the sprint, there were a couple bugs remaining with the build I created from last sprint. Primarily, the code I made for selecting the next or previous audience member did not function correctly when the player tried viewing the previous slot. This was because my previous loop's code would negate the final index for the loop when going in the reverse order. I only realized later that Unreal's for loop blocks cannot go in reverse, and will instead stop at the first index.


To fix this, I made a blueprint macro library so that I could make a universal macro that can properly loop over an array going in either direction. This macro works by having a base for loop that goes from 0 to a given array size, where the current index is added or subtracted to a starting index supplied to the macro. I also found a custom modulo node online that's designed to handle negative values in a way that makes them better suited for traversing arrays. Replacing the old for loop with this new macro immediately fixed the bug, letting me package and ship the game again for playtesting.


Next, I got to work on the discard feature. In the game, the player will be able to sell a member for a fraction of the price they purchased said member for. While I had most of the code already implemented, there were a few things I had to iron out in order for it to work properly: adding a dedicated sell button on the widget and fixing an issue with selling the last audience member.


For the dedicated sell button, I simply copy and pasted the main menu button, relabeled it, and made it call the sell audience member function upon being clicked. As for fixing the issue, it was caused by the widget throwing an error when it failed to find a new member to select. I fixed it by making the widget only update its information when the new selection slot is actually valid (not -1).


With the discard functionality done and some bugs fixed, I realized that a lot of the audience member's UI code was scattered around in multiple classes. Additionally, the camera's move to function was being called by the audience manager itself rather than from our core UI manager. This all resulted in the audience manager's UI logic being scattered across multiple classes without any meaningful consolidation.


Since these scattered responsibilities made it rather cumbersome to work on the audience manager's UI, I decided to create an entirely new blueprint solely dedicated to managing the audience member's UI. This would both make it easier for me to improve the audience manager's UI and allow my teammate and I to work on different aspects of our game's UI at the same time. It would also make it much easier for me to add extra widget components without needing to modify the core UI manager.


Unfortunately, I couldn't just drag and drop all of the code into this new audience UI manager and expect to just work. I also needed to rewire all of the classes that originally handled the audience manager's UI to now either communicate with the audience UI manager or decouple it all-together. An example of this is the camera's move-to logic, where the audience manager had previously been responsible for moving the camera to the currently selected member.


Instead of having the audience manager directly reference the new audience UI manager, I created a delegate that the audience manager would call each time a new member is selected or deselected. This would not only allow the audience UI manager to handle the camera transition logic on its own, but also allow other blueprints to interact with the audience manager when members are selected or deselected. This new delegate also allowed me to fix a longstanding issue, where the preview widget didn't fetch the currently selected user's information until the player hovered over it. The widget now simply listens to the audience manager's on member select delegate and updates its displayed information if a new valid member is selected.


With the audience manager's UI logic now in a much more scalable state, I began work on letting the player drag-and-drop audience members into their preferred arrangements. I thought for some time as to how I would implement this, as I was concerned about the issues that could arise from trying to use raycasts to try and get a member. Instead, I came up with a far simpler solution that would allow me to use Unreal's built-in UI elements.


My proposed system would simply place widget components in front of the audience members, each with a single button. These buttons would then call functions on the audience UI manager upon being hovered over, allowing the UI manager to know which audience member the player is currently trying to select. Then, the button would send another message upon being clicked, telling the UI manager which audience member should be dragged around. Upon the player releasing the mouse, the player's currently dragged audience member will move to another slot if the player releases the mouse on its coresponding widget button. I was able to start this new system, but didn't have time to finish it until the next sprint.


For sprint 5, I will finish the drag-and-drop system for the audience manager. I will also start to implement the unique behavior of each audience member and fully integrate the system into our game's shop. After that, we'll all be working on getting our game into a more polished state.




Full-Tilt: Sprint 3

October 8, 2026


For sprint 3, I spent the majority of my time developing the core foundation of our game's audience system. I developed a core audience manager blueprint to handle the buying and selling of audience members, a data asset to store the settings of our audience system without relying on per-instance variables, helper functions to assist with turning data from data assets into audience member actors, and a preview UI widget to let the player inspect their current audience members. I also helped import a plethora of assets from our artists, improved my generic shader to render the vertex colors of their assets, and improved the primary GameMode to track multiple pinballs at a time.


To start the sprint, I helped to improve our primary GameMode by allowing it to keep track of every active pinball actor at time. Previously, it would only track the number of active pinballs on the field, incrementing and decrementing when pinballs are created and drained. However, my teammate was working on developing the pinball variants, and some of these variants had behavior that changed based on the speed of the other active pinballs.


Since pinballs were already created and destroyed by methods in the GameMode, I simply created a set on it that contains all of the active pinballs. I used a set rather than an array since sets use hashes to store & index data. This makes it considerably faster than finding objects in an array, allowing the GameMode to find specific pinballs without needing to iterate over an array each time.


Updated pinball manager with code for tracking all active pinballs.
Updated pinball manager with code for tracking all active pinballs.

Next, I worked on importing all of new assets our artists made. Specifically, they created modular parts for a room and various models for most of the audience members. I first imported the meshes and textures for the modular room parts, since I was really interested in fleshing out the background of our game. I was able to quickly make a Packed Level Actor containing a small room with multiple lights, making for a more visually cohesive environment than what I came up with last sprint.


Packed level actor for arcade room.
Packed level actor for arcade room.

After the new room was fully set up, I imported all of the audience member assets into the project. While moving the assets into the project, I realized that many of the audience member assets I just imported also had vertex colors. I recognized this because of the lightweight model viewer I used on my PC called F3D, which actually renders vertex colors in the File Explorer file thumbnails.


F3D rendering vertex colors in File Explorer.
F3D rendering vertex colors in File Explorer.

Since the textures hadn't yet been completed for the models, I thought it would be a good idea to try and render these vertex colors instead for the time being. This was thankfully a very easy thing to set up; I just needed to make the generic material I made use the vertex colors as a tint. Like the other parameters, I made the vertex color tinting guarded behind a boolean to give material instances more control over whether they display the vertex colors.


New code in generic shader for handling vertex colors.
New code in generic shader for handling vertex colors.

While having many booleans in materials can lead to exponentially greater numbers of permutations (which can cause longer shader compile times), my master material hasn't yet approached the number needed to be truly concerning.


After adding the new shader code, I was surprised to see that there were many other previous models I imported that also had vertex colors. While these vertex colors will eventually be replaced with actual textures, they do a good job of bringing life to the previously flat colored models. It was surprising how much of an improvement this single change made to the entire game.


Audience members and pinball components rendered with vertex colors.
Audience members and pinball components rendered with vertex colors.

With the smaller tasks of the sprint done, I aimed my sights towards what I would be spending most of my sprint working on: the audience mechanic. In our game, the player can recruit audience members of different rarities in between rounds for a price. These audience members each have their own unique abilities and behaviors that change how the player plays through each round. These behaviors vary in complexity, from +1 starting pinballs to randomly making a single obstacle grant 10x points for that round.


To start creating the audience mechanic, I created an audience manager blueprint to handle the creation, destruction, and modification of audience member actors. Thinking back to my experience with DiveTime, I remembered how I handled settings/configurations for the game's managers: data assets. The data asset class is an Unreal Engine class that can be instantiated as an actual asset rather than just a runtime instance. This makes for a much more stable and reliable way to configure the behavior of actors at runtime without worrying about their configuration resetting due to changes in the actor's blueprint.


Blueprints and other code for audience configuration.
Blueprints and other code for audience configuration.

Instance of configuration data asset.
Instance of configuration data asset.

I quickly discovered that I could simply create data asset blueprints similarly to how I created data asset classes in C++ for the DiveTime game I previously worked on. After creating the blueprint, I got to work filling it with the variables needed to store all of the relevant settings for the audience manager system, including an array for each tier of audience member rarity (Common, Rare, Legendary). While I would make changes to this blueprint over time, it's core structure/layout remained mostly the same.


In order to make it easier for the audience manager to grab relevant data from the data asset, I created a couple helper functions within the data asset's blueprint. These helper functions dramatically reduced the amount of code needed on the audience manager to fetch data from the data asset.


Helper functions for configuration blueprint.
Helper functions for configuration blueprint.

Next, I worked on creating logic for adding and removing audience members from the field. Since the player can only have a maximum of 6 audience members at a time, I created an array whose size would be defined by a constant variable. This allows us to increase or decrease the maximum member count during development if we see fit. I also created an array of transforms to serve as the spawn points for the audience members. By checking a single box, these transforms are treated as actual components in the editor; They have a physical widget that can be selected and dragged around, making it much easier for level designers to adjust.


Parameter for widgets.
Parameter for widgets.
Moving spawn point using widget in editor.
Moving spawn point using widget in editor.

Continuing work on the add/remove logic, I created the two functions that the manager would use to purchase and sell audience members. These functions directly hook into the GameMode's currency and will spend/add to it as necessary. I even made it so that the audience manager's purchase function, when replacing an existing audience member with a new one, takes the sell price of the existing audience member into account when determining if the purchase can be made.


Code for purchasing and selling audience members.
Code for purchasing and selling audience members.
Test level buying audience member on victory.
Test level buying audience member on victory.

Finally, I worked on implementing the inspect UI for the audience manager. Prior to this, I hadn't really worked with Unreal Engine 5's widgets before. Learning this system took me some time, but I quickly realized that it was very similar to Unity's modern UI Toolkit. After plenty of trial and error, I was finally able to create a basic widget that would eventually let the player cycle through and focus on each member.


Widget for previewing audience members.
Widget for previewing audience members.
Early audience viewer test with amusing results.
Early audience viewer test with amusing results.

Since inspecting the audience members would be done during the intermission, I also had to adjust some of the code in the GameMode and the UI Manager blueprint that my teammate was working on previously. I ended up making the UI Manager hold an additional widget component that would be moved around through a dedicated inspect function, as well as adding logic to handle the transition between viewing audience members and viewing the intermission screen.


Code for preview widget's button behavior.
Code for preview widget's button behavior.
Modifications to UI manager for preview widget.
Modifications to UI manager for preview widget.
Player previewing each of their audience members.
Player previewing each of their audience members.

For the next sprint, I will finish developing the rest of the audience manager system and its UI components. I'll implement the unique behavior of each audience member and create logic to allow the player to rearrange their lineup of audience members as they see fit. I will also work with my other programmers to help connect the various features and mechanics we've been working on.



Full-Tilt: Sprint 2

September 24, 2026


For sprint 2, I worked on further establishing the core backend elements of our game. Specifically, I worked with my teammates to create a unified input manager and integrate additional gameplay functionality, made by a teammate, into the core GameMode blueprint. I also replaced the majority of our game's placeholder assets with models from our artists, created an animation system for the player's hands, and built and deployed our first prototype.


Before the second sprint started, our game was in a solid spot. While the playable levels were entirely made of placeholder assets and didn't have proper win/lose conditions, it was very much playable and started to actually feel like a real pinball machine. Our lead designer and producer set a deadline for us to get a prototype built.


While I mainly focused on the fundamental aspects of our game, I also wanted to start adding some visual flair for our first prototype. A game's aesthetic can play an important part in how capable that game is at immersing the player. Since one of our game's core selling points is immersion (physically emulating a real pinball machine), I felt it was reasonable to dedicate time to incorporating the many art assets our team had been working on.


To start off the sprint, I wanted to first create a centralized blueprint for user input. At the start of sprint 2, all of our controlled components simply used debug key press nodes to handle user input. While this worked for the time, it would have been rather messy to keep using this method of input handling. By having a dedicate class that other blueprints could listen to for user input, it would make the process of adding new input keys or adjusting existing ones considerably easier.


Fortunately, Unreal Engine 5 was designed with this conundrum in mind. They already have an established structure for how different aspects of a project should be integrated, including input. Unreal Engine 5's player controller blueprint is primarily designed to let players take control of a single pawn or character, but this can very easily be extended to directly controlling more than just a single pawn. This is handy for our game since it won't have controlled characters: their camera is instead fixed to a certain position, with user inputs instead controlling individual components.


I ended up making a custom player controller that exposes a plethora of delegates that other blueprints will be able to hook into. This prevents the player controller from needing to hold references to everything that might need player input while letting individual blueprints dictate whether they want to listen for player input.


Blueprint for player input.
Blueprint for player input.

With the input manager complete, I then moved on to configuring the player's animated hands. One of our animators had fully rigged and animated a set of hands during the last sprint, and I was responsible for implementing them into our game. My extensive experience with animation blueprints and animation blueprint templates from my last project, DiveTime, came in handy for this task.


Each hand was modeled and rigged separately, so I needed a way to share state logic between them while keeping their models and animations separate. By using an animation blueprint template, I was able to do just that; the core logic for pressing and releasing the buttons lived in the blueprint template, while the two hands were both children of the template blueprint with their own rig and animation overrides.


Logic for hand animation blueprint template.
Logic for hand animation blueprint template.
Hand animation blueprints made from template.
Hand animation blueprints made from template.

Once the animation blueprints were done, I then put both of them into a single hand blueprint. This blueprint automatically binds itself to the player controller I made previously while also having a boolean variable that toggled which hand model would be visible. This way, a level designer could simply drag and duplicate a single blueprint into their level and the hands would work without any further configuration.


Blueprint for hand.
Blueprint for hand.
Hands responding to player input.
Hands responding to player input.

Once I got done with the animated hands, I got a request from one of my level designers to help fix an issue with their menu scene. Their main issue was that the player's cursor simply disappeared, since my player controller was configured not to show it for the game scene. I remedied this by creating a custom GameMode and player controller specifically for UI only scenes, such as the main menu. This allowed me to have menu logic be entirely separate from game logic.


After helping my teammate with UI functionality, I then got to work replacing all of the template assets with the models that our artists had been making. While importing the models was easy, properly scaling them proved to be more difficult. It took a fair bit of trial and error, but I eventually replaced all of the placeholders with actual models.


Pinball components with proper models.
Pinball components with proper models.

Once I got done replacing the placeholder assets with actual models, I then needed to animate the camera's intro sequence; it starts looking at the coin door and then slowly moves up to above the pinball machine. My teammate had already implemented a blueprint for animating the camera's movement between the menu and the playfield, so I asked him if he would be ok with me adding the coin door animation to that same blueprint.


After coordinating with him, I was able to improve the camera blueprint by the hard-coded transition logic into a generic event with parameters for a target location and look-at point. This abstraction meant that we could also add other camera transitions in the future if we needed to.


Blueprint for optimized camera transition.
Blueprint for optimized camera transition.

With most of my cards done for this sprint, I started to work towards getting a built prototype for playtesting. To help our prototype feel more immersive, I spent some time making a simple arcade backdrop for the pinball cabinet. Unreal Engine 5 made it easy for me to quickly add some blue point lights and an enclosed room. While the lighting really helped to capture the more cramped feeling of an arcade, the floor's generic block texture wasn't doing the prototype any favors.


I remedied this by making a custom floor fabric texture inspired by what you'd expect to see in arcades. Even though it was somewhat hastily put together, it adds a lot more personality to a floor that previously felt out of place. For the back wall, I was able to make a simple wall material instance using default engine assets. With everything in place, the prototype felt reasonably polished with an established visual identity.


Textures I made for the alpha level.
Textures I made for the alpha level.
Intro cinematic for alpha level.
Intro cinematic for alpha level.

Before packaging the project, there was one final touch I wanted to add: a custom icon and splash screen. Since our artists hadn't made these specific assets yet, I just used some of their concept art instead. For our first prototype, I personally think it looked great.


Splash for Full Tilt's editor and game.
Splash for Full Tilt's editor and game.
First alpha build for Full Tilt.
First alpha build for Full Tilt.

Packaging the project into a standalone game was not without issue, but still fairly easy. The first time I tried to package the game, it failed to compile. Quickly skimming the output log file revealed the issue to be some redirector files that were confusing the compiler. After applying all of the redirectors in the project, the game packaged without issue. I then zipped the project and sent it to my producer through Google Drive.


With 2 more days left in the sprint, I decided to try and knock out one more feature before sprint 2 came to a close: the Roll Over component. This component is typically found in narrow passages on the pinball field, sticking up from a hole in the ground. When the pinball rolls over it, the Roll Over sinks into the hole and closes a switch, giving the player points.


Demonstration of how a roll over works.
Demonstration of how a roll over works.

Like the other components in our pinball game, I wanted this to be physically simulated. Thankfully, my teammate had already made a similar component that I could use as a base line: the spinner. The spinner blueprint has a single paddle bound to a physics constraint, letting it spin on contact with a pinball. A box trigger placed slightly above the spinner then detects when the spinner makes contact with it, granting points for each 180 degree rotation. All I needed to do was duplicate the spinner blueprint, constrain its rotation range, and move the box trigger below the spinner.


There were a few extra changes I needed to make as well, of course. Beyond repositioning the spinning component and replacing its model with the Roll Over's model, I also had to make the physical component constantly spring up. This was a bit tricky, since I also needed to make the Roll Over not impede the pinball; my teammates went to a pinball arcade and gave me some insights about the Roll Over specifically and how it didn't really slow the pinball. I remedied this by applying a downward force to the Roll Over when a pinball touches it, allowing the pinball to pass over while letting the Roll Over spring back up.


Blueprint for roll over component.
Blueprint for roll over component.
Ball rolling over run over.
Ball rolling over run over.

For the next sprint, I'm going to continue importing artist assets into our project and continue working on our game's backend architecture. I'll work on fleshing out the GameMode blueprint to handle multiple rounds, help my fellow programmers work on the shop systems, and assist the level designers with integrating the features my programmers and I have been working on. I might also start to work on the achievement system, as achievements will be the means in which players unlock certain components.



Full-Tilt: Sprint 1

September 10, 2026


Full-Tilt is a pinball simulator roguelike where the player must reach escalating score thresholds to advance to the next round. The player will earn money after each successful round that can be spent on new machine components, upgrades, and other modifications. The player will need to strategically design their pinball cabinet to maximize score output as each level scales in difficulty.


Michael Musante, our team's lead designer, created our game's concept and recruited a team of 13 consisting of artists, animators, a sounds designer, and programmers, including me. I will be the lead programmer for this project, and I will be responsible for overseeing and assisting in the development of our game's codebase. I will also be responsible for maintaining the GitHub repository and upholding a workflow that allows our 2 other programmers and 2 level designers to collaborate without merge conflicts.


My first task was to set up our project's repository and onboard our programmers and level designers onto it. While I have plenty of experience with this in the past, I decided that we would try a new workflow I thought would be beneficial for our project: Trunk Based Development. Trunk Based Development is, as defined by its dedicated website, "A source-control branching model, where developers collaborate on code in a single branch called ’trunk’* and resist any pressure to create other long-lived development branches by employing documented techniques."



Instead of developers having individual branches with long lifespans, developers instead create short-lived branches off the main branch, or 'trunk', and pull their work back to it within a very short time (a couple days at max). This frequent splitting and merging allows for rapid iteration across a large team, where merge conflicts are caught and dealt with almost immediately as they manifest between branches; day-long conflict resolutions after merging are no longer a concern when the volume of changes per pull request is only a few days of work rather than weeks or even months.


I specifically wanted to employ Trunk Based Development due to the merge conflicts I had with my last Unreal Engine 5 project, DiveTime. Everyone on the GitHub for that game had individual, long-lasting branches that were infrequently merged together, causing massive merge conflicts that took an entire day to resolve. So far, Branch Based Development has been easy to both teach my teammates and practice during development, without any signs of complications.


GitHub repository that I made for the project.
GitHub repository that I made for the project.

Outside of the GitHub, there was only one IT issue that I had to deal with. For some reason, their computers wouldn't recognize their own installation of Unreal Engine 5.8.2. When opening the project, it would prompt them to change the project's version to one labeled with a randomized string that vaguely looked like a GUID. I eventually figured out that their installations didn't properly add a Registry Editor key with a path to the Unreal Engine 5.8.2 installation. After adding this Registry Editor key to their computers, it's been smooth sailing since.


Registry key that was missing.
Registry key that was missing.

With the GitHub repository situated and teammates starting work on their own branches, I pivoted towards configuring Unreal Engine's physics to allow for our pinball simulation to behave like a real pinball machine. I quickly decided that the pinball cabinet and components will be to-scale with a real pinball machine. I found both reference footage for the ball's behavior and online blueprints for a pinball machine for the cabinet's size.


Since our modelers were still working on the cabinet's base layout, I needed to quickly make a test cabinet to compare against my references. I ended up finding a black & white wireframe for a pinball machine concept that someone made online. I removed the overhang parts on the blueprint and imported the edited photo into Adobe Illustrator, where I extruded the layout into a mesh of interconnected walls with bends and obstacles. The complexity was important to better emulate an actual pinball machine compared to an assortment of walls made from cubes.


Test cabinet made using Adobe Illustrator.
Test cabinet made using Adobe Illustrator.

Then, I created a quick and dirty cabinet blueprint that spawned a pinball blueprint on a regular interval. Almost immediately I noticed how quickly the ball would come to a crawl after exiting the launch chute. I quickly realized that the surface friction was the culprit; turning it off made the ball behave almost identically to the reference footage. However, it came with the unfortunate side effect of the ball's rotational inertia appearing completely out of sync with the ball's movement. It also caused the ball to have a gyroscopic effect, letting it 'climb' against angled surfaces.


While I wanted the simulation to be as physically accurate as possible, I came to the conclusion that this wouldn't be possible without considerable engine modifications. Unreal Engine 5's Chaos is simply not designed with these smaller and more intimate interactions in mind. Since the lowered friction really helped the simulation, I decided to simply lock the pinball blueprint's rotation. Instead, it rotates a visual ball mesh based on the direction and magnitude of its velocity.


Code for rotating ball towards velocity.
Code for rotating ball towards velocity.

While not physically accurate, the pinball performs closely to a real pinball in practice. Along with making the simulation asynchronous, forcing the pinball to use CCD collisions, increasing the simulation's tick rate to 120/second, and enabling physics sub-ticks, the pinball blueprint behaves almost identically to a real pinball. The physics engine adjustments also fixed an issue where the player's framerate directly impacted the pinball blueprint's physical behavior.


Pinballs moving across cabinets at varying angles.
Pinballs moving across cabinets at varying angles.

With the two largest tasks of the 1st sprint done, moved on to creating a plunger mechanism for the cabinet. I chose to make the plunger using a physics constraint rather than through kinematic motion, as it would play better into the adjustments I made to the engine's physics and the pinball itself. It also allowed for me to very easily make the plunger's force vary based on how far you retract it before letting go.

Code for handling plunger motion.
Code for handling plunger motion.
Plungers launching pinballs at different angles.
Plungers launching pinballs at different angles.

Next, I started to import many of the 3d models that our artists had made. I had gained a lot of experience with Unreal Engine 5's asset import pipeline from DiveTime, so I volunteered to do it again for this project. Since all of our models would also have textures, I took the time to make a generic material that each of the models will use through material instances. I even added a few static toggles for enabling/disabling the use of certain texture maps, squeezing out extra performance for models that may not need emissive maps, normal maps, mask maps, etc.


Assets imported along with new material.
Assets imported along with new material.

After the currently completed models were imported, I moved onto setting up the rail tube components. The player will eventually be able to place these in the cabinet, but I first needed to make them physically compatible with the pinball blueprint. By carefully adjusting the scale and collision settings of the rails, I got the balls to near effortlessly move into the ramp intake and travel through consecutive rail tubes.


Pinball moving through ramps.
Pinball moving through ramps.

Finally, I implemented a limited pinball system that kept track of the player's currently stored pinballs, active pinballs on the field, and drained pinballs out of play. I programmed this behavior in our game's GameMode blueprint, since these attributes will directly affect the game's current state. I also added helper functions for fetching a reference to the custom GameMode blueprint, checking how many pinballs there are of each state (stored, active, drained), and for spawning the pinball at a given location (as long as the player has at least 1 stored pinball).

Code for spawning a limited number of pinballs.
Code for spawning a limited number of pinballs.
Player only having 3 pinballs to shoot.
Player only having 3 pinballs to shoot.

For the next sprint, I will continue to develop the GameMode blueprint with additional core functionality and essential game states. I will add a fail state for when too many pinballs drain as well as a core input blueprint for all other game systems to hook into.

 
 

Related Posts

See All
Blog: New Clairvaux Vineyard Tour App

New Clairvaux Vineyard: Sprint 4 April 26, 2026 For our 4th sprint, I continued to refine our app's user interface using the UI Toolkit and C# scripts. I placed and filled in POIs on the map, created

 
 
bottom of page