Jump to content
Sign in to follow this  
scotchrt

Selecting several random players at the start of a mission

Recommended Posts

Hey,

I'm currently trying to do a mission where players are given a certain role at the start of the game. What I want to happen is a script picking a certain number of random players from all the players that are on the server and showing a hint to them, notifying them of their role. Since people can have only one role, they mustn't be picked more than once. I'm still at the very start, so I try to keep it simple and assume there is a set number of players and a set number of roles given out. This is what I have so far:

   
if (!isServer) exitWith {};                              // Execute on server only
   _unitarray = [playableUnits];                     // Array with all players

   _traitor1 = (_unitarray call BIS_fnc_selectRandom); //Get first role from array
   _unitarray = _unitarray - [_traitor1];                    // Delete role from the array
   _traitor2 = (_unitarray call BIS_fnc_selectRandom);
   _unitarray = _unitarray - [_traitor2];
   _traitor3 = (_unitarray call BIS_fnc_selectRandom);
   _unitarray = _unitarray - [_traitor3];
   _traitor4 = (_unitarray call BIS_fnc_selectRandom);
   _unitarray = _unitarray - [_traitor4];

   if (_traitor1 == _traitor1) then {hint "You are a traitor! Your goal is to stay alive and make sure the others don't!"};  //Give a hint with role information to players  
   if (_traitor2 == _traitor2) then {hint "Sample Text"};
   if (_traitor3 == _traitor3) then {hint "Sample Text2"};
   if (_traitor4 == _traitor4) then {hint "Sample Text3"};

I tried looking for a way to just randomize the order of elements in an array at first, but couldn't find anything, so I decided to just delete the element from the array after it has been picked in order to make it impossible for players to be given two roles. However the way I'm doing it right now, it's just not working. I launched the game with -showScriptErrors in order to figure out what was going in, so I managed to pinpoint the problem to the last block of the script.

 if (_traitor1 == _traitor1) then {hint "Text"};

apparently doesn't work. It's my way of trying to send the hint to just that one player. The error I got was something along the lines of "_traitor1 == _traitor1 expected array, object, string [...]" So I assume is that somehow in the process _traitor1 got assigned a value that can't be compared. And now I'm standing here and don't know what to do.

Anyone got any ideas or workarounds? Help is appreciated.

Cheers

Share this post


Link to post
Share on other sites

Hello ScotchRT,

we're trying to implement TTT from CounterStrikeSource, aren't we? :D

I also had this in mind and I'm really surprised to see this topic, so I'm gonna help you to set up the basic structures.

Judging from what you've posted, I can assume that you're not really experienced in MP scripting. I'll shortly depict the errors in your code so far:

1) If you want to execute code in your server, a simple if will suffice - what you actually did. But using exitWith in the script-scope is not advisable.

BIKI says: "When you use exitWith not inside a loops, the behaviour is undefined - sometimes it may exit the script, sometimes some other scope, but this is not intended and designed behaviour of this command, and it is not guaranteed to work reliably."

This corresponds to the hardships I've taken until I noticed that exitWith didn't exit my scripts sometimes, when other times it did.

2) playableUnits will return an array with all playable units in your game - what you do is to assign an array to another array, which translates to: [ [ player1, player2, player3 ] ].

I didn't indulge myself into the sourcecode of selectRandom yet, but I bet the command can't handle multi-dimensional arrays, so all the _traiter-variables you defined should be equal to playableUnits (which is an array and not an individual player like you assumed).

3) From the perception of logical equations, every equation returns true if both sides are the same.

Comparing _traitor1 to _traitor1 will ALWAYS return true, same as comparing _traitor2 with _traitor2 and so on, the reason is self-explanatory.

The error you're getting is ...

4) ... because of the aforemeantioned multi-dimensional array from 2). So effectively you're comparing [ player1, player2, player3 ] with [ player1, player2, player3 ], which will NOT return anything valid because the game engine can't implicitly compare arrays, hence the error.

5) Just imagine that using the if statments at the bottom of your script would really hint the message to the respective player.

The problem is: you've decided to execute this script in your server, so the hint's would get outputted (if the problem meantioned in 4) wouldn't be there) but you wouldn't see them, because they're getting outputted on the server :)

