Mini-Galcon Post-Mortem

Min-Galcon is the first of a series of mini games which aims to reproduce the basics mechanics of existing games and experiment with CraftStudio.

You can access Mini-Galcon’s project by searching for it in the community projects in CraftSudio. You can also download it from Daneel’s website.

For those who don’t have CraftStudio, you can at least download the game for Windows.

So here are some of the tricks used while building Mini-Galcon.

Using Font as sprite sheet

The Font asset is basically a sprite sheet, a collection of images. It has been built specifically with the TextRenderer component to display text but nothing prevents you to to use it for anything else. It has the advantage (over the models) to use a single asset for multiple colors/sizes/forms.

Since the planets and the ships are flat, that’s what I used for the planets (See the Planets font). I used models for the ships for the reason you will discover below.

Main Menu

Thanks to the newly released Daneel’s GUI components, the level of interactivity provided by the main menu is pretty high compared to the amount of code needed to implements it (see the MainMenu script).

Toggle component

    -- player colors = team
    local colors = GameObject.Get("Colors").children

    for i, colorGO in ipairs( colors ) do

        colorGO.toggle.OnUpdate = function( toggle )
            if toggle.isChecked == true then
                toggle.gameObject.transform.localScale = 1.5
                local modelPath = Asset.GetPath( toggle.gameObject.modelRenderer.model )

                Game.playerTeam = tonumber( modelPath:sub( #modelPath ) )
            else
                toggle.gameObject.transform.localScale = 1
            end
        end

        if i == (Game.playerTeam - 1) then
            colorGO.toggle:Check( true )
        end
    end

You can select your ship’s color with three radio buttons which are toggle components that are in the same group, so that only one of them can be checked at a time and clicking of a non checked toggle automatically unchecked all other toggles.

In the OnUpdate event (called when a toggle is checked or unchecked), the scale of the model is changed to reflect its state and the player’s team is set based on the model’s name (which ends by the team number).

Asset.GetPath(asset) is mostly an alias of Map.GetPathInPackage(asset) which returns the fully-qualified path of the provided assets (works for any asset, not just Maps !).

Input component

    -- level's number of planet
    local planetCountGO = GameObject.Get("PlanetSelection.Input")

    planetCountGO.textRenderer.text = Game.planetCount

    planetCountGO.input.OnFocus = function( input )
        if input.isFocused then
            input.gameObject.child.modelRenderer.opacity = 0.5

        elseif not input.isFocused then
            input.gameObject.child.modelRenderer.opacity = 0.2
            if input.gameObject.textRenderer.text:trim() == "" then
                input.gameObject.textRenderer.text = "10"
            end
        end
    end

Here GameObjet.Get() is used first to actually search for a hierarchy of objects and returns the last specified child.

When the input gain or lose the focus, its background’s opacity is changed to reflect the new state. The child property on the gameObject is a shortcut for the GetChild() function without argument which returns the game object’s first children (in this case the game object with the background model).

Slider component

    local go = GameObject.Get( "AIDifficulty" )
    local sliderGO = go:GetChild( "Handle", true )

    sliderGO.slider.OnUpdate = function( slider )
        Game.difficulty = math.floor( slider.value )
        go.child.textRenderer.text = "Difficulty : " .. Game.difficulty
    end

    sliderGO.slider.value = Game.difficulty

Another great way to let players set a value within boundaries is to use a slider they just have to moves with the mouse. It requires minimum setup via its scripted behavior and when the slider is updated (moved), the game’s difficulty is set and the value of the text next to the slider is updated accordingly.

Play button

    local playButton = GameObject.Get( "PlayButton" )
    local t = Daneel.Tween.Tweener( playButton.transform, "localScale", Vector3( 0.1 ), 0.8, {
        isRelative = true,
        loops = -1,
        loopType = "yoyo"
     } )

     playButton:AddTag( "button" )
     playButton.OnClick = function()
        Game.planetCount = tonumber( planetCountGO.textRenderer.text )
        Scene.Load( "Level" )
     end

Creating a button requires no more than three steps :

  • set a tag on the button.
  • set the OnClick event on the button.
  • make sure the tag is also set in the tags public property on the MouseInput scripted behavior added on the camera the button is visible from.

The “Heart Beat” animation is produced by the infinite looping tweener that offset the button’s scale by 0.1 over 0.8 second.

Generating the level

The level you play is randomly generated based on the number of planets you choose with the input field. See in the Awake() function in the GameManager script.

Based on the number of planets, a square is defined, inside which the planet’s positions are randomly picked (in addition to the planet’s scale and number of ships it holds). The position is then checked to see if it is not too close to a planet that already exists.

The script also keep track of which planet is at the top and  the bottom of the level. This is those planets who will become the planets the player and the AI start the game with. Once all planets are spawned, the team of both planet is set (the other planets are neutral) and their scale and ship count are made equal (for equity, because the scale impact the rate at which a planet produce ships).

AI

“Dumb automaton” would be a more appropriate term but let’s use AI anyway. The only thing the AI really does is that it picks one of its planets with enough ships and sends them to the nearest neutral or enemy planet, every time it is allowed to take a decision.

The game difficulty (which you select via the slider in the main menu) impacts two settings of the AI : the interval at which it is does something (sends ships to another planet) and the ship threshold or minimum number of ships a planet must have to be “used”. The smaller the difficulty, the smaller both values are.

A difficulty of 1 leads to a very reactive AI that spread quickly but may be easy to conquer since none of its planets holds more than a couple of ships. It is to your advantage on a small level where you can conquer its few first planets quicker than it spread but on a big map, it may have already conquered several planets by the time your ships arrives to its side of the level. Once it has conquered a few more planets, I became really hard to take the advantage back since the AI uses all its ships on all of its planets way faster than you -weak human- can do it.

On the other hand at higher difficulty, the AI tends to have fewer planets but that are harder to conquer because it takes decision less often and have a higher ship threshold. In this case however, you can easily spread faster than the IA and finally overwhelm it with ships.

Selection box

A selection box appear when you left click and drags the mouse in the level, this allows you to select multiple planets.

The selection box is just a model in front of the camera (between the camera and the planets) that is scaled depending on the distance between the location of the mouse when the left click first happened and the current mouse position. This also works because the selection box model is 16 model units wide and its origin has been moved to the upper-left corner of the box and its  game objet has a scale of 1.

Now to detect if a planet is inside the selection box when the left click is released, a ray is created between the camera and the planet. The planet is inside the selection box if the ray also intersects the selection box’s model renderer. (I didn’t tried it yet but this is how I would implement some frustrum/occlusion culling).

Lots of ships !

The planets increase their ship count at a rate based on their scale and spawn their ships (one per frame) when asked to. The ship count on a planet also varies when ships arrive at it, depending on the team of the ships. Same team increase the count, different team decrease the count until it reaches 0 and the planet changes team. The ships just move from one planet to another.

That was the occasion for me to experiment with the performances related to moving a several hundred or thousand of game objects (rendered on screen with a model).

First, be aware that using transform:Move( offset ) is way faster than getting the old position and manually setting the new position ( transform:SetPosition( transform:GetPosition() + offset ) ). Obviously the number of times a second you update the position also has an impact on the performance. Here is a benchmark below.

The number is the approximate number of ships at which the game begins to slow down (the movement of the ships starts to appear jerky) on my computer (Scientific Process Inside).

Using transform:SetPosition()

  1. function called every frames : 440
  2. function called every 2 frames : 750
  3. function called every 3 frames : 950

Using transform:Move()

  1. function called every frames : 760
  2. function called every 2 frames : 1280
  3. function called every 3 frames : 1500

Now the issue is that you can’t just makes the ships move every 10 frames for instance because the movement appears jerky as soon a couple of frames between each movement. It’s probably not necessary for Mini-Galcon unless you have a big level but I wanted to find a way to have even more ships moving at the same time :

In CraftStudio there is only two ways to get something moving : moves the game object or moves a model block via a ModelAnimation. That’s why models are used for the ships. The ships use a looping animation that moves the only ship’s block forward. Since the animation last 0.5 second, the game object itself is moved only every 30 frames and at the same frame where the animation restart, so the movement appear “normal”, yet the game object only moves twice per second.

And since the ModelAnimation are handled by CraftStudio, and not the Lua scripts, they are faster and allows -in my benchmark- to have more than 2500 ships moving at the same time without noticing major performance issue !

Comments are closed