David Talley

Using Gradle to build and version a UE4 project

Automated builds are a pretty important tool in a game developer's toolbox. If you're only testing your Unreal-based game in the editor (even in standalone mode), you're in for a rude awakening when new bugs pop up in a shipping build that you've never encountered before. You also don't want to manually package your game from the editor every time you want to test said shipping build, or to distribute it to your testers (or Steam for that matter).

Unreal already provides a pretty robust build system, and it's very easy to use it in combination with build automation tools. My build system of choice is Gradle, since I use it pretty extensively in my backend Java and Scala work. It's pretty easy to learn, runs everywhere, and gives you a lot of powerful functionality right out of the gate. This won't be a Gradle tutorial necessarily, so you can familiarize yourself with how Gradle works via the documentation on their site.

Primarily, I use Gradle to manage a version file in my game's Git repository, which is compiled into the game so that I have version information in Blueprint and C++ logic. I use that version to prevent out-of-date clients from connecting to newer servers, and having the version compiled in makes it a little more difficult for malicious clients to spoof that build number, as opposed to having it stored in one of the INI files. I also use Gradle to automate uploading my client build to Steam via the use of steamcmd.

Unreal's command line build tool is known as the Unreal Automation Tool. Any time you package from the editor, or use the Unreal Frontend Tool, you're using UAT on the back end. Epic provides handy scripts in the Engine/Build/BatchFiles directory to make use of UAT from the command line, namely RunUAT.bat. Since it's just a batch file, I can call it from a Gradle build script very easily.

Here's the Gradle task snippet I use to package and archive my client:

task packageClientUAT(type: Exec) {  
  workingDir = "[UnrealEngineDir]\\Engine\\Build\\BatchFiles"
  def projectDirSafe = project.projectDir.toString().replaceAll(/[\\]/) { m -> "\\\\" }
  def archiveDir = projectDirSafe + "\\\\Archive\\\\Client"
  def archiveDirFile = new File(archiveDir)

  if(!archiveDirFile.exists() && !archiveDirFile.mkdirs()) {
    throw new Exception("Could not create client archive directory.")
  }

  if(!new File(archiveDir + "\\\\WindowsClient").deleteDir()) {
    throw new Exception("Could not delete final client directory.")
  }

  commandLine "cmd", 
    "/c",
    "RunUAT", 
    "BuildCookRun", 
    "-project=\"" + projectDirSafe + "\\\\[ProjectName].uproject\"", 
    "-noP4", 
    "-platform=Win64", 
    "-clientconfig=Development", 
    "-serverconfig=Development",
    "-cook",
    "-allmaps",
    "-build",
    "-stage",
    "-pak",
    "-archive",
    "-noeditor",
    "-archivedirectory=\"" + archiveDir + "\""
}

My build.gradle file is in my project's directory, alongside the uproject file. This snippet will spit the packaged client out into [ProjectDir]\Archive\Client.

For the versioning, I have two files that Gradle directly modifies. The first, a simple text file, just has a number in it. In my [ProjectName]\Source\[ProjectName] folder, I have a [ProjectName]Build.txt file with the current build number in it. Additionally, in that same folder, I have a C++ header file with the following in it:

#pragma once

#define [PROJECT]_MAJOR_VERSION 0
#define [PROJECT]_MINOR_VERSION 1
#define [PROJECT]_BUILD_NUMBER ###
#define [PROJECT]_BUILD_STAGE "Pre-Alpha"

Here's my Gradle task that increments the build number in that text file, and then replaces the value in the header file:

task incrementVersion {  
  doLast {
    def version = 0
    def ProjectName = "[ProjectName]"
    def vfile = new File("Source\\" + ProjectName + "\\" + ProjectName + "Build.txt")
    if(vfile.exists()) {
      String versionContents = vfile.text
      version = Integer.parseInt(versionContents)
    }

    version += 1
    vfile.text = version

    vfile = new File("Source\\" + ProjectName + "\\" + ProjectName + "Version.h")
    if(vfile.exists()) {
      String pname = ProjectName.toUpperCase()
      String versionContents = vfile.text
      versionContents = versionContents.replaceAll(/_BUILD_NUMBER ([0-9]+)/) { m ->
        "_BUILD_NUMBER " + version
      }
      vfile.text = versionContents
    }
  }
}

I manually edit the major and minor versions and the build stage as needed, since they don't need to update with every build. You can include that header into any C++ file that needs to know the build number, and I also have a few static methods in my game's Blueprint static library that wrap them so I can get the version numbers in Blueprint.

Blueprint Image

I also have some tasks for automatically checking those files into the Git repository and committing them:

task prepareVersion(type: Exec) {  
  workingDir = project.projectDir.toString()
  commandLine "cmd", 
    "/c",
    "git",
    "reset"
}

task stageVersion(type: Exec, dependsOn: prepareVersion) {  
  workingDir = project.projectDir.toString()
  commandLine "cmd", 
    "/c",
    "git", 
    "add", 
    project.projectDir.toString() + "\\Source\\[ProjectName]\\[ProjectName]Build.txt",
    project.projectDir.toString() + "\\Source\\[ProjectName]\\[ProjectName]Version.h"
}

