A single player exploration-puzzle game where the player controls Soos, a frog servant, who is trying to purify the cursed temple.
Role & Responsibilities
As a gameplay programmer, I worked on following:
Rapid Prototyping: During early stages of development, I worked with level designers (LD) and game designer to quickly prototype Puzzle Elements and provided any sort of programming support they need.
Animation Programming: Worked with artists of the team to establish the animation pipeline and integrate it with the movement of the frog.
C++ Tools: Whenever Blueprint fell short, I ended up making tools with C++. Which got exposed as blueprint nodes for ease of use.
The feature list, below, contains the tasks I have worked on.
In La Rana, every interactable game element is called Puzzle Element. I prototyped and developed most of the puzzle elements.
( Chess Piece: It moves on the Rail, in the direction of the squirt )
( Collectible Orbs: When player squirts on them, it purifies the Finale Orb )
( Paired Blocks: Two blocks are connected with each other, they change their state on squirt )
( Water Pool: Player can drink water from here.. )
While working on puzzle elements, my mindset was to promote these three things:
Chainable activation of every puzzle elements
Generalized events for conveyance and audio
Ease of use for level designers
1. Chainable Activation:
The name Puzzle Element goes back to prototyping stage, where La Rana was supposed to be mainly a puzzle game. In this stage, our initial idea was that a player would activate a puzzle element and that action would lead to a chain of other puzzle activation. Thus one of the requirement was to have puzzle elements that can activate each other.
To give an example, when player squirts on a Blue Frog Collectible, it activates a Spline Tool which plays particle trail indicating the direction in which player should move, and then the frog collectible tries to unlock the Door.
( Locked Door: Can be unlocked when required number of blue Frog Collectibles are secured by player )
( Elements to Activate on Frog Collectible's blueprint )
2. Generalized events for conveyance and audio:
After PoCG, we realized that the game is more fun when it serves as an exploration game with easy-to-solve puzzles. For our game, it was clear that we would need solid conveyance and good audio feedback; both would need numerous iterations to make it fit into gameplay. Thankfully, our team had a good number of artists and LD(s), who can share the responsibilities.
While I was working on puzzle elements, I made sure that all the events, displayed in the table, would be exposed on the blueprint of each puzzle element.
( Activation Events on Puzzle Elements )
These are the screenshots of blueprints, showcasing how the events were used:
( On activation, update conveyance and start the audio )
( On deactivation, update conveyance and start the audio )
3. Ease of use for level designers:
I like providing easy-to-use tools without limiting its functionality. While working on puzzle elements, I did the same.
There were two tricks I used frequently:
Automating parameter initialization as much as I can
Exposing debug options for making work of LD intuitive
I will use the Lily Blossom and Lily Plant as an example. To setup a Lily Blossom, all you need to do is add layers on which the Lily Plants will move. Then drag all the lily plants you want in the scene, and select the Blossom as its parent. The Plant will automatically get snapped to the right position! If you want to move the whole setup, just move the Blossom and the Plant will follow.
( Lily Pads: When activated, it rotates around the Lily Blossom )
( Workflow of level designer, using my blueprints of the Lily Plant & Blossom )
As you can see, there's a checkbox to visualize the layers of Lily Blossom. If you click the checkbox, it enables debug lines indicating layers for few seconds.
When an LD adds a reference of Blossom to Lily Plant, it does all the following things in a construction script automatically. Which reduces the amount of work LD needs to do, as well as possibilities of human error.
( Lily Plan's construction script )
Same goes for the Locked Door – just check the Print Debug Info checkbox and all the useful info will appear on screen. This helps identify if frog collectibles are not connected properly, so that an LD will be able to easily bug fix.
( Locked Door printing the debug info )
( Checkbox: Print Debug Info )
I worked on following animations with artists to make the froggo feel alive!
( Idle )
( Hop )
( Jump )
( Water Fill )
( Squirt )
( No Water Bonk )
As you can see the frog has 6 different animations: Idle, Hop, Jump, Water Fill, Squirt, and No Water Bonk.
Since playtime of Hop and Jump animations can vary, both were broken down into three stages: take off, hover, and landing. To get this working, the key was to know how much time the frog has in the air, beforehand, when the jump/hop starts. I worked with Joe, other programmer who worked on movement of the frog, to get the time of flight from trajectory equation.
Once I get the "time until frog lands" or "time of flight", I can easily determine how long or short I should play hover animation, in a loop, to give take-off and landing animations the right amount of time to finish. I can also decide if I want to play long hover or short hover for the jump, from the time of flight.
( Animation Graph: Jump, Squirt & Water Fill )
( Animation Graph: Hop & Idle )
To convey the amount of water the frog is carrying, we decided to use his cheeks. To achieve varying size of cheeks, I ended up using BlendSpace 1D, where the size can be driven by a variable: fraction of water the frog is carrying. In order to make this work, artist provided two animations for each actions: one with fully inflated cheeks and a second with flat cheeks.
( BlendSpace 1D: Idle animation )
( BlendSpace 1D: Water Fill animation )
Hop and jump animations are driven by the frog’s movement, while actual squirting is driven by its animation. That means when the player press shoot/squirt button, the animation starts playing; but player can't squirt again until the animation is finished and is ready to be played. This decision was made for squirting to look good. It required communicating with game designer and artists so that everyone are on same page for the frequency of squirting.
You can see how it works in the blueprint screenshot given below,
( Actual shoot/squirt is driven by the animation )
( Finale cinematic shot )
I worked with Harrison, a level designer, on the finale cut scene. When the player finishes the game, a short cinematic starts playing, concluding the game. The shot is created using UE4's level sequencer and a cinematic camera. During this cut scene, four things happen:
The camera pans around
The frog moves to a fixed location so that it is guaranteed that he'll be visible in the shot
The rain begins
The background music changes
To move the camera, the transform is updated with the sequencer. Rain is enabled by turning on a particle effect at right time.
Moving the frog to final place was a little tricky: for sequencing something in UE4, it requires reference of the object when the sequence is getting authored, even before the game has started. It means the frog has to be in the editor beforehand. But in our game, spawning the pawn (frog) happens during run time through a script. So I used an empty actor as placeholder for the frog, while authoring the level sequence. By the time cinematic is played, a script would bind the frog pawn to that empty placeholder-actor. So, when the cinematic shot starts playing, the level sequence moves actual frog.
During this cut scene, a different audio track gets played. When it ends, the audio gets changed again, right before moving to the credits screen. For triggering audio tracks, I provided similar interface to our sound designer. The blueprint she was using works on event dispatchers: CinematicJustStartedPlaying & CinematicJustStoppedPlaying. It is pretty straight forward, except we had level streaming enabled: the finale stream in which Cinematic Player lives used to get loaded/unloaded dynamically. Whenever the stream gets loaded, its level blueprint will rebind event dispatchers to the background music player. This is how we made it work!
Firstly, aesthetics is the most expensive part about our game. Secondly, once player starts playing, the loading screen never shows up until the game is finished; because game's playtime is very short, about 25 minutes, we didn't want any loading screens during the whole gameplay session. For these two reasons, asynchronous level streaming was essential.
La Rana's map is consist of eight sub-levels, which we needed to load/unload according to where the player is headed. Out of these sub-levels, all Hub(s) and Finale levels has to be always loaded; mainly because of their visibility from other areas. The other four levels get loaded/unloaded dynamically.
( Map overview for level streaming )
The level loader takes two arguments in a form of level names' list. It can simultaneously load and unload these levels.
The blocker also takes a list of level names as an argument. When player tries to cross, it confirms that all the provided levels are loaded. If not loaded, it acts as an invisible wall and does not let the player pass through.
To achieve this goal, I made two Blueprints:
BPA_StreamLevelLoader: loads/unloads levels
BPA_StreamPlayerBlocker: blocks the player, if they try to move towards an unloaded area
( Level Loader: Exposed parameters )
( Player Blocker: Exposed parameters )
Tool: Telemetry Data (C++)
During alpha milestone, one of the requirements was to automatically export useful telemetry information to a CSV file, at the end of each gameplay session. These information is consist of playtime, frog collectibles' collection time, FPS, and the session's end time and date.
I provided following Blueprint nodes which were coded in C++ to achieve the goal:
When player finishes an area, this function gets called to note down time(s): when all the blue frog collectibles of that area were collected.
Just before the gameplay session is about to end, this function gets called to note FPS: min, max & avg
At the end, this function saves all the stored telemetry information to a CSV file.
You can see how these functions were used in following screenshots:
( Adding frog collectibles to telemetry data )
( Adding FPS to telemetry data & flush the telemetry data to CSV )
( Output: CSV file )
The output of all these, a CSV file ended up looking like the chart above.
Since this tool is one of the only two tools which got implemented in C++, I'm including its code here. It's pretty simple.
( Function declarations )
( Function definitions )
Tool: Controller Detection (C++)
We needed this tool for UI and menu, to indicate which buttons to press according to the input source currently being used: Xbox Controller or Keyboard-Mouse. It is as simple as detecting whether an Xbox Controller is connected or not and returning a boolean. This functionality was exposed via a custom blueprint node, which got used in UI and menu scripts.
( Usage of function: IsGamepadConnected )
( Implementation of function: IsGamepadConnected )
What went well?
We released the game on Steam.
Planning and scope were well-handled among programmers.
From my peer evaluations, it appeared that I was a positive force when the team's morale was low.
What went wrong?
During the early development period, the vision of the game was not concrete which lead to its unclear communication across the team.
Thus, we had to realign the vision right before jumping into the alpha sprint.
Getting the movement and animation right for a quadruped (frog) is very hard! It took some time to make it feel good.
What did I learn?
During prototyping stages of the development, it is very crucial to focus on the core loop and finding the secondary loop(s) of the game.
Not doing so could lead towards an unclear vision of the game, in later stages.
In a third-person game, the movement and its animation must serve a specific purpose; finding that purpose could take some time. An iterative approach is a right tool for such cases.