Just add Lua

One of my requirements for the Altair project is to make it as moddable as possible. To that end, I decided that the base game itself should be implemented as a mod in order to ensure that modders would have as much support as I could possibly give them. My first step down that road was to embed a Lua interpreter and create abilities in Lua rather than hard coding them in C#. This effectively makes abilities data and the data is executable code which makes it incredibly powerful.

Below is my first draft of what an ability script written in Lua might look like. This script does technically run at the moment but ends up spitting out console messages when making the calls to the game object rather than actually doing anything useful.

Note I have never written anything in Lua before so if I’m doing something very wrong, please feel free to let me know in the comments.

--[[
init is called when the ability is first initialized and should setup the provided
ability object so that the UI knows how to make it available to the player

parameters:
	ability		the ability's setup object that should be modified
--]]
function init (ability)
	ability.Id = "base.selfdestruct"
	ability.Name = "Self Destruct"			-- name to display for this ability
	ability.Icon = "self_destruct.png"		-- the image to use as the icon for this ability
	ability.Description = "Destroy the selected ship and damage all surrounding ships."
											-- description to be displayed for this ability
	ability.Source = "PlayerShip"			-- ability can be activated only from the current player's selected ship
	ability.Target = "PlayerShip"			-- target the current player's selected ship for this ability
	ability.PowerCost = 100					-- cost in power to activate this ability
	ability.Cooldown = 0					-- the number of turns before this ability can be activated again
end

--[[
activate is called when the ability is activated and is where the ability's effect should occur

parameters:
	game		object used to sense and affect game state
	source		the object that triggered this ability
	targets		a table of the target objects for this ability
--]]
function activate (game, source, targets)
	range = 100		-- range of the explosion
	damage = 100000	-- damage to apply to any ships in range of the explosion

	-- since this is a single target ability, use targets[1] to select the first target from the table
	ships = game.GetShipsInRange(source.Position.Position, range)

	if ship ~= nil then
		for i = 1, #ships do
			game.ApplyDamage(ships[i], damage)
		end
	end

	game.ApplyDamage(source, damage)
end

At game start, Altair parses the abilities folder for Lua files and executes the init function in each script which is responsible for setting up the ability in the engine. The ability is a dead simple object that exposes the properties set in the init function along with a reference to the script object itself so that I can execute it later.

So how do we execute Lua code in Unity? I’m glad you asked! I stumbled across the MoonSharp project a while ago and decided to give it a shot for this implementation.

Calling in to the activate function in the Lua script looks like this:

public override void Execute() {
	var Abilities = AbilityLoader.GetAbilities();

	var ability = Abilities[AbilityId];
	var pGame = UserData.Create(GameField);
	var pSource = UserData.Create(new Ship {Id = "Ship1"});
	var ships = new List<Ship> {new Ship {Id = "Ship2"}, new Ship {Id = "Ship3"}};
	var pTargets = UserData.Create(ships);

	ability.Script.Call(ability.Script.Globals["activate"], pGame, pSource, pTargets);
}

This code is mostly just temporary while I’m testing out the integration but I’ll walk you through it anyway. The AbilityLoader is the code responsible for parsing the abilities and executing the init method but after that its this bad boy that calls the activate function in the script whenever the ability is activated.

As described in the comments of the Lua script, the parameters are; the GameField object which allows the script to sense and affect the game, the source ship that triggered this ability and finally the targets for this ability.

Just for the sake of completion, this is the LoadAbility method that’s doing the script initialization.

private Ability LoadAbility(string path) {
	var ability = new Ability();
	ability.Script = new Script();
	ability.Script.Options.DebugPrint = s => { Debug.Log(s); };
	UserData.RegisterType<Ability>();

	var scriptText = File.ReadAllText(path);
	ability.Script.DoString(scriptText);
	var pAbility = UserData.Create(ability);
	ability.Script.Call(ability.Script.Globals["init"], pAbility);

	return ability;
}

That guy there is calling the init function in the Lua script and passing it an Ability object that the Lua script populates and then I can store the whole thing for later.

That’s pretty much it. Integrating MoonSharp was pretty trivial.

This has of course brought a large portion of the rest of my codebase in to question as my game field isn’t actually smart enough to make this work properly yet, so I’m looking at paying down some technical debt in the not too distant future to get this all working the way I’m envisioning it should.

