Position
Lead Programmer
Team Size
57 Students
Engine
Unreal Engine 4
Time Frame
3 Months
Release
May 2020
Platform
PC
About
HaberDashers is a console-style arcade (kart) racer for the PC in which up to four players control miniature humanoid inhabitants of an everyday home, racing past outsized household items and through rooms as they compete against human and AI opponents with both driving skills and item pick-ups.
This game was developed by 57 students during one semester at SMU Guildhall. Over the course of the semester, we went through the process from concept, to vertical slice, to alpha and beta, with a transition to virtual development halfway through. We finally finished early May with the game now available on Steam.
Leadership
As part of the leadership team working on HaberDashers, I was tasked with directing the developers on our team in the correct direction. As the lead software developer, the proof of concept: technology (PoCT) was the first hurdle the team and I had to make. I immediately split up some developers to tackle the biggest unknown, kart control. The desire was to get as many different solutions as quickly as possible then choose the one that, going forward would be the best. Further down I talk about the specifics of the choices. Along with sorting out the technical unknowns, I was responsible with high level feature and milestone planning
Milestone planning
Stakeholder negotiations
Feature decomposition and dependency tracking
Managing tasks for 15+ programmers
Kart Movement System Design
When developing the movement system, I initially split up the kart team into three separate groups to each flash hack a different system. Some of the systems investigated were Unreal Engine 4’s physics system, and a completely custom built speed, direction and orientation control system. The goal was to determine the best system for our needs. In the end we choose a combination of using the UE4’s physics system for some things, and custom control logic for rotations, orientations, and kart body height. This gave the designer very fine control over the look and feel of the kart. Decoupled the karts physical movement system from the visuals to allow for more cartoony designs and feelings.
Delegated the prototyping of three distinct movement systems
Decoupled the physical movement system from the visual animation system
Worked between programmers and artists to implement animations and functionality
High Level Architecture
I, along with some of the backend developers, worked together to develop a high level system and information diagram that allowed for the efficient data and command structure for the game. This includes which modules informed, instantiated, and were owned by other modules. This allowed the programmers delegated to these systems to quickly understand the complete coupled game system and where information and directions would come from.
Developed the high level overview of our game’s modules
Worked with other programmers to determine the best design
Refined the design as features dictated
Nightly Build System
As the lead programmer, I was tasked with the dev ops side of the project as well. This included the creation, and distribution of the current latest build of the game. Previous cohorts had used batch scripts for automation but I developed a system in python that managed and ran the entire build pipeline. This systems was designed with configuration in mind to allow following cohorts to use this with little need for modifications
Python based pipeline for Unreal Engine 4 builds
Configuration system to handle multiple environments
Asynchronous source control retrieval
File logging for debugging and monitoring
Get Latest From Source Control
import time
import threading
from P4 import P4
from . import Environment as env
# Threaded P4V Sync function
def p4_sync():
global synced_files
synced_files = p4.run( 'sync', '-f' )
return synced_files
# Threaded P4v Sync callback
def p4_sync_callback( synced_files_from_p4 ):
global files_synced
synced_files = synced_files_from_p4
files_synced = True
#
threaded_callback_lock = threading.Lock()
class Threaded_Callback (threading.Thread):
def __init__(self, thread_id, function, callback):
threading.Thread.__init__(self)
self.thread_id = thread_id
self.function = function
self.callback = callback
def run(self):
returnValue = self.function()
threaded_callback_lock.acquire()
self.callback( returnValue )
threaded_callback_lock.release()
p4 = P4()
synced_files = {}
files_synced = False
game_name = env.get_env_variable('Game', 'game_name')
def update_from_P4( log_file ):
log_file.write( '----------------------------------------------------------------------------------------------------\n' )
log_file.write( '{} - Step 1: Update the local workspace for P4\n'.format( game_name ) )
log_file.write( '----------------------------------------------------------------------------------------------------\n' )
log_file.flush()
# Perforce Settings
p4.port = "129.119.63.244:1666"
p4.user = env.get_env_variable('Perforce', 'user_name')
p4.password = env.get_env_variable('Perforce', 'user_password')
p4.client = env.get_env_variable('Perforce', 'client')
# Connect to the perforce server
success = p4.connect()
log_file.write( str(success) )
log_file.write('\n')
log_file.flush()
p4_thread = Threaded_Callback(1, p4_sync, p4_sync_callback)
p4_thread.start()
start_time = time.time()
while( not files_synced ):
pass
log_file.write( 'Completed Perfoce Sync in {:.2f} seconds\n'.format( time.time() - start_time ) )
log_file.flush()
files_updated = 0
files_deleted = 0
for file in synced_files:
if file['action'] == 'refreshed':
continue
if file['action'] == 'deleted':
files_deleted += 1
continue
files_updated += 1
update_message = str(files_updated) + ": "
relative_file_name = file['clientFile']
name_loc = relative_file_name.find( game_name )
relative_file_name = relative_file_name[ name_loc + len(game_name):]
update_message += relative_file_name + " ( "
update_message += file['rev'] + " ) - "
update_message += file['action']
log_file.write(update_message)
log_file.write('\n')
if files_updated % 100 == 0:
log_file.flush()
if files_deleted > 0:
log_file.write( '{} files marked for deleted in total\n'.format( files_deleted ) )
if files_updated == 0:
log_file.write( 'All files are current\n' )
log_file.flush()
return True
Build The Game EXE
import subprocess
from datetime import datetime
from . import FileUtils as file_utils
from . import Environment as env
game_name = env.get_env_variable('Game', 'game_name')
builds_dir = env.get_env_variable( "Game", "builds_dir" )
def build_game( log_file ):
log_file.write( '----------------------------------------------------------------------------------------------------\n' )
log_file.write( '{} - Step 4: Starting BuildCookRun\n'.format( game_name ) )
log_file.write( '----------------------------------------------------------------------------------------------------\n' )
log_file.flush()
uproject_file = env.get_env_variable( "Game", "uproject_file" )
ue4_batchfiles_dir = env.get_env_variable( 'Local', "ue4_batchfiles_dir" )
ue4_binaries_dir = env.get_env_variable( 'Local', "ue4_binaries_dir" )
result = subprocess.run( [ ue4_batchfiles_dir + 'RunUAT.bat', "BuildCookRun", "-project=" + uproject_file, "-noP4", "-nocompile", "-nocompileeditor", "-installed", "-cook", "-stage", "-archive", "-archivedirectory=" + builds_dir, "-package", "-clientconfig=Shipping", "-ue4exe=" + ue4_binaries_dir + "UE4Editor-Cmd.exe", "-pak", "-prereqs", "-nodebuginfo", "-targetplatform=Win64", "-build", "-CrashReporter", "-utf8output" ], stdout=log_file )
log_file.flush()
return result.returncode == 0
def zip_build():
latest_build_dir = env.get_env_variable( "Game", "latest_build_dir" )
now = datetime.now()
now_str = now.strftime( "%m_%d_%H_%M" )
file_utils.zip_file_directory( latest_build_dir, builds_dir + game_name + "_" + now_str + ".zip" )
Upload to Steam
import subprocess
from . import Environment as env
game_name = env.get_env_variable('Game', 'game_name')
def upload_to_steam( log_file ):
log_file.write( '----------------------------------------------------------------------------------------------------\n' )
log_file.write( '{} - Step 5: Starting Upload to Steam\n'.format( game_name ) )
log_file.write( '----------------------------------------------------------------------------------------------------\n' )
log_file.flush()
user_name = env.get_env_variable( "Steam", "user_name" )
user_password = env.get_env_variable( "Steam", "user_password" )
steam_dir = env.get_env_variable( "Steam", "steam_dir" )
steam_cmd = env.get_env_variable( "Steam", "steam_cmd" )
app_build = env.get_env_variable( "Steam", "app_build" )
result = subprocess.run( [steam_dir + steam_cmd, "+login", user_name, user_password, "+run_app_build_http", steam_dir + app_build, "+quit"], stdout=log_file )
log_file.flush()
return result.returncode == 0
if __name__ == '__main__':
upload_to_steam()
Retrospective
What Went Well
Transition to Virtual
Planning and communication
Get it on screen and iterate
What Went Wrong
Understanding scrum in a large setting
Understanding team and sub-team roles
Only in build bugs
Trickle down information
Even Better If
Improve accurate time tracking
Improved communication with the stakeholders
Improved communication with teams regarding schedule
Improved QA process