Author Archives: steeffeen

Eliminating Teams

So far one can win rounds by defending or destroying the goals.
Another aspect of the gameplay is eliminating the whole opponent team.


Until now players immediately respawn as soon as they are eliminated, like that you just can’t eliminate all opponents.
So it’s needed to not spawn players anymore as soon as the round has started. This leads to spectating for the rest of the round which might not be fun for some players so I also added a setting to enable respawning during rounds. (As I said this will automatically prevent winning rounds by eliminating the opponent team.)

When the script is about to spawn a player I simply check if the StartTime is over:

if (!S_EnableRoundRespawn && Now > StartTime) {
     // Round has already started
     continue;
}

Now all members of a team can be eliminated which will lead to a win for the other team.


For knowing how many players of each team are alive I will use the property ClansNbPlayersAlive of CSmMode.
In the round end conditions I check if this numbers drops to 0 for a team and appoint the respective opponent team to the round winner.

Pay attention to a case where a team doesn’t immediately lose by being eliminated!
If the attacking team already planted the bomb it can still win because the defense has to defuse the bomb in order to save the goal and win the round. Because of this I protect the attacking team from losing as long as the bomb is planted.


In my SpeedBall game mode I implemented Friendly Fire which brought the competitive play to another level because you have to care even more about each shot you make to not hit your team members.
Defusing will have the same feature because I really like it. ;)
It can be easily disabled via a new script setting (which is in fact disabled by default).

For preventing hitting team members I checked if an OnHit event represents a hit between players of the same team and discard the event if it’s the case.
From now on the script only checks it as long as the FriendlyFire setting is disabled. While it’s enabled the events will still be handled and passed on leading to damage for the victim.

if (!S_FriendlyFire && Event.Shooter.CurrentClan == Event.Victim.CurrentClan) {
     // Invalid Team Hit
     Discard(Event);
     continue;
}

As players who are hitting their own team members shouldn’t be rewarded for that action I grant them negative points leading to a loss of total points.

if (Event.Shooter.CurrentClan == Event.Victim.CurrentClan) {
     // Team Hit
     Points *= -1;
}

That’s all that needs to be done for friendly fire! An easy but great feature.


During testing the mode you might have noticed that you can’t press backspace yet.
That’s caused by the fact that I Discard() all events that I don’t explicitly handle.
Whenever a player presses backspace there is an OnPlayerRequestRespawn event risen which I from now on simply PassOn().

case CSmModeEvent::EType::OnPlayerRequestRespawn: {
     // Player wants to be despawned
     PassOn(Event);
}

Now you can be despawned by pressing backspace. Keep in mind that you will have to spectate for the rest of the round if the respawning during rounds is disabled. ;)


I’ve also added some delay between rounds to let players breathe a bit. ;)

MB_Sleep(1000);

The function MB_Sleep() is implemented in the ModeBase (a general hint for that is the MB_ prefix) and lets the script wait for the given amount of milliseconds.


You can find the new version of the script here: Defusing6.Script.txt

Have fun eliminating your opponents (and not your team members! :P).

Did you know API-Arrays?

One strange thing about ManiaScript is that built-in API objects and arrays sometimes behave differently than the ones created by users.

Here’s one such behavior about arrays:
They have two types of keys.

Let me explain that with an example: The Players arrays in CSmMode. (It applies to TM as well.)
This arrays holds several player objects which (like all ManiaScript objects) have their own Ids.

In order to take a specific player object out of the array you can use the player’s Id to reference it.
Additionally you can take the Index of the object in the array.

declare Ident SomeId;
declare PlayerById <=> Players[SomeId];

declare Integer SomeIndex;
declare PlayerByIndex <=> Players[SomeIndex];

This can be really helpful. For example the Scores array gets sorted by points so you can access the best score via:

declare BestScore <=> Scores[0];

(Because the object with the index 0 has the most points.)

There’s one thing that you have to keep in mind though.
When using the Id to access an element of such an array which has an Integer key by default: It just searches for the object from beginning to the end, it’s not as performant as an actual key (with a lookup-table).

Unfortunately this is an API only feature. Your own arrays don’t support these two key-types.
You can only access their elements with a key of the type which you’ve set when declaring the array.

Greetings,
steeffeen

Taking Home Victory

There is already quite some stuff possible in Defusing but one important thing about the game play that’s still missing is the actual winning of rounds in order to win with your team.
Today I’m going to show you how to realise that.


