Jake Rowland
Tools/Engine Programmer
Library_Log640x360.png

HaberDashers

Tiny Racers. Big World

Design by Saad Baloch

Design by Saad Baloch

Position

Lead Programmer

Team Size

57 Students

Engine

Unreal Engine 4

Time Frame

3 Months

Release

May 2020

Platform

PC


gameplay2.png
gameplay6.png

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.

gameplay1.png
gameplay3.png
 
 

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