Also note that if technical jabbering bothers you, this thread might not be very interesting.
Hmmm. Interesting title, Comrade. But what the hell are we talking about?
You probably know that Arma 3 natively supports the Headless Client feature. If you don't know what the headless client is, you should probably not be here, but I'm a nice guy and I'll give you a bit of reading.
Notably, this is a feature that I hope can improve the global performance during our big coops, by offloading the AI handling from the server, and after our two first A3 sessions, it seems reasonable to explore that possibility.
Now, the headless client does present one big problem. The AI needs to be local to that client, obviously, when everything you put down in the editor is local to the server. There are multiple ways of dealing with that, and hopefully the project I'm presenting here is one of those.
I'm posting this here as Comrade Wolfenswan asked me to, to get some feedback, code reviews and whatever comes to your mind, and finally because it makes a good base for the future documentation of the script.
Okay, so what is that thing about, then?
This is basically a light set of functions designed to allow mission makers to easily spawn AI, at any time, either directly from the editor, or on the fly in a scripted manner, and on the headless client if it is present. It all relies on creating "group templates", and then allowing the mission maker to spawn them where and when needed.
All the groups you see in this trigger will be saved as templates, under the given name. There is only one trigger for all templates, no matter what side they are on. When the game starts, the script will save a template for each group, then remove any trace of those units and their groups.
A typical mission of mine includes a total of about 5 different groups of enemies, copied/pasted all over the AO. So, a system based on templates might look restrictive, yet I believe it's not, regarding what is usually done in coops.
For now, the template is a simple set of informations, that only contains the classname of each unit, its rank, and its skill level, as well as some optional init code, that'll be run everytime that particular unit is spawned. As I'm aiming for something really simple, I settled for those three (four), but it might be considerable to expand that to other infos in the future, such as the ammo level or whatnot.
Now, to the actual spawning script.
Since we're going to do spawning, I figured we might give the mission maker a few options as to how. The first method relies on triggers: This makes for an easy way to populate an area dynamically. We can spawn any number of iterations of the given template in the area defined by the trigger. The groups will appear randomly in that area, and if no specific behaviour is defined, they'll patrol that area. However, the mission maker can choose between a number of pre-scripted behaviours (patrol, garrison, ambush), or inject his own script to be executed by the groups upon spawning.
Notice how the triggers are synchronized to that gamelogic. This allows every client that is not in charge of the AI to delete all those triggers upon mission start, so that they don't gimp client performance.
I know for a fact that not every mission maker is a big fan of randomizing stuff. Even if you use it, at some point you might want to place a group precisely somewhere and give them manually some waypoints. That second method allows you to do just that, by placing one unit where you need your group to spawn. You can then give that unit waypoints, exactly how you would do it when making a "conventional" mission. Upon mission start, that unit will be deleted, and a group corresponding to the template you need will be spawned in its position, and will retain the waypoints you placed.
Good. But how does that even relate to the Headless Client concept?
Well, all of this relies at first on a mission parameter, set by the host. It allows the script to know if there is an HC connected to the server or not. If there is, then all the spawning will be done directly by the HC. If there's not, the server takes over and does the spawning, as it usually does. This means a mission made with this system is compatible with both HC-equipped sessions, and regular dedicated-server only sessions.
Well, let's see the actual code!
First of all, it needs a slight addition in the editor. The f_spawn_logic (GameLogic >> Objects) with its init line (f_spawn_initDone = false;) and the f_spawn_trigger (trigger) are mandatory in order to use this script.
Code: Select all
if f_param_HCpresent then {
f_isHC = (!hasInterface && !isServer);
};
Code: Select all
/* ===================================================================================
Retrieves all units inside the f_spawn_trigger, and makes a template for each group.
Executed from the f_spawn_trigger.
This is done on the HC (or server) only, the other clients delete all objects synchronized to f_spawn_logic
init line of units can be simulated: this setVariable ["f_spawn_initLine", {code}]; in the init line of units.
==================================================================================*/
f_spawn_local = if (f_param_HCpresent == 1) then {f_isHC} else {isServer};
waitUntil {_cnt = count list f_spawn_trigger; _cnt == f_spawn_count};
private ["_list","_tempVeh"];
_list = list f_spawn_trigger;
if isServer then {
{
_tempVeh = f_spawn_logic;
if (vehicle _x == vehicle (leader group _x)) then {
_grp = group _x;
_tempName = groupID _grp;
_temp = [side _x];
_units = units _grp;
{
_veh = vehicle _x;
_isInf = if (_x == _veh) then {true} else {false};
_type = if _isInf then {typeOf _x} else {typeOf _veh};
_rk = rank _x;
_tempUnit = [_type, skill _x, _rk, _x getVariable ["f_spawn_initLine", {}]];
if (_veh != _tempVeh) then {
_temp set [count _temp, _tempUnit];
};
_tempVeh = _veh;
} forEach _units;
f_spawn_logic setVariable [_tempName, _temp, true];
deleteGroup _grp;
};
} forEach _list;
{
deletevehicle vehicle _x;
} forEach _list;
sleep 0.5;
{
deletevehicle _x;
} forEach list f_spawn_trigger;
f_spawn_logic setVariable ["f_spawn_initServer", true, true];
};
waitUntil {f_spawn_logic getVariable ["f_spawn_initServer", false]};
f_spawn_initDone = true;
Every machine that is not in charge of the spawning will delete all objects synchronized with the f_spawn_logic, to save performance (this is still bugged. I'm running into weird issues here).
Note: a template is basically an array: [SIDE, [Classname, Skill, Rank, {init code}], [Classname, Skill, Rank, {init code}], [Classname, Skill, Rank, {init code}]]. You can actually create templates by script if need be.
f_spawn_logic setVariable ["TemplateName", [Template Array]];
The init code is optional and defined in the init line of the unit like this:
this setVariable ["f_spawn_initLine", {code}];
For now, manually setting those unit as allowdamage false, and enableSimulation false is required, at least if you have the trigger somewher in the water, or if there are groups from opposing sides in the trigger.
Code: Select all
/* ===================================================================================
fsp_fnc_trigSpawn
Used for Trigger-based spawning; spanws a group at a random position inside the trigger, based on a template. Multiple behaviours can be selected.
Arguments: [Trigger, ["TemplateName", Minimum number of copies, Maximum number of copies], "behaviour", optional arguments]
Returns: spawned group
Condition: f_spawn_initDone
On Act. [thisTrigger, ["PatrolGroup3", 2, 5], "patrol"] call fsp_fnc_trigSpawn;
Note: Optional arguments are used for the ambush (provide a position to ambush, if not provided a random position, preferably on a road, is defined)
Optional arguments are used for the scripted behaviour (provide code to be executed (spawned); Arguments passed to the code are [group, position, trigger])
==================================================================================*/
private ["_tempName", "_behav", "_args", "_pos"];
if !f_spawn_local exitWith {};
waitUntil {f_spawn_initDone};
_tempMin = 1;
_tempMax = 1;
_area = _this select 0;
_temp = _this select 1;
if (typeName _temp == "ARRAY") then {
_tempName = _temp select 0;
if (count _temp > 1) then {_tempMin = _temp select 1;};
if (count _temp > 2) then {_tempMax = _temp select 2;};
} else {
if (typeName _temp == "STRING") then {
_tempName = _temp;
} else {hint "Error, template not defined";};
};
if (count _this > 2) then {_behav = _this select 2;} else {_behav = "patrol";};
if (count _this == 4) then {_args = _this select 3;};
_num = [_tempMin, _tempMax] call BIS_fnc_randomInt;
for "_i" from 1 to _num do {
_pos = [_area] call BIS_fnc_randomPosTrigger;
_grp = [_tempName, _pos] call fsp_fnc_spawnGroup;
switch _behav do {
case "patrol" : {[_grp, _pos, _area] call fsp_fnc_taskPatrol;};
case "garrison" : {hint "defined garrison";};
case "ambush" : {_nPos = if (isNil "_args") then {_pos call fsp_fnc_getRoadPos} else {_pos}; [_grp, _nPos] call fsp_fnc_taskAmbush;};
case "scripted" : {[_grp, _pos, _area] spawn _args;};
default {hint "invalid behaviour";};
};
};
This one handles spawning groups from a trigger. You'll notice a few unfinished parts of code here.
The hints are placeholders, and will in time be replaced by a proper debug system working alongside F3's, or the corresponding functions.
Advanced use: In the example above, the condition for the trigger was f_spawn_initDone; That means the trigger would spawn the groups as soon as the sytem was done retrieving the templates. You can actually use any kind of condition here, allowing you to spawn those groups when needed, which is not necessarily right at the beginning of the mission.
Code: Select all
/* ===================================================================================
fsp_fnc_logicSpawn
Used for Unit-based spawning:
Arguments: [unitToReplace, "TemplateName"]
Returns:
Init line: [this, "Template1"] spawn fsp_fnc_logicSpawn;
==================================================================================*/
private ["_tempName", "_logWps"];
waitUntil {f_spawn_initDone};
if !f_spawn_local exitWith {};
_logic = _this select 0;
_tempName = _this select 1;
_logic enableSimulation false;
_pos = getPosATL _logic;
_grp = [_tempName, _pos] call fsp_fnc_spawnGroup;
_logWps = waypoints _logic;
{
_wp = _grp addWaypoint [(getWPPos _x), _forEachIndex];
_wp setwaypointTimeout (waypointTimeout _x);
_wp setwaypointType (waypointType _x);
_wp setwaypointStatements (waypointStatements _x);
_wp setwaypointSpeed (waypointSpeed _x);
_wp setwaypointScript (waypointScript _x);
_wp setwaypointPosition (waypointPosition _x);
_wp setwaypointName (waypointName _x);
_wp setwaypointFormation (waypointFormation _x);
_wp setwaypointDescription (waypointDescription _x);
_wp setwaypointCompletionRadius (waypointCompletionRadius _x);
_wp setwaypointCombatMode (waypointCombatMode _x);
_wp setwaypointBehaviour (waypointBehaviour _x);
} forEach _logWps;
_oldgrp = group _logic;
deleteVehicle _logic;
deleteGroup _oldgrp;
Code: Select all
/* ===================================================================================
fsp_fnc_spawnGroup
Spawns a group from a template
Arguments: ["TemplateName", position]
Returns: spawned group
==================================================================================*/
private ["_unit"];
_tempName = _this select 0;
_pos = _this select 1;
_temp = f_spawn_logic getVariable [_tempName, []];
if (count _temp == 0) exitWith {hint format ["no template defined with this name! %1", _tempName];};
_side = _temp select 0;
_grp = createGroup _side;
{
if (typeName _x == "ARRAY") then {
_class = _x select 0;
if (_class isKindOf "Man") then {
_unit = _grp createUnit [_class, _pos, [], 10, "NONE"];
} else {
_unit = ([_pos, 0, _class, _grp] call BIS_fnc_spawnVehicle) select 0;
};
_unit setSkill (_x select 1);
_unit setRank (_x select 2);
_unit spawn (_x select 3);
};
} forEach _temp;
_grp
Advanced use: It is actually possible to spawn templates anywhere, anytime, by simply calling this function. Note that for this exact reason, it has no locality check. Make sure to call it from the right machine.
Other functions;
Code: Select all
/* ===================================================================================
Creates a randomized patrol for the given group, inside the given trigger area
Arguments: [group, initial position, trigger]
Returns:
[_grpPatrol5, getMarkerPos "PtrlStart", triggerPatrol] call fsp_fnc_taskPatrol;
==================================================================================*/
fsp_fnc_taskPatrol = {
private ["_grp", "_wp"];
_grp = _this select 0;
_pos = _this select 1;
_area = _this select 2;
for "_i" from 0 to (2 + (floor (random 3))) do {
_newPos = [_area] call BIS_fnc_randomPosTrigger;
_wp = _grp addWaypoint [_newPos, 0];
_wp setWaypointType "MOVE";
_wp setWaypointCompletionRadius 20;
if (_i == 0) then {
_wp setWaypointSpeed "LIMITED";
_wp setWaypointFormation "STAG COLUMN";
};
};
_wp = _grp addWaypoint [_pos, 0];
_wp setWaypointType "CYCLE";
_wp setWaypointCompletionRadius 20;
};
/* ===================================================================================
Set the given group for ambushing enemies at the given position
Arguments: [group, ambush position]
Returns:
[_grpAmbush3, getMarkerPos "mkr_ambush3"] call fsp_fnc_taskAmbush;
==================================================================================*/
fsp_fnc_taskAmbush = {
private ["_wp", "_newPos", "_grp"];
_pos = _this select 1;
_grp = _this select 0;
_side = side _grp;
_sidesEnemy = _side call BIS_fnc_enemySides;
_newPos = [_pos, 400, 100, 10] call BIS_fnc_findOverwatch;
_wp = _grp addWaypoint [_newPos, 0];
_wp setWaypointType "MOVE";
_wp setWaypointCompletionRadius 20;
_wp setWaypointBehaviour "STEALTH";
_wp setWaypointCombatMode "GREEN";
{
_trg = createTrigger ["EmptyDetector", _pos];
_trg setTriggerArea [75,75,0,false];
_trg setTriggerActivation [str(_x), "PRESENT", false];
_trg setTriggerStatements ["this", "", ""];
_trg setTriggerType "SWITCH";
_trg synchronizeTrigger [_wp];
} forEach _sidesEnemy;
_wp = _grp addWaypoint [_pos, 1];
_wp setWaypointType "SAD";
_wp setWaypointCompletionRadius 20;
_wp setWaypointBehaviour "COMBAT";
_wp setWaypointCombatMode "RED";
_dir = [_newPos, _pos] call BIS_fnc_dirTo;
{_x setPos _newPos; _x setdir _dir;} forEach units _grp;
};
/* ===================================================================================
Finds a randomized road position 300m around the given position.
Arguments: Position
Returns: road position
(getPos player) call fsp_fnc_getRoadPos;
==================================================================================*/
fsp_fnc_getRoadPos = {
_pos = _this;
_roads = _pos nearroads 300;
_roadsNum = count _roads;
_newPos = if (_roadsNum > 0) then {
getPosATL (_roads select (random (floor (_roadsNum))));
} else { _pos};
_newPos
};
Comrade, I noticed a lot of f_ prefixes in that script. Is this a part of F3?
Absolutely not. If it can't be found on the F3 Wiki, it's not part of F3.
On the other hand, it is designed to be used with F3, as a separate module. That's why I chose the f_spawn prefix for all variables and functions.
As far as I understand, the F framework was designed to solve a number of issues for any mission maker, so that his only mandatory job would be to place units in the editor, and a dynamic spawn module is somewhat out of scope. On the other hand, in light of the recent issues we encountered in A3, I reckon the use of the Headless Client might become more important in the Arma community. In which case, providing an easy solution to handle it can be agood thing.
So what's happening with this project now?
Well, first of all, I'll finish the script itself. You might have noticed there is no garrison function yet, for example, or that some of those functions are still WIP.
I'll proceed to some testing, and when I'm confident everything seems to work fine, I'll create a sample mission, based on F3, and organize a testing session on that server.
Later, my plan is to create an F3 stub including this component, but I'll have to discuss that with the F3 team.
In the meantime, any kind of feedback is most welcome!
See you soon, Comrade.
Update 1: Code updated, added ambush function.
Update 2: Code updated. All functions renamed to be used with the Arma 3 Function Library. I chose the fsp tag for now, for F Spawn module.
fsp_fnc_clearObjects is still problematic, I've been working on workarounds that seem a bit overkill here. Some performance testing will be needed to determine if this is necessary or not.
Update 3: Code updated, to match the current version used with an F3 mission. All functional, some parts are still missing: garrison, deleting triggers clientside, as well as maybe a simple attack function.
Update 4: Retrieving the templates has been moved server-side to cater with trigger-related issues upon mission start.