Unity

Development Update #31

Another fortnight, another blogpost! Rumor has it that Lord Manhammer had to change his name to “Lord Querkius”, and he’s knee deep in admin & branding work to make himself known throughout Barya again with his new moniker.

This means I get to hijack the development blog again and show you what we’ve been up to for the last two weeks! So let’s get right to it!

PIXEL ART GOODNESS

Team art have been working on pixel art all over the place, focusing on our campaign. Specifically our first campaign map to make it look as jaw dropping as possible. I for one, think they have done amazing progress, and if you stick around until the end of the post I’ll show you an amazing mood video that Patrik created.

AURELIAN CLERIC

Martí, one of our pixel artists, worked on finishing up our latest Loth troops from the animation sketches created by Emanu. The result is fantastic!

AurelianCleric.gif


AI IS GETTING BETTER

This is the more scary part of the blog post. Marcus improved the architecture to have the AI make smarter decisions. I’ll never win a game again!

We are not were we want to be, but we are getting closer. The changes we make now are mainly made to prepare for the things we want to do in the future.

However, we found and fixed why our AI seemed to have a death wish and always attacked the most difficult hostile around. It also didn’t recruit troops as fast as we wanted it to. All of that should be a bit better now.

FIXING BUGS

As all game development teams, we have bugs to fix. This sprint we have squashed quite a few. Most of them are difficult to show in a visual way. So I’ll highlight this clipping issue that Patrik solved earlier this week.

cliffclipping.gif

One bug was that our objects would clip into the ground at certain camera angles. Since our ground is 3D and our objects are billboarded 2D sprites, this is a fairly unique problem.

Patrik fixed this by doing the following changes:

  • Moved wielders slightly towards the camera

  • Hard cap at 55 degrees for all billboards

  • Reduced camera angle from 65 to 60

  • Unified the way we lean our landscape backwards for battle and adventure, and applied it to battle

Screenshot 2020-12-10 at 14.42.32.png

It’s one of those issues that looks easy to fix on the outside, but is actually very complicated. Above is another image of the fix from the side.

CAMPAIGN

All our teams have been working with a campaign focus lately. Carl is working in an iterative fashion to fine tune our maps for the Arleon campaign and is making sure that all the other campaigns will make sense story wise as he goes along.

Can’t show you much from this yet, but it is very exciting!

DIALOGUE SYSTEM & TRADING

A part of our campaigns need wielders to talk to each other - which is what I and Christian have worked on lately. A challenge was to make this work through our in game level editor, and we still have some work to do.

Robin is working currently on the ability to trade troops & artifacts between your wielders! It’s a feature we wanted for a long time and it will be great to be able to play with that soon.

BURNED VILLAGE

And finally, here’s the amazing video showing a bit of collected progress from the art team. I really think it looks stunning, what do you think?


Phew! That was more than I intended to share ;) Hope you’ll enjoy it! I need to get back to fixing the camera system for the dialogues now.

This is also the last blog post of the year. Take care and we’ll see each other in 2021!


Please note that this is a dev blog. Features and graphics mentioned or displayed above may or may not change during the development process.

Connecting Unity Cloud Build and HockeyApp (the slightly hackish way)

When we started our project at Lavapotion, we wanted an automated way to build our game to any platform we wanted as well as a seamless and easy way to give these builds to ourselves, stakeholders and testers.

With the number of great services that is available today, we decided to use Unity Cloud Build to build and then use a script that uploads all the successful ones to HockeyApp with the commit messages as the changelog.

Why Unity Cloud Build?

Unity Cloud Build got options to unit test, build and export to platforms like iOS, Android, MacOS and Windows. Which is all we need at the moment. They also have the options of choosing exactly what Unity Version to build from if you want, and they always have the latest as an option to.

It's super easy to connect with slack (for build notifications) and the setup is pretty straightforward. Disregarding that sometimessome builds fail for no reason, it's really everything we need at the moment.

