Coop Mission DevLog

Intro

Creation of Coop mission is a complex procedure with 3 main stages each taking huge amount of time and requiring specific skills and knowledge:

  1. Map design
  2. Scripting
  3. Testing and Balancing

Of course don't forget about whole Idea of the mission with its own plot and structure of objectives.

Map design

Maps in Coop are not the same as in competitive games. Those are lacking symmetry to be realistic and entertaining. Map structure is usually made so each new expansion during the mission reveals new areas. This is why world border sometimes breaks this mystery of next map expansion. Map should be simple but at the same time providing different ways of completing an objective.

I'm not a designer and have made few maps to be that experienced in this craft. But I'm still gonna use map editor to combine all of things I'll get with Gaea.

Basically it is a lego for creating natural and realistic terrains for games. It is a long talk about this tool so I'll keep it for next time. Main idea I'll make map for mission with this one not touching map editor at all, but for base design and setting up some stuff for scripting...

Scripting

I'll tell you a secret: everything is trying to kill you is scripted to kill you on missions. There is no AI that thinks what to do actually. Script is a determined sequence of actions to keep track of all things players do and good scripts mean good flow of the mission.

However, I find it a problem. Current state of Coop missions scripts is very disappointing. Imagine good looking house from outside but having inside a total mess upside down furniture. Well, you still can live there except having issues with finding things and having troubles walking around the house. If you will get used to it, it doesn't mean others can.

Studying for Software Engineer made me hate bad code, so, I decided to create better foundation for scripting of coop missions at this point. An another long talk for demo of Oxygen framework.

Testing and Balancing

Testing and Balancing is very important. Mission must entertain casual players at lower difficulties but give a challenge for experienced players on higher difficulties. Mission mustn't be a hellhole where you suffer (ofc If player wants, he finds the way).

Scripts are allowing everything to tune according to difficulty: amount of units, amount of resources, amount of time for objectives, conditions based on players' progress, and so on. Map is about base positions, their setup depending on difficulty, terrain, resources distribution and paths for "AI".

What do I want?

To share my experience! I'm doing that for fun and to challenge myself. Ofc if you are interested, you can always help me or suggest things 😛

I'll keep this thread updated throughout of mission dev sharing stuff I do.

Oxygen

Mission scripting is one of the most hard part of making coop mission.
It has its own stages and parts that decide mission flow.
Currently there is only one way of scripting mission in coop. It is using ingame Scenario Framework and its dependencies.
However, it has so many problems in root that make scripting more pain than joy.

Oxygen is a Framework that solves this problem and improves some aspects of mission scripting.
These are main parts that form coop mission.

Objectives and their management

As I mentioned before each mission is a bunch of sequential objectives of different types, such as:

  • Kill specific unit
  • Kill units in area
  • Capture unit
  • Reclaim unit
  • Build specific unit
  • Protect units
  • Locating units
  • Timer
  • Damage unit on required value (by Oxygen)

There can be added more as I did for Damaging unit, but rn objectives are pure trash in their implementation.
To reduce communication with old framework Oxygen provides with Objective Builder and Objective Manager.

Objective Builder simplifies creation of objective to be loaded then with Objective Manager.

local objectives = Oxygen.ObjectiveManager()
local objectiveBuilder = Oxygen.ObjectiveBuilder()


objectives:Init
{
objectiveBuilder
  :New "objective1" --name of objective to be called then with ObjectiveManager
  :Title "your title" -- title that player will see in UI
  :Description [[
    description of objective
    ]] -- description that player will see in UI
--[[:Function "CategoriesInArea" -- function that will be called during objective start (annotated string)
  :To "kill" -- action to perform (can also replace objective function if wasnt specified) (also annotated)]] -- deprecated
 :To(Oxygen.Objective.Kill) -- class of the objective that will be instantiated and will receive Target
  :Target
  {
    ... -- arguments for objective (annotated)
  }
  :OnStart(function()
    ... -- function that will be called before objective is assigned and actually started, can return table with arguments for Target (useful when specific unit required for objective)
  end)
  :OnSuccess(function()
   ... -- function that is called when objective is successfully complete
  end)
  :OnFail(function()
    ... -- function that is called when objective is failed (optional)
  end)
  :Next "objective2" -- objective that is started after success of this one (optional)
  :Create(),
  ...
}