First of all I’ve included a Nadeo script called “Victory.Script.txt”. It’s very useful in order to manage the winners of rounds and matches.
For that it’s necessary to call a few functions of the script at certain points of the game execution, e.g. when a round begins so that it can reset the former round winners.
Whenever you want to use another script (library) you will have to check if there are such functions.


In order to allow players or server admins to customise the way matches are played I’ve added several settings.
For example there are settings for the duration of a round and the number of rounds a team has to win in order to win a whole map.
Additionally there is a setting called “S_BombExplosionTime” which will define how long an activated bomb takes to finally explode.

Any setting that defines a time (duration) uses seconds or minutes as units in order to make it easy for the users. Keep in mind that you have to multiply the setting values with 1000 (and 60) in order to get the amount of milliseconds which the script is using.


The amounts of won rounds are saved in the array “ClanScores” (from CSmMode). Via the keys 1 and 2 you can set and get the round scores for the Blue and Red team. They will also be shown on the scoreboard (if you use this array).
When a map begins I reset these scores to 0.


Rounds end if the time runs out, if the bomb has been defused or if it exploded. By setting the “EndTime” property the countdown at the top is ticking down until this specific point. That’s done at the beginning of each round and as soon as the bomb has been activated which lets the clock show the time left until the bomb explodes.

If the bomb has been completely defused in-time the round winner is set to the defending team (using the Victory script).
Furthermore if the time runs out the winner is set based on the bomb activation status (activated bomb means that the bomb exploded, so the attacking team won).

If a round winner has been found the variable “MB_StopRound” of the ModeBase script is set to True which lets the round end so that the script enters the “EndRound” label.
In this label’s implementation I announce the winning team and grant the score.
Also there is a check done if the whole match is won by now.


Please note that the score summary overlay at the top showing the team names and their scores isn’t updated automatically. The class CUIConfig (representing player UIs) has various properties in order to set the data for the summary.
Each time the team scores changed the data is updated via my method “UpdateClanScoresSummary”.

The overlay needs the team points and a player Id for each team to fill the color and name.
Via the Boolean “OverlayScoreSummary” you can finally turn the complete overlay on and off.


For the current version of the game mode I’ve also implemented a label called “LogVersion” which is declared in the ModeBase. Inside it I simply log the name and the version of my own script and each library script I’ve included.

Please find the new version of the script here: Defusing5.Script.txt

Cheerio,
steeffeen

Capturing

One thing about ShootMania that I think is really rewarding is the sound of a captured goal.
So lets take a look at how we can get this capture satisfaction. :)

Important to know is the fact that poles don’t get captured on their own as soon as a player steps on them.
You have to check for this circumstance and set the capture speed of the pole manually.


The goal posts of a map are accessible via the API array BlockPoles and have the class CSmBlockPole.
For managing the capturing progress they have their own CSmGauges.

In the Defusing game mode the attacking team has to plant the bomb and the defending team has to defuse it afterwards. For now I will only focus on capturing and defusing, without actually winning the round.

The bomb will be represented by an Ident variable that holds the id of the (imaginary) bomb carrier. As long as it has not yet been planted the variable has the value NullId. When a pole has been captured the variable will hold the Id of the pole so that I know where the bomb is located. After this point other goals will be ignored and will stay inactive.

Of course only members of the attacking team can plant the bomb (later only the single player carrying the bomb). In Counter-Strike the planting gets aborted as soon as you move. In my ShootMania version I will abort the capturing when the attackers leave the goal posts.
Defusing is only possible by the defending team (the rule about progress abortion applies here as well).

In order to check if a specific player is standing on the goal you can check the Sector of the pole object. It has an array of player ids "PlayersIds".
I simply loop through the array and check which kind of player is standing on the pole at which moment.

if (Player.CurrentClan == Pole.Gauge.Clan) {
// Attacker
}

So I can set the capture Speed to a positive value when an attacker is trying to capture and to a negative value when a defender is trying to defuse the bomb.
For the abortion of bomb activation/defusing I reset the capture progress as soon as no attacker/defender is standing on the pole.

The capture Speed is calculated based on the Max value of the Gauge and the capture time setting I’ve introduced in this version. Via settings players can modify the gameplay parameters to their liking.

Speed = MathLib::NearestInteger(Pole.Gauge.Max / (S_ActivationTime * 1000.));

Pole.Gauge.Max is the maximal value the gauge has to reach in order to actually capture the goal, it’s set to a high value at the beginning of the map.
S_ActivationTime is the setting defining the duration for capturing, it’s set to 5 seconds by default.
* 1000. is necessary to convert the amount of seconds to milliseconds with which the script is working.