Why HockeyApp?

Sure, you can share all your builds in Unity Cloud Build by creating a share link and send it to the people that needs it, but there's a whole manual process involved there and no easy way to restrict downloads to certain groups.

HockeyApp got distribution groups that you can add/restrict to any builds pretty much whenever you want. You can also notify people with an email with a changelog when a new build is ready. It's neat.

Connecting the two platforms. Why do we need it?

The obvious way to connect the services is to do it manually everytime. To break it down into a step-by-step instruction - take a look at the list below.

  1. Log into Unity Cloud Build
  2. Download all the successful builds for the platforms you want to upload to HockeyApp to your computer
  3. Log into HockeyApp
  4. Choose the app that's on the platform you want to update.
  5. Upload a new version
  6. Write/Copy-Paste the changelog
  7. Choose the Distribution Groups that should have access to this specific build.
  8. Decide if they should get an email notification.
  9. Repeat steps 4-8 for every platform you would like to update.

A philosophy that we strongly hold at Lavapotion is to reduce all the "screw-up-able" steps that can occur in our process and automate as much as possible. Not only is the list above extremely time consuming, but almost every step is error prone to all kinds of mistakes because there are so many manual steps.

So as a first step, we decided to reduce the manual steps to the following when there's a successful build.

  1. Run a script.

That looks way better, doesn't it? ;)

Setting everything up

You will need to do the following before we start:

  • Your project in a Git repository (for the automatic changelog)
  • Install Node Package Manager on your computer
  • Setup your project in Unity Cloud Build
  • Setup your applications in HockeyApp

The script we're creating will be written in Javascript and using a gulp job to perform the actual tasks of downloading and uploading, so we will need to setup the node project first.

Create a file called package.json at the root of your project and fill it with the following: 

(Replace the placeholder with your own information.)

{
  "name": "Super secret project",
  "description": "The best super secret game in the world.",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "gulp": "node ./node_modules/gulp/bin/gulp.js"
  },
  "repository": {
    "type": "git",
    "url": "<YOUR GIT URL>"
  },
  "author": "<YOUR COMPANY NAME>",
  "license": "<YOUR LICENSE>",
  "private": true,
  "devDependencies": {
    "gulp": "^3.9.1",
    "gulp-download": "0.0.1",
    "gulp-hockeyapp": "^0.2.4",
    "gulp-rename": "^1.2.2",
    "request": "^2.80.0",
    "simple-git": "^1.67.0"
  }
}

This will setup a simple npm project. The devDependencies object explains what modules we will use in our scripts. The "gulp" part in scripts is optional - it's added so that we can easily use the locally installed version of gulp rather than a global one.

To install all of the devDependencies, open the command prompt (or terminal) and locate yourself at the root of the project and run the following command:

D:\YOUR_PROJECT_PATH>: npm install

After everything is installed, you can create a file called Gulpfile.js in your project root.

Gulp!

Given that your project is setup in Unity Cloud Build and HockeyApp, you can setup your Gulpfile.js like this and change the placeholders to your specific settings. This example will use an android build to upload.

'use strict'
var gulp = require('gulp');
var gulpDownload = require('gulp-download');
var rename = require('gulp-rename');
var request = require('request');
var git = require('simple-git');
var hockeyApp = require('gulp-hockeyapp');

var cloudBuildSettings = {
    apiKey: "<YOUR_CLOUDBUILD_API_KEY>",
    organizationID: "<YOUR_CLOUDBUILD_ORGANIZATION_NAME>",
    projectName: "<YOUR_CLOUDBUILD_PROJECT_NAME>"
};

var hockeyAppSettings = {
    apiKey: "<YOUR_HOCKEYAPP_API_KEY>",
    androidDevKey: "<YOUR_HOCKEYAPP_APPLICATION_ID>",
    devTeamID: <YOUR_TEAM_ID>
};