I won't provide how this looks in base game because I dont want to hurt your eyes 😛

After objectives were initialized they can be started:

objectives:Start "objective1"

When mission can be ended at success or fail:

 objectives:EndGame(true) -- success
    ...
 objectives:EndGame(false) -- fail

And Objective Manager will automatically assemble all data for primary, secondary and bonus objectives to be then displayed in UI.

Cinematics

During start or end of objectives players must be notified about next ones with some kind of message quickly briefing then about it.
After mission is complete and new one starts players can be notified with dialog appearing briefing them.
Also we can make cutscene showing objective targets during which players cant do any input: NIS mode.
This is useful to create immersive shots of battles and enemy units moving towards players.

Oxygen Cinematics kicks in:

local OC = Oxygen.Cinematics
...
-- During NIS mode players cant do any input and black bars appear on top and bottom.
-- We can also pass in areas where units will become invulnerable during cutscene.
OC.NISMode(function()

    -- Position camera with marker defined on map
    OC.MoveTo("Cam1", 0)

    -- Create dialog where nice man tells you about objective (there are lots of examples in other missions,
    -- most important here that we can create our own provided with mission, but it isnt easy process since game is really old)
    ScenarioFramework.Dialogue( {{
            text = '[HQ]: <something describing mission>',
            vid = 'video.sfd',
            bank = 'wolf',
            cue = 'corre',
            faction = 'Cybran'
        }}, nil, true)
    
    -- waiting a bit
    WaitSeconds(2)

    -- we can display some text, but as I tried to make it bigger, it would crash game
    -- UI 4 Sim is better which I'll show later (UI 4 Sim allows you create custom UI for map which is synced)
    OC.DisplayText("Global\nWarning", 12, 'ffffffff', 'center', 1)

    -- moving camera to other position for 2,5 seconds
    OC.MoveTo("Cam3", 2.5)
    WaitSeconds(2.5)

    -- creating vision at enemy base just to show how dangerous it is
    -- it will be hidden after NIS mode ends leaving no icons ('true' flag)
    OC.VisionAtLocation("MainBase_M", 60, Brains.Player1):DestroyOnExit(true)

    -- and so on...
    OC.MoveTo("BaseCam1", 3)
    OC.MoveTo("BaseCam2", 1)
    OC.MoveTo("Cam3", 4)

end, {"BattleField1", "BattleField2"})

There are lots of possibilities for phase between objectives:

  • expanding map and loading new bases
  • creating attacking units defined on map and giving them orders
  • setting up objective's targets

But let's do players' setup since we need those as well.

Players

Oxygen provides with PlayersManager which simplifies process of setting up players and spawning em, however, it also cuts some certain advanced options, but for our purposes this will fit extremely well.

local playersManager = Oxygen.PlayersManager()

...