Okay, now we know what we did wrong. We'll try to come up with some working solutions now.

First of all, you've to ask yourself how many traitors you'd want to have in your game.

Using a static number is easy at first, but will put the game balance at risk. Using 2 traitors for a party of 5 players seems appropriate, but using just two traitors in a party of 20 could be rather boring, or consider a party with just 3 people with two of them beeing traitors...

What we need is a dynamically defined number based on the total number of players in your server. How about 25% of all players convert to traitors?

_allPlayers = +(playableUnits);
_numPlayers = count _allPlayers;
_numTraitors = ceil(_numPlayers * 0.25);

Now we're going to assign the roles using the same approach you already did: Pick a random player out of the array and take it out of it.

_currPlayer = objNull;

// Cycle number equals the number of traitors
for "_i" from 1 to _numTraitors do
{
   // pick someone from the players list
   _currPlayer = _allPlayers call BIS_fnc_selectRandom;

   // assign the traitor role (and sync it with all clients)
   _currPlayer setVariable ["currentRole", "traitor", true];

   // delete the currently picked player out of the array
   _allPlayers = _allPlayers - [_currPlayer];
};

// Now iterate over the other players and assign them a default role
{
   _x setVariable ["currentRole", "innocent", true];
} forEach _allPlayers;

We've successfully assigned roles to every player in your game.

The advantage of using setVariable is the capability to check the current role of the player from every client when the optional third parameter is set to true (which will sync the variable with all clients).

Now we have to take care of the hints. Because we're executing this in the server (with a good reason to boot!), we need to remotely execute the hint on every client which became a traitor.

Using the Multiplayer-Framework will enable us to use the RE-command, but there is another way to achieve this: We did assign all the roles so every client knows which role it has, so why not using this fact and combine it with a PVEH? :D

"roleInfo" addPublicVariableEventHandler {
   _role = player getVariable ["currentRole", "innocent"];
   _output = switch (_role) do 
   {
       case "traitor": {"You are a traitor! Your goal is to stay alive and make sure the others don't!"};
       default {"You're an innocent lamb, try to survive and take care of the wolfs amongst your flock!"};
   };

   hint _output;
};

All you have to do now is to trigger above code:

// ...

// Now iterate over the other players and assign them a default role
{
   _x setVariable ["currentRole", "innocent", true];
} forEach _allPlayers;

// just a short nap to make sure the variables are being synched successfully
sleep 2;

roleInfo = true;
publicVariable "roleInfo";

Phew, some scripting we made here, huh? :D Let's put all this together into one script.

assignRoles.sqf:

_allPlayers = +(playableUnits);
_numPlayers = count _allPlayers;
_numTraitors = ceil(_numPlayers * 0.25);
_currPlayer = objNull;

// Cycle number equals the number of traitors
for "_i" from 1 to _numTraitors do
{
   // pick someone from the players list
   _currPlayer = _allPlayers call BIS_fnc_selectRandom;

   // assign the traitor role (and sync it with all clients)
   _currPlayer setVariable ["currentRole", "traitor", true];

   // delete the currently picked player out of the array
   _allPlayers = _allPlayers - [_currPlayer];
};

// Now iterate over the other players and assign them a default role
{
   _x setVariable ["currentRole", "innocent", true];
} forEach _allPlayers;

// just a short nap to make sure the variables are being synched successfully
sleep 2;

roleInfo = true;
publicVariable "roleInfo"; 

// For the case that we're the server but don't host the mission on a dedicated one
if (!isDedicated && isServer) then
{
   call outputCurrentRole;
};

And into your init.sqf:

if (!isDedicated) then
{
   outputCurrentRole = {
       _role = player getVariable ["currentRole", "innocent"];
       _output = switch (_role) do 
       {
           case "traitor": {"You are a traitor! Your goal is to stay alive and make sure the others don't!"};
           default {"You're an innocent lamb, try to survive and take care of the wolfs amongst your flock!"};
       };

       hint _output;
   };
   "roleInfo" addPublicVariableEventHandler outputCurrentRole;
};