var tasks = {
    hockeyApp: 'hockeyapp',
};

var paths = {
    dist: 'dist/'
}

Downloading information about the latest build

Before doing anything, we need to be able to download the build information of the last successful build on Unity Cloud Build so that we can download it to our computer.

Luckily, Unity got a REST API that we can use to do this that is well documented.

Using the "List all builds" method, we can fetch the information that we need. Add the following function to your Gulpfile.

function downloadBuildInformation(pBuildTargetID, pOnCompleteCallback) {
    var baseURL = 'https://build-api.cloud.unity3d.com/api/v1';
    var options = {
        url: baseURL + '/orgs/' + cloudBuildSettings.organizationID + '/projects/' + cloudBuildSettings.projectName + '/buildtargets/' + pBuildTargetID + '/builds?buildStatus=success',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Basic ' + cloudBuildSettings.apiKey
        }
    };
    request(options, function (error, response, body) {
        var latestBuild = null;
        if (!error) {
            var builds = JSON.parse(body);
            var latestDate = new Date(-8640000000000000);
            for (var i = 0; i < builds.length; ++i) {
                var currentBuild = builds[i];
                var finishedDate = new Date(currentBuild.finished);
                if (latestDate < finishedDate) {
                    latestBuild = currentBuild;
                    latestDate = finishedDate;
                }
            }
        }
        else {
            console.log('Failed to get build information! message: ' + error);
            pOnCompleteCallback();
        }
        if (typeof (pOnCompleteCallback) !== 'undefined' && pOnCompleteCallback != null) {
            pOnCompleteCallback(latestBuild);
        }
    })
}

This function will download the successful builds available and return the newest one in the complete handler once done.

Downloading the build

After we get the information, we need to download the build to our computer. The build will be downloaded with a npm module called gulp-module. When the file is downloaded, we will return the file name in the complete handler.

Add the following function to your Gulpfile:

function downloadLatestBuild(pBuildInfo, pOnCompleteCallback) {
    if (pBuildInfo === null) {
        throw 'Cannot download build! Information is null!'
    }
    var latestDownloadFileName = cloudBuildSettings.projectName + '_' + pBuildInfo.buildtargetid + '.' + pBuildInfo.links.download_primary.meta.type;
    gulpDownload(pBuildInfo.links.download_primary.href)
        .pipe(rename(latestDownloadFileName))
        .pipe(gulp.dest(paths.dist))
        .on('end', function () {
            console.log('\nFinished Downloading Build\n');
            pOnCompleteCallback(latestDownloadFileName);
        });
}

Creating the change log

Since HockeyApp supports uploading a change log in markdown, we wanted a quick look on what's been done since the last build. At this point we don't need any curated or filtered logs, so we can just use the git commit logs for now (given that our commit messages are properly written).

To do this, you can add the following three functions to your gulp file. One takes the information available from unity cloud build and the other two gets information from your git repository and retrieves the commit messages.

function createMarkdownReleaseNotes(pBuildInfo, pOnCompleteCallback) {
    if (pBuildInfo === null) {
        throw 'Cannot get changelog! Information is null!'
    }
    var markdownChangelog = '#' + cloudBuildSettings.projectName + ' ' + pBuildInfo.buildTargetName + '\n';
    markdownChangelog += '**Branch:** ' + pBuildInfo.scmBranch + '\n';
    markdownChangelog += '**SHA1:** ' + pBuildInfo.lastBuiltRevision + '\n\n';

    var ignoreCommitIds = [];
    if (pBuildInfo.changeset.length > 0) {
        markdownChangelog += '## Recent Changes \n\n';
        markdownChangelog += 'If installed, this build will: \n';
        for (var i = 0; i < pBuildInfo.changeset.length; ++i) {
            var message = pBuildInfo.changeset[i].message;
            markdownChangelog += '* ';
            markdownChangelog += message;
            markdownChangelog += '\n';
            ignoreCommitIds.push(pBuildInfo.changeset[i].commitId);
        }
    }

    var gitLogSettings = [];
    var maxHistoryLogs = 50;
    gitLogSettings.push(pBuildInfo.lastBuiltRevision);
    gitLogSettings.push(pBuildInfo.scmBranch);
    getGitLog(gitLogSettings, function (pLogArray) {
        markdownChangelog += buildChangelogHistoryFromGitArray(pLogArray, maxHistoryLogs, ignoreCommitIds);
        pOnCompleteCallback(markdownChangelog);
    });
}