In order to defuse an activated bomb defenders have to decapture poles longer than it takes to activate them in the first place. That’s managed by another setting, for now its default value is 1.5 so that defusing takes 1.5x the time as activating. So the activation time is multiplied with S_DefusingTimeFactor and changed to negative for inverting the capture progress.

With each yield; the current value of a Gauge is modified by its Speed value so that after some time the value reaches its Max and an OnCapture event is risen. By handling this event I know that the pole has been captured and I don’t have to check for (Value == Max).


Additionally I’ve set a constant called “CompatibleMapTypes” which restricts the amount of compatible maps. So in my case I’ve set it to “DefusingArena”. You can use several different map types by listing them separated with commas (example: “DefusingArena,RoyalArena,EliteArena”)

There are a few optional constants that you can set for the script.
– Version (shown for example on the server-join-screen),
– CompatibleMapTypes (defining the compatible maps)
– Description (shown in the server-join-screen and via the ingame menu “Help”).


In case you think: “Hey, Bomb Defusal in Counter-Strike doesn’t work like that!”
Yes, I know. The details of the game mode will come with time. It makes sense to focus on a properly working variant before paying attention to details of the specific gameplay.

Without requirements or design, programming is the art of adding bugs to an empty text file.

– Louis Srygley

You can find the current version of the game mode script here: Defusing4.Script.txt

Greetings,
Steff

Preparing the Playground

Having built the map type and a first script test map I will let the mode load and prepare the map properly and let players spawn in their correct spawns.

When the match is about to start I call my own function PrepareMap(). The function initialises the “Bases” of the map and collects all spawns and goals in global arrays.
I save the blocks because then I don’t have to search for them when I need them: For example when I want to check if a player is planting the bomb.
Pay attention to the fact that instead of saving the blocks I only save their ids. It’s an important approach that I will explain in another post.

The ids of the spawn blocks are saved in a two-dimensional array according to their orders which have been set by the map editor while building the map.
So I know which spawns belong to which team.

As bomb spots are represented by goal posts I save their ids as well in an additional array. This one doesn’t need to have any special keys because during each round only one team attacks and tries to plant the bomb while the other team needs to defend. There is no “owning” of goals involved.

Another global variable is the number of the currently attacking team.
This number is generated randomly for the first round of each match and switches between 1 (blue) and 2 (red) before each following round.
So Defusing will for now let the attacking and defending team switch while in the Bomb Defuse mode of Counter Strike you choose your team at the beginning of a match and you will always defend or attack based on that choice.
Later I will probably add a script setting that allows you to use the Counter Strike game play.

In order to properly switch sides I switch the spawn arrays. That may be difficult to understand so I will take a closer look at the idea behind it.

Imagine the spawns array like that:
G_ClanSpawnIds =
[1 => [SpawnId11, SpawnId12], // Spawns Team 1
2 => [SpawnId21, SpawnId22]]; // Spawns Team 2

This is how the array looks after the PrepareMap() function. In the array saved for the key 1 spawn ids for the attacking side are saved while key 2 holds the defender spawns.

Start: The blue team (clan 1) attacks. When I need to spawn a player I can simply take the array referenced by the current clan of the player.

declare SpawnIds = G_ClanSpawnIds[_Player.RequestedClan];

Now I can easily choose one out these spawn ids in order to find a correct spawn for the player.

// Choose a random id out of the array
declare SpawnId = G_ClanSpawnIds[MathLib::Rand(0, G_ClanSpawnIds.count-1)];

// Take the spawn block with the chosen id
declare Spawn <=> BlockSpawns[SpawnId];

The round ends.
For taking all necessary steps before the next round starts if have a function PrepareRound().
An example is that teams need to switch sides, so in order to accomplish that I will simply switch the spawn arrays saved for the keys 1 & 2.

declare Temp = G_ClanSpawnIds[1];
G_ClanSpawnIds[1] = G_ClanSpawnIds[2];
G_ClanSpawnIds[2] = Temp;

Thanks to these 3 lines I don’t have to worry about finding a proper spawn for a player during the running match because I can simply refer to the array of spawns referenced by her/his current/requested clan number.

Additionally to collecting and managing the spawns and poles I constantly update the Clan of the Bases.
So what exactly are “Bases”?
Well, when a map is loaded the game itself automatically connects blocks and pools them into Bases. The blocks considered for these collections are all block types that have lights or colourable elements on them. Especially Spawn, Goals, Heal/Laser/Arrow Pads, Speed Paths and similar stuff.