The last thing you have to do is executing "assignRoles.sqf" when enough players have connected.

if (isServer) then 
{
   execVM "assignRoles.sqf";
};

Disclaimer: I didn't test any of these. I just came up with the codes so there could be errors in them.

Also this is just covering the basics, other important steps aren't considered in my post.

Share this post


Link to post
Share on other sites

Wow.

I knew that the Arma community was a helpful bunch, but your post pretty much exceeded all expectations I had when I made this thread. I took me some time to work through it; you're right, I'm not very advanced regarding MP scripting in Arma, or rather scripting in Arma at all. You were close regarding my intentions as well. I'm not trying to convert TTT to Arma 2, but something somewhat similar. I'm a fan of the board/online game Mafia. If you're not familiar with that, it's a game where every player gets assigned a role out of many that only he knows. Some of them share their goals, but most roles have their very own, which forces players to betray, lie and deceit each other. It's a bit more extensive and in-depth than TTT, not even meant to be played in an environment like Arma 2, but I have a few ideas on how to transport the concept and basic rules of it into a military simulator. Eventually I'll find out whether it works or not.

Alright, let's start. The tip about not using exitWith is probably very valuable, or will be once I get far enough with this. It's memorized. I've also had the wrong idea about running the whole script on the server, because I thought I could just let the server do all the calculations, get the assigned roles and tell them to each player, but the way you did it makes a lot more sense; what I thought was just "telling the clients the answer" was pretty much having them execute that part of the script. Regarding the

(_traitor1 == _traitor1)

,

I honestly didn't think that much about it. I was looking for a way to display a hint that is not global but just for one player, discovered an answer to that problem in this very forum, tried it out with a brief example and got it working. But let's get into the code. I had a field day with what you posted; whenever I read up something in the BIKI, I quickly skipped any part that talked about Event Handlers because they sounded rather nasty and hard to grasp, meaning there was a lot of knowledge I had to dig up in order to keep up with you. I do understand what's happening in general, but there's a few little details I'm stuck on.

The first one is simple and just about syntax:

_allPlayers = +(playableUnits);

Why the plus in front of (playableUnits)? This is the same I tried to do initially, squeezing the aray into a variable, but I don't understand the plus. Shouldn't 'adding' the array playableUnits produces be the same as just assigning it to a variable?

_currPlayer = objNull; 

Why did you do this? Just to initialize a variable without a value? Is that really necessary, couldn't you have just skipped this line and started using the variable without it?

  _currPlayer setVariable ["currentRole", "traitor", true]; 

I never used setVariable before, but I understand the logic behind it. Somewhat. If I were to translate that part, does it mean _currPlayer turned into the variable _currentRole with the value "traitor"? Is there no need to add an underscore to make sure it stays a local variable, or do they stay local unless you use publicVariable? Actually that can't be, you did use setVariable again a few lines later again and there can't be two variables called _currentRole. So what's happening here exactly? Are both "currentRole" and "traitor" values? But that can't be, a variable can have only one value. So what variable exactly is set to what here?

Now I've said before that I think of Event Handlers as mysterious, nasty things, and the last part of your script doesn't really do anything to convince me otherwise.

"roleInfo" addPublicVariableEventHandler {
   _role = player getVariable ["currentRole", "innocent"];
   _output = switch (_role) do 
   {
       case "traitor": {"You are a traitor! Your goal is to stay alive and make sure the others don't!"};
       default {"You're an innocent lamb, try to survive and take care of the wolfs amongst your flock!"};
   };

   hint _output;
}; 

I do understand the switch function, but it's really hard for me to grasp what exactly is happening here, since there's getVariable again, on top of an EH. If you could explain that part a little bit further, I'd be very thankful, because I don't even know how to properly put the questions I have about it into words. Also that part of the code didn't even seem like it made it into the final script like this; when you posted again at the end, it turned into

"roleInfo" addPublicVariableEventHandler outputCurrentRole; 

Now you're pretty much a genius in my book, because I tested the script and it actually works. It does what it should without any complaints or problems. How you managed to do that on the fly and without even testing it yourself is a mystery to me, but I guess I'm just talking to someone with a lot of experience.

