Stuttering tractors, Agrarian Coops and headless Chicken

Workshop for all Mission Engineer Comrades. Home of the FA Mission Making Template.
Post Reply
Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

Stuttering tractors, Agrarian Coops and headless Chicken

Post by Black Mamba »

Warning: This is a Work in Progress. It does not contain a fully working system as is, and I'll update this thread to show progress when necessary.
Also note that if technical jabbering bothers you, this thread might not be very interesting.

Image

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.

Image
Image

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.

Image

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.

Image
Image

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);
};
The f_param_HCpresent is the variable defined by that mission parameter. If set to true, the f_isHC variable is a boolean, set to true on the HC, false on every other client and the server. This is quite an important thing, as it could potentially be used by quite a number of other scripts to determine who is the Headless client. That's why it is set apart from the rest of the script.

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;
This is the base of the script. The f_spawn_local is a boolean, defining who will be in charge of the spawning (either the HC, or the server). Then it goes to retrieving all the templates from the f_spawn_trigger.
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 function, and the two coming after that, should be registered in the Description.ext, with the flag PreInit = 1; so they are ready to be used when the mission starts.
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;
This one handles spawning a group in place of an editor-placed unit. It is pretty straighforward, and probably doesn't need the ability to pass code, as it could be passed in any of those waypoints. Note that for simplicity purposes, I use a simple deleteVehicle to remove the editor placed unit. The mission maker should only use infantry units as placeholders, as vehicles won't be deleted properly. That could be "fixed" if needed.

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
Now this is the function that handles the actual spawning. As you can see, this is a very light one. More testing will be needed to see if something more complete is needed.

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
};
Those are minor functions, some are WIP.

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.
Last edited by Black Mamba on Sun Sep 22, 2013 10:15 am, edited 8 times in total.

User avatar
harakka
Posts: 365
Joined: Fri Jul 29, 2011 3:35 pm
Location: Finland

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by harakka »

Thanks for posting this, Mamba. Interesting read. Looking forward to see how it develops.

When we chatted about this previously, I mentioned to you the idea of just hoisting all AI groups onto the HC with setOwner, first storing their waypoints and such, and then re-applying the waypoints once ownership transfer is finished. Did you do any digging into this before starting with your implementation? If yes, I'd be interested in hearing what the issues were, or why you believe your current approach is better?
Me and him, we're from different ancient tribes. Now we're both almost extinct. Sometimes you gotta stick with the ancient ways, the old school ways. I know you understand me.

Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by Black Mamba »

I actually did not dismiss the idea. I already made a function doing just that (not posted here), that might get added to the thing.
The issues I'm looking at:
- Right now, with the lack of actual testing on a server with an HC, I don't know how badly things would react to transfering all units from one machine to another.
- I need to test if the function needs to transfer group leaders only, or the whole groups. I'm pretty sure there would be complications with groups including vehicles.
- If this method was to be employed, it would need to be very transparent for the mission maker, as it could mess up a lot of scripting relating to those units (most mission makers will not test their mission with an actual HC).

Another important reason why I chose to implement this type of thing is that, when reading feedback about F3 (be it on BI forums, reddit, etc.), I often see people complaining about the fact that it does not include a simple spawning script.
I'm usually tempted to just answer that it's not in the scope of the project, but I believe that including a separate download that actually does might not be a bad thing.

Finally, this allows for "dynamic spawning", meaning that you don't need to have all units on the map at the beginning of the mission, and can spawn them on the fly by just setting up triggers in the editor. Seeing the performance issues we're encountering with coops, it might be one way to improve performance a bit.

User avatar
Cam
Posts: 65
Joined: Sun Dec 02, 2012 10:35 pm

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by Cam »

Apparently, setOwner has some strange effects when used to move AI over. Obviously, second-hand knowledge, but I figure it could be useful to have noted down here.
Famine wrote:setOwner has produced some strange effects when I tested it once, aside from the AI losing all instruction (waypoints, etc)
http://forums.unitedoperations.net/inde ... /?p=206611

User avatar
harakka
Posts: 365
Joined: Fri Jul 29, 2011 3:35 pm
Location: Finland

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by harakka »

Too bad there's nothing beyond "strange effects". That isn't very helpful.
Me and him, we're from different ancient tribes. Now we're both almost extinct. Sometimes you gotta stick with the ancient ways, the old school ways. I know you understand me.

Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by Black Mamba »

I ran into the same kind of comments while reading stuff here and there: setOwner would have issues with waypoints and "other stuff". But I couldn't find what that "other stuff" was refering to anywhere.

User avatar
head
Posts: 133
Joined: Sun Jul 31, 2011 4:22 pm
Location: Sweeeden

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by head »

Indeed. i wonder if these "strange issues" are the same i have been having relation to the AI being immobile monsters.

Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by Black Mamba »

A bit of progress here, as I got the first version to work okay with an F3 mission (available on the test server as fa3_HCTest_v3).
OT updated.

feanix
Posts: 31
Joined: Sun Jun 03, 2012 10:25 pm

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by feanix »

I'm curious and also I suck at scripting so I may have missed this when reading the OT. Somewhere on the BIS forums I think I saw someone mention that when using a HC all the unit's AI must be done via scripting? They suggested using UPSMON. Are we doing this? If not, are we doing a simplified version of UPSMON? I was unable to tell by looking at the code.

Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

Re: Stuttering tractors, Agrarian Coops and headless Chicken

Post by Black Mamba »

Well, it probably can't really be compared to UPSMON, as UPSMON does change quite drastically the way the AI behaves, but this is indeed a simplified solution to create all AI directly on the HC.
The basic features are somewhat along the lines of UPSMON: randomization of placement, of numbers of enemies, different behaviours (patrol, garrison, ambush, attack, helo insertion, etc...)
On the other hand, everything in here was thought with efficiency as the main goal: functions are pretty simple, lightweight (the full folder is less than 15Ko), but you can easily stage the spawning of your AI with triggers, whereas UPSMON is quite the heavy beast (it has been maintained by different people as time went by, there's a lot of redundant stuff in it, and it's generally a resource hog).

By the way, this thing is not dead at all: the Hell's Highway mission (on the FA A3 server) is entirely based on this. I'm just adding the last features in, and I'll release a working F3 stub in this thread.

For all missions makers looing to try HC stuff, you can also have a look at Wolfenswan's work, as it has some neat, but different ways to deal with HC and all that. And it's generally just pretty neat and useful.

Post Reply