The class CSmBase has a property Clan that can be used to colour the blocks connected by that base. This colouring gives the map a better look as you spawn in or around blocks that are coloured like your own team. You feel like actually defending your own base instead of just fighting for no reason. That’s why I always try to pay attention to proper colouring of bases.

Each CSmBase needs to be “activated” first though. That’s easily done by setting it active during the preparation of maps. It will turn on the lights of the blocks so that you will be able to see their colours.

Base.IsActive = True;
Base.Clan = 1; // Base will have color of Team 1 (usually Blue)

Now we got a proper map on which the action will take place. Next step will be to actually let the action begin. :)
You can find the current version of the script here: Defusing3.Script.txt

It’s probably quite difficult to understand the steps I’m taking only by reading the posts. A suggestion by me would be to actually load up the script ingame and analyse what the lines I added do. Maybe even try to play around with other values like switching the Base coloring by changing their “Clan”.

I’m sorry if anything is badly described. I still need to figure out which parts I have to explain extensively and which I can leave behind.

Cheerio,
steeffeen

Map Crafting

Today I’ll show you the basic idea behind MapType scripts.
These are necessary in order to build maps that you can use for your game mode. You can’t just use any map as there are usually some requirements a map needs to implement. In the case of Defusing 2 different spawns for the teams and at least one bomb goal will be needed.

So I created the map type script accordingly which you can find here: DefusingArena.Script.txt
I called it DefusingArena because that’s the pattern for map types.

For the map type script I don’t extend another script because it isn’t very complex.

Some important facts that need to be mentioned:

#RequireContext CSmMapType

This directive defines the class in which the script is running. For the game mode the ModeBase script does this assignment for CSmMode. It’s a necessary statement in order to run the script as a map type or a game mode.

CustomEditAnchorData = True;

Enables the editing of anchors. In the map type you have access to anchors that hold metadata about spawns and goals. So you can enable the map makers to assign objectives like the corresponding team of spawns and names of goals.

yield;

yield; causes the script to sleep for a moment. It’s basically a server tick. During yield the environment variables are updated like for example the PendingEvents.
Keep in mind that events are only available for one single server tick, that’s why I process them after each tick in order not to “overlook” one.
In the game mode script yield is called inside the ModeBase script so we don’t have to worry about it.

In my own function UpdateValidability() I check for the presense of all necessary spawns and goals. It’s called whenever the map is modified. By updating the ValidationStatus of a map the flag at the bottom right of the map editor is changed accordingly.
It’s a key feature of map types as invalid maps can’t be used for the game mode and therefore the map type script requires the map maker to build a valid and usable map.

ValidabilityRequirementsMessage = "Error message";
ValidationStatus = CSmMapType::ValidationStatus::NotValidable;

These statements turn the flag red and show an error message when the user clicks on the red flag. That’s useful in order to show the map maker what’s wrong with his map so that he can fix it.
If all requirements are met I set the ValidationStatus to Validated which will turn the flag green.

When the map maker switches into anchor editing mode and clicks on an anchor there’s an EditAnchor event risen.
I handle the event by showing a manialink that offers possibilities to the metadata of spawns or goals.

The manialink is running an additional script which will need to communicate with the actual map type script.

For this process key-value-coding is used. The map type declares variables for the object ManialinkPage that is also available for the manialink script.
As soon as the user clicks on a button like 1 or 2 (representing the team assigned to a spawn) a variable is changed by the manialink script and the map type script recognises it because it’s observing the variable. This key-value-variable could be understood as a “shared” variable between the map type script itself and the manialink script.

The new value will then be assigned to the anchor and when the map is saved and loaded by a game mode the spawn and goal blocks will have the set .Order and .Tag

I will take a closer look on manialink scripts later.

For the development I always build a “ScriptTestMap” for the specific mode. This map serves as a playground for the coming work on the game mode. It usually only has the needed amount of spawns and goal so that you can reach them really quickly and don’t have to run from one side of a big map to the other in order to capture a goal for example.

Closing up I would like to point you to the Links Page I’ve created on which you can find further information about ManiaScript. Especially the ManiaScript Class Documentation is very helpful. It can be generated with the ManiaPlanet game client and covers all available classes and their properties.

For example you can find the validation variables in the class CMapType in case you forget them.
I myself use the documentation all the time in order to look up how exactly the properties are called and for looking up which other useful values the classes offer.

Cheerio,
steeffeen