Thank you again for your whole post, I really appreciate it. I did not expect things to become that complicated so early on, but I'll stick with it anyways. Your post was worth one or two Arma 2 scripting lessons at least and I feel a bit more comfortable now, even though there were still some parts that confused me.

Cheers,

ScotchRT

Share this post


Link to post
Share on other sites
Wow.

I've also had the wrong idea about running the whole script on the server, because I thought I could just let the server do all the calculations, get the assigned roles and tell them to each player, but the way you did it makes a lot more sense; what I thought was just "telling the clients the answer" was pretty much having them execute that part of the script.

Your idea was spot on!

The server is the only machine which can do all this calculations because if every player did this on his own machine, you'd have to synchronize every role with each player and that would lead to a slow, crucifyingly death :D

The first one is simple and just about syntax:

_allPlayers = +(playableUnits);

Why the plus in front of (playableUnits)? This is the same I tried to do initially, squeezing the aray into a variable, but I don't understand the plus. Shouldn't 'adding' the array playableUnits produces be the same as just assigning it to a variable?

Well, as a computer scientist I do much programming in several languages and therefore know the aspects of pointers and references.

Basically every abstract datatype you're assigning to any variable is a reference (an address leading) to that data.

Every change you create using the variable will also change the data it's referencing.

Here's a short example:

_array1 = [1,2,3,4,5];
_array2 = _array1;

_array2 set [0, 10];

hint str _array1;  // [10, 2, 3, 4, 5]

As you can see we just changed a value in _array2, but the change was made to _array1, because _array2 just references _array1.

If we, however, put a plus infront of the assignment, we can actually copy the data to _array2 without using a reference:

_array2 = +(_array1);

Now the change we made to _array2 will stay withing _array2, with _array1 being equal to the first state.

_currPlayer = objNull; 

Why did you do this? Just to initialize a variable without a value? Is that really necessary, couldn't you have just skipped this line and started using the variable without it?

Surely you could have skipped this part.

Using a scripting language really helps you creating straitghforward stuff without having to worry about memory usage, data types and other stuff a programmer has to be aware of.

But it's "good manners" to declare the variables you're going to use before you're using them and initialize them with a default value.

This also helps preventing errors due to incompatible data types and scope limitations.

Especially the scope limitation would be ineffective here because of:

// Upper scope
{
   // inner scope

   _currPlayer = ....;  // creating the variable here and assigning a value

   // using the variable

  // upon leaving this scope, the variable get's destroyed
};

Using a for-loop means entering and exiting the above depicted "inner scope" in every cycle, so the variable would get created and destroyed in every cycle, which can be prevented when declaring the variable in the upper scope.

Please bear in mind that this fact applies to every highlevel programming language out there, I don't know if ArmA-Script also behaves like that but I strongly believe it is :D (after all ArmA-Script get's interpreted by the game engine which was written in such a high level language).

  _currPlayer setVariable ["currentRole", "traitor", true]; 

I never used setVariable before, but I understand the logic behind it. Somewhat. If I were to translate that part, does it mean _currPlayer turned into the variable _currentRole with the value "traitor"? Is there no need to add an underscore to make sure it stays a local variable, or do they stay local unless you use publicVariable? Actually that can't be, you did use setVariable again a few lines later again and there can't be two variables called _currentRole. So what's happening here exactly? Are both "currentRole" and "traitor" values? But that can't be, a variable can have only one value. So what variable exactly is set to what here?

Okay, what I did here was using the reference to the player and setting a variable on them.

Let's get that step for step:

_currPlayer = objNull; // initialized with a null-value 

// Now choose a random player out of the array
_currPlayer = _allPlayers call BIS_fnc_selectRandom;

// This translates to: _currPlayer --> B: 1-1-B:2 (Anderson) ; as an example.

So what we have here is a local variable which points to a player. We already learned that using the variable which holds the reference is equal to using the data which is referenced.

Using setVariable on the local variable is equal to using:

somePlayer setVariable [...];

This means we're attaching an attribute named "currentRole" with the accompanied value to the object (in this case the randomly chosen player).