playersData = playersManager:Init
 {
    -- name of each upgrade is annotated, so, you dont need to 
    -- learn each to set. But dont forget that not all upgrades may fit in the same slot.
    -- Setting up upgrades for all players:
    -- the returned value is data per player, it has as many entries as there are players in lobby
  enhancements = {`
    Aeon = {
        "AdvancedEngineering",
        "T3Engineering",
        "ResourceAllocation",
        "ResourceAllocationAdvanced",
        "EnhancedSensors"
    },
    Cybran = {
        "AdvancedEngineering",
        "T3Engineering",
        "ResourceAllocation",
        "MicrowaveLaserGenerator"
    },
    UEF = {
        "AdvancedEngineering",
        "T3Engineering",
        "ResourceAllocation",
        "Shield",
        "ShieldGeneratorField"
    },
    Seraphim = {
        "AdvancedEngineering",
        "T3Engineering",
        "DamageStabilization",
        "DamageStabilizationAdvanced",
        "ResourceAllocation",
        "ResourceAllocationAdvanced"
    }
  },
  {
    -- we can set any color we want as it is done in UI
    color = "ff18DAE0",
    -- those are used to spawn player on map (those names must be defined on map
    -- for each player)
    units =
    {
        Aeon = 'AeonPlayer_1',
        Cybran = 'CybranPlayer_1',
        UEF = 'UEFPlayer_1',
        Seraphim = 'SeraPlayer_1',
    },
    -- custom name for a player (if not set it will use its own from lobby)
    name = "Punch lox"
  },
  {
    color = "ff69D63E",
    units =
    {
        Cybran = 'CybranPlayer_2',
        UEF = 'UEFPlayer_2',
        Aeon = 'AeonPlayer_2',
        Seraphim = 'SeraPlayer_2',
    },
    name = "Zadsport",
    -- we can set specific upgrades per player as well
    enhancements = {
        Aeon = {
            "AdvancedEngineering",
            "T3Engineering",
            "ResourceAllocation",
        },
        Cybran = {
            "AdvancedEngineering",
            "T3Engineering",
            "ResourceAllocation",
        },
        UEF = {
            "AdvancedEngineering",
            "T3Engineering",
            "ResourceAllocation",
        },
        Seraphim = {
            "AdvancedEngineering",
            "T3Engineering",
            "ResourceAllocation",
        }
  },
  },
 }

-- we can get their count like this
local playersCount = table.getsize(playersData)

 ...
-- After that we can spawn players' ACUs warping them ...
playersManager:WarpIn(function()
    ScenarioFramework.Dialogue(VOStrings.E01_D01_010, PlayerDeath, true)
end)
-- or gating in (this only changes effects ACUs spawn with, you will have to setup gate on map yourself)
playersManager:GateIn(function()
    ScenarioFramework.Dialogue(VOStrings.E01_D01_010, PlayerDeath, true)
end)
-- The function passed in is players' death callback.
-- You can make mission end if player dies or keep count of dead players.

Base Managers and Platoons

AIs must have something to control to offend player during objective.
This can be reached with Base Managers that produce units groups.

Base Manager handles most of things dedicated to base management:

  • Managing engineers
  • Construction and maintenance of base
  • Production of Platoons
  • Scouting
  • Transporting (by Oxygen)

The most important here is that base manager does most of the stuff for us from the box.
There are platoons left to be setup here and it is pretty large topic, because this makes AIs "alive".

Platoons

Platoon is a group of units controlled by a thread specified after its assemble.
And why it is a large topic is because platoons have so many options to be set.

Here comes Platoon Builder to simplify this process:

...
local pb = Oxygen.PlatoonBuilder()
 -- before platoon builder is used it can be set up to reduce amount of code
pb
    -- sets AI function to be used for any platoon created by platoon builder 
    -- if there wasnt specified
    :UseAIFunction(Oxygen.PlatoonAI.Common, "PatrolChainPickerThread") 
    -- base manager name that builds this platoon
    -- not necessary since platoon loader of Advanced Base Manager sets it by default
    -- (ill show later)
    :UseLocation "SE_BASE"
    -- type of platoon to produce (can be Land, Air, Sea, Gate or Any)
    :UseType 'Land'
    -- sets PlatoonData to be used by AI function
    :UseData
    {
        PatrolChains = {
            "LAC01",
            "LAC02",
            "LAC03",
        },
        Offset = 10
    }

    -- after that we can add platoons into base manager directly with creating

baseManager:LoadPlatoons 
{
    pb:NewDefault "Rhinos SE" -- name of platoon, must be a unique value
        :InstanceCount(5)     -- number of instances of platoon that base manager will produce (defaults to 1)
        :Priority(280)        -- priority of platoon construction, base manager will build platoons with higher priority first
        :AddUnit(UNIT "Rhino", 4) -- adding 4 rhinos into platoon
        :AddUnit(UNIT "Deceiver", 1) -- and 1 deceiver
        :Create(), -- creating, before it is actually added into BM it will get all 'Use' we set before

    -- another way of doing this
    pb:NewDefault "Brick Attack"
        :InstanceCount(5)
        :Priority(200)
        -- we can set on what difficulties to build this platoon
        :Difficulty { "Medium", "Hard" }
        :AddUnits
        {
            { UNIT "Brick", 5 },
            { UNIT "Banger", 3 },
            { UNIT "Deceiver", 1 },
        }
        :Create(),
    
    -- we can make brick fly, platoon will pick transport from dedicated BM or from global pool
    -- and then move on transport by chains specified (PlatoonData is being replaced as well as AIFunction,
    -- because we specified those)
    pb:NewDefault "Flying Brick"
        :InstanceCount(3)
        :Priority(250)
        :AddUnit(UNIT "Brick", 1)
        :AddUnit(UNIT "Deceiver", 1)
        :AIFunction('/lua/ScenarioPlatoonAI.lua', 'LandAssaultWithTransports')
        :Data
        {
            TransportReturn = "MainBase_M",
            TransportChain = "FlyingBrickRoute",
            LandingChain = "FlyingBrickLanding",
            AttackChain = "TransportAttack"
        }
        :Create(),

    -- transporting engineers to build expansion base, also requires dedicated BM to exist
    pb:NewDefault "SE Engineers"
        :InstanceCount(1)
        :Priority(500)
        :AddUnit(UNIT "T3 Cybran Engineer", 5)
        :Data
        {
            UseTransports = true,
            TransportReturn = "MainBase_M",
            TransportChain = "SE_Base_chain",
            LandingLocation = "SE_Base_M",
        }
        -- this line makes all magic, it makes specific platoon setup
        -- so it becomes an expansion one (also provided by Oxygen)
        :Create(Oxygen.BaseManager.Platoons.ExpansionOf "SE_BASE"),
    
    -- we can load platoon template units that were defined on map
    -- setting up squad (Artillery) and its formation (GrowthFormation)
    -- by default all units added into platoon builder without specified
    -- squad or formation get those as 'Attack' and 'AttackFormation'
    pb:NewDefault "Arty attack"
        :Priority(500)
        :AddUnits(
            Oxygen.Misc.FromMapUnits("Evil Bot", "ArtyAttack", 'Artillery', 'GrowthFormation')
        )
        :Create(),

    -- Ras bots are also supported :P
    -- of course Gate must present in base
    pb:NewDefault "bois"
        :Type "Gate"
        :Priority(500)
        :AddUnit(UNIT "Cybran RAS SACU", 10)
        :Create(Oxygen.BaseManager.Platoons.ExpansionOf "NukeBaseGroup"),

}

Game and Expanding map

TODO

Triggers

TODO

...

reserved

Turn those streams into videos 👍 , it will be a great help to see what you're doing

A work of art is never finished, merely abandoned

Updated second post for Oxygen

Added Platoon part

Why do you use the syntax sugar where functions don't need parenthesis?

pb:NewDefault "bois"

Why not just:

pb:NewDefault("bois")

A work of art is never finished, merely abandoned

Because it looks better 🙂

But it also makes it less intuitive 🙂

A work of art is never finished, merely abandoned

Updated Cinematic and players.

Fu... It is really bad having no proper highlight on lua code
You can read same here.

Good news! Oxygen now completely replaces base game objectives with its own. Currently, there is no significant logic difference. But I think besides new objectives like Damage, other changes will appear in common ones. Of course this change was done towards code improvements and optimization!

This post is deleted!

2553dd7e-4bf2-4676-b52b-6e2256fcf920-image.png

Oxygen got an update. Sooner or later I'll make a post about mission I've been working on and may be continue work on it 🙂

If you wanna try it in COOP Dev discord server there is a link in public uploads. You can also discuss this mission there.