Overall I’m quite pleased with this approach although I am a little concerned that this is going to cause me grief with some platforms that demand AOT compilation, but one problem at a time right? :)

The Input Manager

ship
SHip/Submit
verb
past tense: shipped; past participle: shipped
1. transport (goods or people) on a ship.
“the wounded soldiers were shipped home”
2. (of a boat) take in (water) over the side.
3. zip up a build and save it on a server without sharing the link with anyone.

I just shipped version 0.1.8-alpha of Altair as per the 3rd definition above. I think it’s important to clearly define for myself what it means to actually ship a version of Altair. At some point I’ll start sharing this with people. IPromise toShare = new Promise();

What’s new?

  • Input manager to control which input contexts get a chance to process input in any given frame
  • Attack order which allows targeting a ship and a basic laser animation followed by an explosion!
  • Attacks are limited in range and will also account for obstructions. Example: the right-most ship targets the left-most ship and fires but hits the middle ship because.. wrong place wrong time.
    0.1.8-alpha_setup

The most interesting challenge with this release has been the implementation of the input manager to enable more control over user input.

The original input handling was trivial as it relied solely on the built-in OnMouseUpAsButton method baked in to every GameObject. This worked great as long as I was only concerned with a single input action which was Select Ship. The Move Ship action was baked in to the movement arc view which worked great because the select ship code only kicked in to effect if you were clicking on a ship, not empty space.

Of course to implement combat, I needed to be able to select a ship and then select its target without losing the current ship selection. I had a pretty good idea as to how to pull this off but went looking for examples anyway.

It is my hope that one day a post on this blog will prove to be as useful to someone as this post was useful to me.

My input manager is quite a bit simpler than the above linked implementation but it certainly confirmed that I was on the right track.

The input manager in Altair is implemented with a couple of pretty trivial interfaces:

public interface IInputManager {
    void Update();
    void Push(IInputContext context);
    IList<IInputContext> Peek();
    void Remove(IInputContext context);
}

public interface IInputContext {
    Handled Update();
    ContextPriority Priority { get; set; }
}

It’s basically a wrapper for a list masquerading as a stack. On each frame, InputManager.Update() is called which simply iterates over the highest priority input contexts that it knows about and lets them do what they need to do.

There are a couple of neat things in this implementation that I want to call out such as the ContextPriority. I decided that I needed to be able to have more explicit control over where an input context would live in the stack rather than just pushing it on to the top. The ContextPriority is an enum that I’m casting to an integer value which is used to order priorities any time one is added to the queue. If no priority is provided, then it is given the current highest priority value +1 which is basically intended to allow me to make overlays or something similar later.

A case that I had to account for with the ContextPriority bit in play was the potential for duplicate priorities. I solved this by allowing a list of input contexts to exist at any priority. The actual declaration for that data storage looks like this:

SortedList<int, List<IInputContext>>

The int is the priority which then keys in to a list of input context objects. All input context objects within a given priority are treated as equal and are given the opportunity to handle input regardless of whether a sibling input context has returned a true value for Handled.

That’s another neat thing that I don’t see enough of when people are writing code in C#. C# allows us to create rich data types that make things easier and safer to use. Take a look at the return value of IInputContext.Update() for example. It is of type Handled. It could just as easily be a boolean as that’s basically how it’s used, but check out this implementation:

public struct Handled {
    public bool IsHandled { get; private set; }
    public Handled(bool isHandled) {
        IsHandled = isHandled;
    }
    public static implicit operator Handled(bool isHandled) {
        return new Handled(isHandled);
    }
    public static implicit operator bool (Handled handled) {
        return handled.IsHandled;
    }
}

That little bit of code lets me have a nice type-safe object that happily converts itself to a bool as required to support my common use case. I use this technique in several places throughout the codebase so far. A demonstrably more useful place for this technique is the ShipId type which is similair but implicitly converts to a string. It makes it super easy to find any location that requires the id of a ship and to ensure that those methods get what they need.

So, what’s next?

I think the next push is going to be to implement a game lobby where players will actually be able to join a game and begin with ship placement and follow that up with being able to play through a simple game of cat and mouse (movement and shooting at one another until somebody explodes). I might even go so far as to suggest that if I can pull that off, I might actually have a game on my hands!

Alright that’s it for now. Let’s wrap this up with a quick clip of Altair 0.1.8-alpha in action!