Additionally setting the third parameter of setVariable to true will sync the value the object holds for every client.

So using getVariable for the player on any client will get the value which was assigned in the server to them :D

After assigning all the traitor-roles, we're cycling through the rest of the array and assigning a default role to everyone else.

If we didn't do this, all of the players would be traitors at some point in your game because we didn't reset the roles.

Now I've said before that I think of Event Handlers as mysterious, nasty things, and the last part of your script doesn't really do anything to convince me otherwise.

"roleInfo" addPublicVariableEventHandler {
   _role = player getVariable ["currentRole", "innocent"];
   _output = switch (_role) do 
   {
       case "traitor": {"You are a traitor! Your goal is to stay alive and make sure the others don't!"};
       default {"You're an innocent lamb, try to survive and take care of the wolfs amongst your flock!"};
   };

   hint _output;
}; 

I do understand the switch function, but it's really hard for me to grasp what exactly is happening here, since there's getVariable again, on top of an EH. If you could explain that part a little bit further, I'd be very thankful, because I don't even know how to properly put the questions I have about it into words. Also that part of the code didn't even seem like it made it into the final script like this; when you posted again at the end, it turned into

"roleInfo" addPublicVariableEventHandler outputCurrentRole; 

Okay, that was my fault, I didn't even comment something here so it could look like a "deus ex machina" doing it's work, but without knowing why :D

The topic about publicVariables and the eventhandlers is rather long and wouldn't fit in here. So I'm gonna do this the short way :D

The command publicVariable is used to synchronize the value of a public variable (those without underscore) in any client to everyone else.

This is useful if you have to keep track of overall kills or other data which should be shared to every player (the server is also included as a recipient).

The problem here is: Even if the variable gets broadcasted over the network to every machine and changes the value of that variable for everyone (or creates it if it didn't exist before), no one will notice the change if not manually checked in certain time intervals. This would however lead to unnecessary polling, which is an evil thing in every programming language :D

Therefore the devs of ArmA came up with the idea to use an event handler which asynchronusly get's called whenever a change of a public variable is noticed over the network.

We can "misuse" this fact and divert it from it's inteded use to create something like a trigger. We can use a public variable to trigger the eventhandler, which we beforehand defined in the init.sqf for every client (thus checking for isDedicated to make sure that we define the PVEH [public variable event handler] only for players).

And this is exactly what we do here. After assigning all the roles in the server we're using publicVariable to broadcast roleInfo, an unimportant variable which just exists for the sake of triggering the PVEH.

This translates to: "I'm the server! I assigned all the roles. To all clients: here I present you roleInfo, change the value of this variable accordingly and while you're at it, trigger the PVEH if you defined one".

So whenever the PVEH get's triggered, we know that the server did it's thing and we can savely ask for our own role using getVariable. Remember, we used "true" for the third parameter while using setVariable, if we didn't do that, we wouldn't be able to use getVariable in the PVEH because the attribute attached to the player would be local to the server.

In the last step, I put the content of the PVEH into its own (global) function and supplied the PVEH with the reference (yea, another one...) to the function, which equals defining a so called "anonym function" in the PVEH.

I did this for a certain reason though!

When you're scripting for MP, you have to keep in mind that you can host your mission generally in two ways: using a dedicated server or hosting it through your client.

Let's say you chose the latter.

if (isServer && !isNull player) then
{
   // This would be true, because you're the server and a player at the same time!
};

Just asking for isServer could be dangerous exactly for this fact.

Okay, why am I telling you this you might ask?

Because of this snippet of code:

roleInfo = true;
publicVariable "roleInfo"; 

// For the case that we're the server but don't host the mission on a dedicated one
if (!isDedicated && isServer) then
{
   call outputCurrentRole;
};

The BIKI description of PVEH says: "Note that the EH is only fired on clients where the publicVariable command has not been executed, as publicVariable does not change the variable where it has been executed."

If you're the server, the PVEH won't get triggered, so we have to manually call the function. That's why I put the code into the function, so we can call it manually AND supply the PVEH with it.

Okay, that should end my explanations for now. It's up to you know to refine the scripts and add your own stuff :)

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
Sign in to follow this  

×