task commitVersion(type: Exec, dependsOn: stageVersion) {  
  workingDir = project.projectDir.toString()
  commandLine "cmd", 
    "/c",
    "git",
    "commit",
    "-m",
    "\"Incrementing [ProjectName] version\""
}

And here's the task I use to actually push it to Steam:

task pushBuildSteam(type: Exec) {  
  doFirst {
    println "Pushing build to Steam..."
  }

  workingDir = "[SteamworksDir]\\sdk\\tools\\ContentBuilder"
  commandLine "cmd",
  "/c",
  "builder\\steamcmd.exe",
  "+set_steam_guard_code",
  "[steam_guard_code]",
  "+login",
  "\"[username]\"",
  "\"[password]\"",
  "+run_app_build",
  "..\\scripts\\[CorrectVDFFile].vdf",
  "+quit"
}

You can also spit out a generated VDF file with the build number in the build's description so that it'll show up in SteamPipe. I have a single Gradle task I run that increments the build number, checks in those version files, packages both the client and server, and then uploads the packaged client to Steam. Another great thing about Gradle is that Jenkins has a solid plugin for it, so you can use Jenkins to set up a nice continuous integration pipeline for your game to push builds out regularly, which you absolutely should do if you're working with a team.

Requirements for any competitive multiplayer game

You've got this great idea for a competitive multiplayer game. Maybe you've even built a prototype and tested the core game loop extensively and found the fun. You think maybe you could just throw that hot potato up on Steam Early Access and make a buck or three. After all, good gameplay is ultimately what determines a game's success, right?

You're totally right! If the game's fun, you're 90% of the way there, and you could probably release and make some quick cash. The problem is that competitive games live and die by their daily active users (DAU), or how many unique players play over the course of 24 hours, and probably more important is their average player concurrency (APC), or the average number of players playing at any given moment. If you don't have players, you don't have competition. Competitive games are very different from, say, PvE games like Borderlands or Destiny, where you can get by with playing only with your friends. People crave competition, and they want to test their skills against similarly skilled players, and friends don't always scratch that itch. So, what can you do to help preserve your DAU and APC?

Time to Game