function buildChangelogHistoryFromGitArray(pLogArray, pMaxHistoryLogs, pExcludeShaArray) {
    var changeHistory = '';
    if (pLogArray.length > 0) {
        changeHistory += '\n## Change History \n\n';
        for (var i = 0; i < pLogArray.length; ++i) {
            var message = pLogArray[i].message;
            if (pExcludeShaArray.indexOf(pLogArray[i].hash) === -1) {
                changeHistory += '* ';
                changeHistory += message;
                changeHistory += '\n';
            }
            if (pMaxHistoryLogs !== -1 && i >= pMaxHistoryLogs) {
                break;
            }
        }
    }
    return changeHistory;
}

function getGitLog(pGitLogSettings, pOnCompleteCallback) {
    git().log(pGitLogSettings, function (err, log) {
        var logArray = null;
        if (!err) {
            logArray = log.all;
        }
        pOnCompleteCallback(logArray);
    });
}

Uploading the build to HockeyApp

To upload the build to HockeyApp, we are using a package called gulp-hockeyapp. It does exactly what we need and you can also decide whether or not you should send an email notification to your HockeyApp distribution groups.

Add this function to your Gulpfile.js that will use all of our previous functions and once everything is done, upload the build to hockeyapp.

function downloadAndUploadToHockeyApp(pBuildTargetId, pHockeyAppID, pOnCompleteCallback, pNotifyAll, pTeams) {
    downloadBuildInformation(pBuildTargetId, function (pBuildInfo) {
        downloadLatestBuild(pBuildInfo, function (pDownloadFilename) {
            createMarkdownReleaseNotes(pBuildInfo, function (pMarkdownChangelog) {
                var options = {
                    id: pHockeyAppID,
                    apiToken: hockeyAppSettings.apiKey,
                    inputFile: paths.dist + pDownloadFilename,
                    notify: pNotifyAll === true ? 2 : 0,
                    status: 2,
                    teamList: pTeams,
                    notes: pMarkdownChangelog,
                    notes_type: 2
                };
                console.log('\nUploading to hockeyapp...\n');
                hockeyApp.upload(options).then(
                    function (response) {
                        console.log("Successfully uploaded build to HockeyApp!");
                        pOnCompleteCallback();
                    },
                    function (err) {
                        throw err;
                    }
                );
            })
        });
    });
}

Creating the Gulp job

By now, we have all the functions that we need. The only thing left is to create a gulp job that uses the function that we just created.

Add the following gulp task to your Gulpfile:

gulp.task(tasks.hockeyApp, function (done) {
    var notifyByEmail = false;
    downloadAndUploadToHockeyApp('android-dev',
        hockeyAppSettings.androidDevKey,
        done,
        notifyByEmail,
        [hockeyAppSettings.devTeamID]);
});

That's it! To run this, just use the following command at the root of your project in the command prompt or terminal to set the job in motion:

D:\YOUR_PROJECT_PATH>: npm run gulp hockeyapp

Ending Notes

As the title suggests, there is much room for improvement with this script. You could split the methods into several files, create unit tests, add better error handling or even use a build computer with jenkins to automatically poll when a new build is successful and run everything automatically.

But at this moment, this is good enough for us. We removed some manual steps and saved everyone in the team some time. We hope that this can be of use to some of you as well!

If you need to have a look at the Gulpfile in it's entirety, you can find it here.