By far the most important thing you should be concerned with is time to game (TTG). How long is the window between the user launching your game and entering into a match? I've been in a Rocket League match within 30 seconds. Same with Overwatch. If it takes more than a minute on average to get into a game, you've done something wrong. This breaks down a little at high skill levels, but you should not be catering your game flow to professionals (which does not mean you shouldn't cater your game at all to professionals). It needs to be easy for casual Joe Shootyface to jump into a game and enjoy it, so that he can watch a Let's Play or Twitch stream of some amazing player doing their thang, and quickly act on whatever sense of inspiration that experience instills in him to practice what he learned. If the process of playing your game is tedious, people will settle for watching it, but if people are just watching it, your player base is already extinct.

The generic ways of lowering TTG that apply in all circumstances are pretty common sense:

  • Minimize required menu selections. It should not take more than 3 or 4 menu selections to join a matchmaking queue or join a server. Overwatch requires just 2 selections to jump into a match. Rocket League requires 4, which is about the limit. Rocket League has the benefit of not requiring you to perform any actions after a match is found. Overwatch only requires you to select a character.

  • Short load times. Minimize any loading required to display the main menu and to join a server or matchmaking queue. Save the bulk of the loading for entering a match, which is the point where someone is committed to playing your game. Even then, your levels should not take more than 30 seconds or so to load.

  • Responsive web services. If you have to hit a web service, do your best to make sure it's quick. Authentication, progression, friends, whatever. Favor quick response times over all. It should not take more than 5 or 10 seconds to authenticate a player and load whatever you need to load to access the main menu. If you need to load news, friends, or any other side data for the main menu, do it asynchronously.

  • Minimize or eliminate lengthy animations and movies. Don't show the Unreal Engine logo movie. Don't show a fancy studio logo animation your cousin made. There are legitimate reasons to have these, like it takes a minute or two to load into the main menu, in which case I refer you to the first point. If you do need to display things for legal reasons, make them easily skippable. You can display "Powered By" information on the main menu, or in a separate page off of the main menu, or display everything in one single pre-menu screen, instead of in multiple.

All of these are important to consider, but by far the largest contributor to TTG is grouping players together. This is where things can branch a little, and it depends entirely on the size of your player base. If you've yet to release and your community is a little small, your best bet is a server browser. Show a list of servers, show their ping, show how many players are in the server, and that's it. Name them something unique, and maybe players will form little subcommunities in their favorite servers. You should also give the player a "quick join" option that automatically joins a server from the server list based on some predefined criteria (player count and ping are most important here).


If you've got the concurrent players to support it (quadruple digits), matchmaking is the next step up. The most important thing to consider with matchmaking systems is that you need to minimize the number of queues or pools that players can join. The more queues you have in your matchmaking system, the more divided your player base is. If you've got 1000 concurrent players, but they're divided between 10 different pools, that only gives each group of players 99 other potential opponents. Ranked and unranked queues further divide your player base, so I don't recommend even supporting ranked queues until you're in the tens of thousands of concurrent players. That's not to say that your unranked queues can't still use a player rating system under the hood, and in fact both Overwatch and Rocket League do this, but the visible rank and rating is a psychological roadblock that some players don't want to overcome. In addition to minimizing the total number of queues, you should make the most popular queue stupid easy to join. Overwatch is the best example of this. The first button you're presented with after clicking Play is Quick Play which joins a single queue for the most played game type. Rocket League is actually a little cumbersome here and could benefit from a main menu option to, at the very least, automatically join the last set of matchmaking queues a player selected. You're trying to funnel players into the situation they'll have the most fun in, so be opinionated and forceful, and if you give them the option to customize their experience, don't make them do it every time they want to play. Finally, matchmaking within a single pool needs to be quick. Favor getting people into a game over getting them into the best possible game. When your matchmaking service finds a viable match, it should act on it immediately without some grace period to allow for a more appropriate match to maybe come along.

Tutorial and Gameplay Feedback

This can take many forms, but if you've got some common game mechanics across your entire game, you need a quick, and I mean quick, tutorial that players can run through to grasp the basics before heading online to play with or against others. Rocket League's basic tutorial only takes a few minutes to complete. Overwatch has an equally short tutorial, and overcomes the huge number of abilities with easy character-specific cheat sheets accessible from character select, and in-game to remind players of their abilities.

Another key thing a competitive game needs is visual and audio feedback whenever players execute specific actions. In Rocket League it's very visible when someone is using boost, or the dash button. The tutorial is a barebones introduction to gameplay, but players should be able to learn advanced techniques just by watching other more skilled players during a normal match, or when watching a Twitch stream. When a player presses a button, they should hear a sound, or see some visual effect happening on or around their character, so that they can identify that same action when used by someone else.

Dedicated Servers and Server Authority

Reducing latency for all players in a match is one of the easiest ways to improve user experience for everyone, and peer to peer doesn't cut it. You need dedicated servers distributed around the world so that you'll always have a low ping server for people to play on. With lower player counts you can do this pretty cheaply using Amazon's AWS or some other cloud provider. That said, dedicated server infrastructure can get pretty costly. One option to minimize cost is to actually distribute your dedicated server binary to end users and allow them to host dedicated servers on their own. There will always be some die hard fans with fat pipes and juicy hardware that can host a server or two.

Additionally, cheating is always going to be a major problem in online games, but you can drastically reduce the impact of cheating if your servers are authoritative over gameplay. Unreal includes a concept of authority, and allows you to specify that certain functionality can only be run on the server. This is another reason why peer to peer is awful for competitive games, because in a peer to peer situation, one of the clients is actually the authority over gameplay, which beckons cheating.

Quick Communication

Communication is huge in competitive games, but think about all of the times you've played an Overwatch or Rocket League match against strangers and how much you actually used voice chat? Anonymous communication tools are key here. Rocket League's quick chat is a great example, where most communication needs are met by saying "Defending..." or "In Position" or "I got it!" I would take it one step further and say voice packs would take quick chat to the next level, so that players don't need to look up from the gameplay to know what their teammate just said.

Another very useful feature to have is user-placeable waypoints or pings. Evolve had a pretty neat feature that let you point to anything in the world and ping it, and that ping would show up to other players and be color coded depending on what you were pointing at (white for generic, yellow for mob, red for enemy player). Strategy games have had this kind of functionality for a while, like StarCraft allowing players to ping things on the minimap for others. This isn't terribly useful in a game like Rocket League, but in games with large play areas, or where future positioning is very important and a player is able to pinpoint a specific location a teammate needs to be, being able to easily direct them on where to go is invaluable.

Replay and Spectator Tools

Twitch streamers are one of competitive gaming's best tools for exposure outside of marketing, but if streamers can only stream from their own point of view, you're limiting the action a viewer can absorb. Intuitive spectator controls that allow a streamer to jump from one player to the next, with simple information displays that give them the entire picture of the game are incredibly important for viewer experience. When someone watches a match, they want to see the action, which won't necessarily be focused on one individual player.

YouTubers are another potential area to gain new players. If you don't have a good set of tools for viewing replays of past matches, you're missing out on a huge opportunity. You need to allow people to frame your game in as epic a package as possible. Rocket League allows players to tweak camera angles and game speed in replays, which fueled a lot of the early game hype when people would make neat little gifs of their best plays and share them online.

Conclusion

That's a lot. I know. But all of it is important, and if you want to give your game the best chance of success, you need to keep all of it in mind. Developing, launching, and supporting an online competitive game is a huge task, but so is developing a game in general. The biggest thing I'd like you to take away from this article is that your game needs to be easy to learn, easy to play, and easy to watch.