[Tutorial] Creating a basic dynamic multi-race system
#1

Creating a basic multi-race system

Introduction
Hey there guys, it's been a while since I've done pretty much anything to do with sa-mp scripting. Recently I felt like editing one of my old tutorials but someone requested that I make a race tutorial and seeing that there wasn't one at all, I thought I might as well. This tutorial was done over 2 days and some of it may be a bit hard to understand so just point out what needs fixing and I'll do it. I've tried to explain what every bit of code does so you can change the code to what you want and make you're own race system to your needs rather than copying and pasting the code :P.

I also want to note that this script does use quite large arrays but there are defines which you can change so that they are only the size you need them to be. In the title I have used the word "multi-race" because you can load multiple races into the script and multiple races can be running at the same time. This is probably not as efficient as it would be if it only ran one at a time but it's so much more awesome. With carefully crafted commands you could also add races while you're in game if you wanted to. I will also at the end of this tutorial show you how to add races easily to the script. Lets get started!

What do I need?
You don't really need that much for this tutorial. You can either intergrate it with your gamemode or make it a filterscript. Here is what you will need though:
  • A PAWN editor with the compiler
  • The stock sa-mp includes (a_samp.inc)
  • A command processor (I'm using ZCMD) in this tutorial
Lets get started!


We always start off with the includes and defines. As you can see in the comments below, the lesser the MAX_RACES and the lesser the MAX_CHECKPOINTS, the smaller array sizes the script will create. The rest of it pretty much explains itself really.
pawn Код:
#include <a_samp> //I'm pretty sure we all know what this line does. It adds the sa-mp natives :).
#include <zcmd> //Could be y_cmd or something else. It's not hard to adapt the code but most people currently use ZCMD.
#define MAX_RACES 25 //Maximum races will be 50, the lower the better due to array sizes - Change this higher if you need to
#define MAX_CHECKPOINTS 150 //Maximum checkpoints will be 100, the lower the better due to array sizes - Change this higher if you need to
#define WAIT_RACE 15 //The time in seconds between the opening of the race to the time that it starts

The code below is our enum and array. An enum is pretty much just an easier way for us to store a mass amount of information that is allocated to a specific variable. It's far easier using an enum then creating multiple different variables. You can find more information on what an enum is on the sa-mp wiki. This variable with the enum holds all the information related to races including dynamic and static information from checkpoint locations to if a race is currently running. More information is in the comments in the code about specific parts of the enum. The currentraceslot variable tells us how many races we have stored in the array and what slot we can save the next race into. Without this variable we would need to use a loop to find the next free slot in the array.
pawn Код:
enum rInfo
{
    //This enum is for anything related to the races themselves.
    racetimeout,
    //The above varible is the timeout in seconds before a race stops. We
    //use this so the lovely people that decide to just sit in a race don't
    //cause it to run forever.
    Float:checkpointx[MAX_CHECKPOINTS],
    Float:checkpointy[MAX_CHECKPOINTS],
    Float:checkpointz[MAX_CHECKPOINTS],
    //These will be the float positions of the checkpoints in a race.
    //All checkpoint positions will load into these arrays on start-up.
    racename[100 char],
    //We use char to lower the array size to make the script more efficient
    //and use less memory that the sa-mp server could use for doing other things
    //We can use chars on strings because their values do not exceed 255.
    //For more information on char visit:
    //For example if racename[0] = 'a' then racename[0] would actually
    //be racename[0] = 97. This will reduce our array size by 4x. If you are
    //going to have race names over 100 characters then you should change
    //the 100 to a higher value.
    cpnum,
    //We need to know how many slots are currently being used in
    //the checkpoints array. We could use a loop everytime we
    //wanted to add more checkpoints to the array but this would
    //be very time wasting and pointless. the cpnum variable
    //gives us the amount of checkpoints in a race and the next
    //free cell in the checkpoints variable
    racevehicle,
    //The racevehicle variable isn't really needed but we will add it anyway.
    //This variable allows us to specify a specific vehicle for the race
    //such as an NRG or a specific boat if you're in a boat race. Later on
    //we will make it so that if the variable is 0 then there is no specific
    //vehicle needed.
    bool:racerunning,
    //The above is just a boolean that is true if the race is currently
    //running and false if it's not. We set it to true on race start and
    //false when the race is stopped.
    bool:racejoinable,
    //This variable is different to the one above because this is used if the
    //race is open but hasn't started yet. If the race hasn't started, this would
    //be true and racerunning would be false but if the race had started it would
    //be the oppisite.
    racetimer,
    //This is the timer id for the timeout for the race. This will be dynamic
    //and will change whenever the race is given a timer and set to start.
    //We need this variable so that we can cancel the timer if there is no
    //players before the timeout of the race.
    racecount,
    //This variable is to show the amount of players that are currently
    //in the race so when it hits 0 we can end the race if it hasn't
    //already timed out.
    originalcount,
    //This variable is static from the start to the end of the race because
    //it gives us the amount of people in the race when it started. This is used
    //for positioning in the race combined with the race count. It wouldn't be a
    //race if we didn't have the race positions at the end :).
    racetime
    //This variable is just used to determine how long the race has been running
    //for and it will allow us to use the timeout timer as a double for the
    //time people have spent in the race. The race timer will run at 100ms and
    //give us our race time accurate to .1 of a second. When the race timer reaches
    //the timeout (if it does) then it will exit. It will be easier to understand
    //the code when you see it :P.
};

new RaceInfo[MAX_RACES][rInfo];
//The lovely variable that holds all race information required
//For operating the races.

new currentraceslot;
//The above variable tells us how many races we have loaded and the
//next available slot that we can use in the array. This is very useful
//so we don't have to create a loop to find this information.

The below code is just an array that we are going to use in one case if a race has a specified vehicle that must be used. This array holds all vehicle names that can be easily accessed by the code when it is running.
pawn Код:
new VehicleNames[212][] =
{
    "Landstalker",  "Bravura",  "Buffalo", "Linerunner", "Perennial", "Sentinel",
    "Dumper",  "Firetruck" ,  "Trashmaster" ,  "Stretch",  "Manana",  "Infernus",
    "Voodoo", "Pony",  "Mule", "Cheetah", "Ambulance",  "Leviathan",  "Moonbeam",
    "Esperanto", "Taxi",  "Washington",  "Bobcat",  "Mr Whoopee", "BF Injection",
    "Hunter", "Premier",  "Enforcer",  "Securicar", "Banshee", "Predator", "Bus",
    "Rhino",  "Barracks",  "Hotknife",  "Trailer",  "Previon", "Coach", "Cabbie",
    "Stallion", "Rumpo", "RC Bandit",  "Romero", "Packer", "Monster",  "Admiral",
    "Squalo", "Seasparrow", "Pizzaboy", "Tram", "Trailer",  "Turismo", "Speeder",
    "Reefer", "Tropic", "Flatbed","Yankee", "Caddy", "Solair","Berkley's RC Van",
    "Skimmer", "PCJ-600", "Faggio", "Freeway", "RC Baron","RC Raider","Glendale",
    "Oceanic", "Sanchez", "Sparrow",  "Patriot", "Quad",  "Coastguard", "Dinghy",
    "Hermes", "Sabre", "Rustler", "ZR-350", "Walton",  "Regina",  "Comet", "BMX",
    "Burrito", "Camper", "Marquis", "Baggage", "Dozer","Maverick","News Chopper",
    "Rancher", "FBI Rancher", "Virgo", "Greenwood","Jetmax","Hotring","Sandking",
    "Blista Compact", "Police Maverick", "Boxville", "Benson","Mesa","RC Goblin",
    "Hotring Racer", "Hotring Racer", "Bloodring Banger", "Rancher",  "Super GT",
    "Elegant", "Journey", "Bike", "Mountain Bike", "Beagle", "Cropdust", "Stunt",
    "Tanker", "RoadTrain", "Nebula", "Majestic", "Buccaneer", "Shamal",  "Hydra",
    "FCR-900","NRG-500","HPV1000","Cement Truck","Tow Truck","Fortune","Cadrona",
    "FBI Truck", "Willard", "Forklift","Tractor","Combine","Feltzer","Remington",
    "Slamvan", "Blade", "Freight", "Streak","Vortex","Vincent","Bullet","Clover",
    "Sadler",  "Firetruck", "Hustler", "Intruder", "Primo", "Cargobob",  "Tampa",
    "Sunrise", "Merit", "Utility Truck", "Nevada", "Yosemite", "Windsor", "Monster",
    "Monster","Uranus","Jester","Sultan","Stratum","Elegy","Raindance","RCTiger",
    "Flash","Tahoma","Savanna", "Bandito", "Freight", "Trailer", "Kart", "Mower",
    "Dune", "Sweeper", "Broadway", "Tornado", "AT-400",  "DFT-30", "Huntley",
    "Stafford", "BF-400", "Newsvan","Tug","Trailer","Emperor","Wayfarer","Euros",
    "Hotdog", "Club", "Trailer", "Trailer","Andromada","Dodo","RC Cam", "Launch",
    "Police Car LSPD", "Police Car SFPD","Police Car LVPD","Police Ranger",
    "Picador",   "S.W.A.T. Van",  "Alpha",   "Phoenix",   "Glendale",   "Sadler",
    "Luggage Trailer","Luggage Trailer","Stair Trailer", "Boxville", "Farm Plow",
    "Utility Trailer"
};
//A lovely array of all vehicle names by ?. This allows us to tell a person what
//vehicle a race requires if it requires one.

The next two variables are our player information variables. The RaceCheckpoint variable is used to tell us what RaceCheckpoint a player is currently at in a race so that we can set the correct checkpoint when it is needed. The InRace variable tells us if a player is in a race and if they are it will also give us the players race id. If the player is not in a race then this will be set to -1. You can easily use this variable to integrate parts of your code with this race system such as not allowing a player to teleport while in a race etc.
pawn Код:
new RaceCheckpoint[MAX_PLAYERS];
//This variable tells us what race checkpoint that the player is currently
//in so that we can send him to the next checkpoint in the array.


new InRace[MAX_PLAYERS];
//The above will be either "true" or "false" whether the player is
//currently racing or not. This variable is useful also because you
//can integrate it into your script in other ways.

The CreateRace function is our function that we will add races into our lovely array / variable with. We can easily create a race with one line and add a checkpoint with one line as you will see in the function in the next section. Using this CreateRace function could be done on script startup or while the script is currently running. You can either specifiy a race vehicle or not (RaceVehicle = 0 for on foot) and you must specify a race timeout in seconds.
pawn Код:
stock CreateRace(RaceName[], RaceTimeout, RaceVehicle = -1)
{
    //This is the function that we will use to load our race information
    //into the variables themselves. We know that our variable currentraceslot
    //will give us the next slot that we can use freely. So lets load the
    //information that we have used in the function into our variables
    //I have put RaceVehicle = -1 so it's the default value which means that
    //we don't even need to specify it when creating a race, it's like an extra
    //thing like the draw distance of an object. You could just do the following
    //CreateRace("DEATH RACE :)", 300) to create a race with a timeout of 5 minutes
    //with any vehicle or CreateRace("DEATH RACE :)", 300, 411) for a race with
    //a specified vehicle. This will be shown more later on.
    format(RaceInfo[currentraceslot][racename], 100, "%s", RaceName);
    //We are inserting the function information of RaceName into our array
    //RaceInfo with the next avalible race slot. format just transfers one
    //string to another with ease. I was having issues with strcat for
    //some reason with char.
    RaceInfo[currentraceslot][racetimeout] = RaceTimeout;
    //The above is pretty much the same as I explained for the name.
    //We put the function information into the array. In this case
    //it's the timeout information into the timeout variable in the array
    RaceInfo[currentraceslot][racevehicle] = RaceVehicle;
    //Same as above. Loading the vehicle information into the array.
    RaceInfo[currentraceslot][racerunning] = false;
    //Sets the race running variable to false
    RaceInfo[currentraceslot][racejoinable] = false;
    //Sets the race joinable variable to false
    return currentraceslot, currentraceslot ++;
    //We return the array ID so we can use it to add checkpoints later.
    //It's the exact same thing you do with a pickup, you assign a variable
    //to the ID of the pickup so that you can identify it later. For example
    //new deathrace = CreateRace("DEATH RACE :)", 300); and we will be able to
    //add checkpoints using the variable deathrace. currentraceslot++ is just
    //adding 1 to the variable because we have used that slot.
}
This lovely function allows us to add checkpoints into the specified race ID. The first AddCheckpointToRace is counted as the starting point of the race and the last checkpoint is the finishing point.
pawn Код:
stock AddCheckpointToRace(RaceID, Float:CheckPointX, Float:CheckPointY, Float:CheckPointZ)
{
    //This function adds information into the designated array using the race
    //id that was returned from the CreateRace function. We just add information
    //to the arrays using this function rather than doing it manually because it's far
    //easier this way. An example how this would look would be
    //new deathrace = CreateRace("Death Race", 300);
    //AddCheckpointToRace(deathrace, 1432.13, 312.311, 12.1);
    //Pretty straight forward and easy to understand.
    new nextcheckpointfree = RaceInfo[RaceID][cpnum];
    //The above variable just makes it look cleaner because it gets
    //the next free checkpoint slot in the array.
    RaceInfo[RaceID][checkpointx][nextcheckpointfree] = CheckPointX;
    RaceInfo[RaceID][checkpointy][nextcheckpointfree] = CheckPointY;
    RaceInfo[RaceID][checkpointz][nextcheckpointfree] = CheckPointZ;
    //Just putting the checkpoint locations from the function
    //into the correct race array and into the correct checkpoint array.
    //The variables go into the race ID defined and the next free checkpoint
    //id in that specific race. It's like inception but less confusing :).
    RaceInfo[RaceID][cpnum] ++;
    //We add one so that the script knows that we have used that checkpoint
    //slot up and to use the next one avalible.
    return 1;
    //We don't need to return a specific value unless you plan on editing
    //the checkpoints location later so we just return 1 :).
}

The code below is pretty self explanatory because it just resets the race variables on spawn. We could do this on OnPlayerConnect although I've done it on OnPlayerSpawn to make sure they reset and it also allows you to load and unload this script without having to disconnect to reset your variables.
pawn Код:
public OnPlayerSpawn(playerid)
{
    //This code just resets our player variables (InRace and RaceCheckpoint)
    InRace[playerid] = -1;
    //InRace is -1 if the player is not in any race or it will be a value > -1
    //if the player is actually in a race. This is also quite useful because
    //We could return the name of the race a player is in as well by using
    //RaceInfo[InRace[playerid]][racename] in a string :).
    RaceCheckpoint[playerid] = -1;
    //We use RaceCheckpoint = -1 to show no checkpoint at all. We cannot
    //have it at zero because in the array the first checkpoint is at 0 on
    //the start. When the player is actually in the starting checkpoint
    //and heading to the first checkpoint then this can be set to 0. The starting
    //checkpoint is #0 and the second one is #1. It will be easier to understand
    //when you see the code.
    return 1;
}

Now to get onto the functions that are actually going to control the behaviour of the races. We will start with OpenRace which opens a race for people to join. OpenRace opens the race to players and sets a timer for the race to start in WAIT_RACE seconds which you defined back in your defines at the top of this tutorial. This function just opens the race for joining and sends a message that this race can be joined.
pawn Код:
stock OpenRace(raceid)
{
    new RaceStr[128];
    //We create this string to format the string that will be sent to all
    //players when the race is joinable.
    if(RaceInfo[raceid][racevehicle] == -1) format(RaceStr, sizeof(RaceStr), "[RACE] The Race '%s' will start in %d seconds. Type /joinrace %d to join!", RaceInfo[raceid][racename], WAIT_RACE, raceid);
    if(RaceInfo[raceid][racevehicle] > 0) format(RaceStr, sizeof(RaceStr), "[RACE] The Race '%s' will start in %d seconds. Type /joinrace %d to join! A %s is required for this race.", RaceInfo[raceid][racename], WAIT_RACE, raceid, VehicleNames[RaceInfo[raceid][racevehicle]-400]);
    if(RaceInfo[raceid][racevehicle] == 0) format(RaceStr, sizeof(RaceStr), "[RACE] The Race '%s' will start in %d seconds. Type /joinrace %d to join! This is a onfoot race only.", RaceInfo[raceid][racename], WAIT_RACE, raceid);
    //We all probably know what format is and the above formats the message to
    //send a message that the race can be joined by typing /joinrace [raceid]
    //an example message would be something like the following:
    //[RACE]The Race 'Death Race' will start in 60 seconds. Type /joinrace 1 to join
    //Or if it requires a specific vehicle then it will add that on the end too.
    SendClientMessageToAll(0xFF00AA, RaceStr);
    //Sends the formatted message in yellow. You can easily change that colour =].
    RaceInfo[raceid][racejoinable] = true;
    //The above allows players to join the race. This will be set to false
    //when the race starts.
    SetTimerEx("StartRace", WAIT_RACE*1000, false, "d", raceid);
    //The above sets the timer for the start race function. There will be 60 seconds
    //before this function is actually called. StartRace is the function name,
    //we have WAIT_RACE*1000 because WAIT_RACE is in seconds and the timers work
    //in milliseconds (1 second = 1000 milliseconds).
    //We also pass the raceid to the function 60 seconds later.
    return 1;
}

This is the function we are going to use in the blow function although we have to put this function before it. This function is used to set the player or their vehicle facing the first checkpoint from the starting checkpoint. If you don't understand this part or simply don't give a shit then copy and paste it into your code and move on to the next section .
pawn Код:
stock Float:AngleToPoint(Float:x2, Float:y2, Float:X, Float:Y)
{
    //This function by ? is used so that we can face the vehicle in the correct angle
    //when we teleport the player to a race. This isn't a real biggie but it looks more
    //awesome. Explaining this will be a bit tricky but I'll try to a basic overview.
    //If you have ever done basic trig in class you will know that with two lengths
    //we can find out an angle of a triangle
    //        /|
    //     H / | A              (Lets pretend that's a triangle)
    //      ----
    //        O
    //
    //using tan ( O / A) we can find the angle that we are meant to be facing at
    //The rest of the function just calculates the angles because we are not just
    //in a triangle, we have a circle around us. If you don't understand then it
    //is really no big issue. We use this function below.
    new Float:DX, Float:DY;
    new Float:angle;
    DX = floatabs(floatsub(x2,X));
    //Get the positive value of x2 - x (finding the length we need for the
    //'O' side or the oppisite side to the angle
    DY = floatabs(floatsub(y2,Y));
    //Get the positive value of y2 - y (finding the length we need for the
    //'A' side or the adjasent side to the angle
    if (DY == 0.0 || DX == 0.0)
    //If for some reason one length is zero then we do not need trig to complete
    //the function and we can just find the angle by determining what value is
    //the non-zero value. Pretty self explanitory.
    {
        if(DY == 0 && DX > 0) angle = 0.0;
        else if(DY == 0 && DX < 0) angle = 180.0;
        else if(DY > 0 && DX == 0) angle = 90.0;
        else if(DY < 0 && DX == 0) angle = 270.0;
        else if(DY == 0 && DX == 0) angle = 0.0;
    }
    else
    {
        angle = atan(DX/DY);
        //If we have both values (no zeros) then we have to use the TOA
        //tan(oppisite / adjacent) to find the angle required. We also have
        //to add to it to find the actual angle as if we where in a circle.
        //Sadly I cant fully explain it because it's hard to create a circle
        //but think of it as a triangle in a circle and we need to find the
        //full angle rather than just the angle of a specific segment.
        if(X > x2 && Y <= y2) angle += 90.0;
        else if(X <= x2 && Y < y2) angle = floatsub(90.0, angle);
        else if(X < x2 && Y >= y2) angle -= 90.0;
        else if(X >= x2 && Y > y2) angle = floatsub(270.0, angle);
    }
    return floatadd(angle, 90.0);
}

So the function below is the function that we will use to make a player join a specific race. This function tells a player if he is able to join a race and checks if the player passes all the checks such as not being in a race, being in the right vehicle, seeing if the race is actually join able etc and then putting him / his vehicle into the race. This function will set the player into another world as it's very annoying trying to race in the same world as everyone else is in. In the JoinRace function we also use the function above (AngleToPoint) to set the players vehicle or the players facing direction. This function also sets the players variables to in that race so that the StartRace function next can find out who is in the race with a basic loop.
pawn Код:
stock JoinRace(playerid, raceid)
{
    if(RaceInfo[raceid][racejoinable] == false) return SendClientMessage(playerid, 0xAA3333AA, "Race is currently not joinable!");
    //If the race isn't joinable then we send a message to the player that the
    //race cannot be joined right now and we stop them there.
    if(InRace[playerid] != -1) return SendClientMessage(playerid, 0xAA3333AA, "You are currently in a race! /leaverace before joining a new one!");
    //If the player is already in a race and trying to join another one it's
    //also a bit silly. The player has to first leave his current race before
    //going into another one. As you see, /leaverace will be our command
    //to leave the race :)
    if(GetVehicleModel(GetPlayerVehicleID(playerid)) != RaceInfo[raceid][racevehicle] && RaceInfo[raceid][racevehicle] != -1)
    {
        if(RaceInfo[raceid][racevehicle] == 0) return SendClientMessage(playerid, 0xAA3333AA, "You are not allowed vehicles in a foot race!");
        //If it's a foot race then it will send the player an error and stop here
        new RaceStr[75];
        //creates the string array
        format(RaceStr, sizeof(RaceStr), "You need to be in a %s to join this race!", VehicleNames[RaceInfo[raceid][racevehicle]-400]);
        //formats the string
        return SendClientMessage(playerid, 0xAA3333AA, RaceStr);
        //Or here you could always get rid of the return 1; and give the player
        //that specific vehicle that is required or for a running race, take
        //the players vehicle away from him/her. It's really up to you on
        //what you want to do here. What I have done is just denied enterance
        //to the race if the player is not in the required vehicle.
    }
    if(RaceInfo[raceid][racevehicle] == 0 && IsPlayerInAnyVehicle(playerid)) return SendClientMessage(playerid, 0xAA3333AA, "You are not allowed vehicles in this race!");
    //I just made this clause for running races or swimming races, whatever
    //floats your boat. Handy little addition and it's easy to add on. Just
    //CreateRace("Runnin' Race", 300, 0) to make a running race :).
    RaceCheckpoint[playerid] = 0;
    //Checkpoint set to 0 - First checkpoint
    InRace[playerid] = raceid;
    //Players race is set to the race he/she chose to join.
    if(IsPlayerInAnyVehicle(playerid))
    //We are going to check if the player is in any vehicle so that we can
    //then teleport that vehicle to the correct location + world. We do not
    //use world 0 as the race world as people would just ram people for the fun
    //of it.
    {

        new vehicleid = GetPlayerVehicleID(playerid);
        //We fetch the players vehicle ID
        SetVehicleVirtualWorld(vehicleid, 1050005 + raceid);
        //We set the vehicle to a virtual world with 1050005 + the raceid like
        //we are going to do with the player too. I used 1050005 so it doesn't
        //overlap any worlds used by your script (hopefully). E.g. Race 20 would
        //be in world ID 10050025 etc...
        SetPlayerVirtualWorld(playerid, 1050005 + raceid);
        //Sets the player to the same virtual world as above
        PutPlayerInVehicle(playerid, vehicleid, 0);
        //We put the player in the vehicle just in case
        SetVehiclePos(vehicleid, RaceInfo[raceid][checkpointx][0], RaceInfo[raceid][checkpointy][0], RaceInfo[raceid][checkpointz][0]);
        //We set the vehicles position in the first checkpoint waiting for the race to start
        SetVehicleZAngle(vehicleid, AngleToPoint(RaceInfo[raceid][checkpointx][0], RaceInfo[raceid][checkpointy][0], RaceInfo[raceid][checkpointx][1], RaceInfo[raceid][checkpointy][1]));
        //The code above sets the angle of the vehicle towards the second checkpoint.
       
    }
    else
    {
        SetPlayerVirtualWorld(playerid, 1050005 + raceid);
        SetPlayerFacingAngle(playerid, AngleToPoint(RaceInfo[raceid][checkpointx][0], RaceInfo[raceid][checkpointy][0], RaceInfo[raceid][checkpointx][1], RaceInfo[raceid][checkpointy][1]));
        SetPlayerPos(playerid, RaceInfo[raceid][checkpointx][0], RaceInfo[raceid][checkpointy][0], RaceInfo[raceid][checkpointz][0]);
        //Same as above with the cars. The angle of the player is set to the next
        //checkpoint so that they are not facing backwards when the race starts.
        //Actually, that would be pretty funny :P.
    }
    new RaceInfoString[75];
    format(RaceInfoString, sizeof(RaceInfoString), "~y~Joined the race! ~b~Please wait %d seconds for the race to start!", WAIT_RACE);
    //We have to format the amount of seconds before the race starts because it's
    //most likely going to change from 60 seconds that have been set.
    GameTextForPlayer(playerid, RaceInfoString, 3000, 3);
    //We send the player a nice on screen message telling them
    //that they have joined the race :).
    SetPlayerRaceCheckpoint(playerid, 0, RaceInfo[raceid][checkpointx][0], RaceInfo[raceid][checkpointy][0], RaceInfo[raceid][checkpointz][0], RaceInfo[raceid][checkpointx][1], RaceInfo[raceid][checkpointy][1], RaceInfo[raceid][checkpointz][1], 6);
    //This sets the starting checkpoint and it points to the direction that the
    //next checkpoint will be in. The player has to be in this checkpoint when
    //the race starts or they won't racing :P. Remember that our array ids start
    //at 0 rather than one.
    return 1;
}
Now to get to the fun part - the race will now start. This StartRace public function is called by a timer from the OpenRace function. The startrace function checks who is in the race and if they meet the criteria to start the race. If they are not in the checkpoint or they are in the wrong vehicle then they will be kicked from the race before it starts and that will be the end of that. If they do meet the criteria to start then their checkpoint will be set to the first race checkpoint and off they go.

pawn Код:
forward StartRace(raceid);
public StartRace(raceid)
{
    RaceInfo[raceid][racejoinable] = false;
    //We now make it so the race is no longer joinable by anyone else.
    for(new i; i<MAX_PLAYERS; i++)
    //if you have foreach then foreach (new i : Player) or make your own
    //illiterator for players in races. Just loops through everyone currently
    //that has tried to join the race
    {
        if(InRace[i] != raceid) continue;
        //skips anyone that isn't in the race that we are looking into.
        //continue just skips a certain id. For example if ID 5 wasn't in
        //our race so InRace[playerid] = -1 it would skip his ID and not check
        //any further.
        if(IsPlayerInRaceCheckpoint(i) && (GetVehicleModel(GetPlayerVehicleID(i)) == RaceInfo[raceid][racevehicle] || RaceInfo[raceid][racevehicle] == -1 ))
        //If the player is in the race we are looking at + the player is in
        //the checkpoint + the vehicle model is correct if there is one.
        {
            SetPlayerRaceCheckpoint(i, 0, RaceInfo[raceid][checkpointx][1], RaceInfo[raceid][checkpointy][1], RaceInfo[raceid][checkpointz][1], RaceInfo[raceid][checkpointx][2], RaceInfo[raceid][checkpointy][2], RaceInfo[raceid][checkpointz][2], 6);
            //The above line sets the players checkpoint from the start point
            //to the second checkpoint in the race. It also points to the direction
            //Of the third checkpoint RaceInfo[raceid][checkpoint[x][y][z]][2].
            //Remember that the first and starting checkpoint was used in the
            //Joinrace function with the index of 0. 0 is the first number in
            //our array rather than 1.
            RaceCheckpoint[i] = 2;
            //The players race checkpoint is set to checkpoint #1. As we see
            //above the race checkpoint is being set in the array as 1.
            RaceInfo[raceid][originalcount] ++;
            //We add to the originalcount so that we get the players in the race
            //This count does not change throughout the race and will be reset
            //at the end of the race on StopRace
            RaceInfo[raceid][racecount] ++;
            //This racecount variable will change whenever someone leaves the
            //race or finishes it. It helps us determine when to stop the race
            //and what position someone finishes in a certain race. This variable
            //will be changed over the course of the race.
            GameTextForPlayer(i, "~r~GO!!!!", 3000, 3);
            //We send the player a nice on screen text that tells them that
            //the race has started and they better get going :).

        }
        else
        //The above code is if we find that the player isn't even in the checkpoint
        //when the race has started or the vehicle ID specified isn't the one that
        //the race allows.
        {
            RaceCheckpoint[i] = -1;
            //Race checkpoint reset
            InRace[i] = -1;
            //Player is put out of the race
            DisablePlayerRaceCheckpoint(i);
            //Players checkpoint is disabled.
            if(IsPlayerInAnyVehicle(i))
            //If the player was in a car
            {
                new vehicleid = GetPlayerVehicleID(i);
                //We fetch the players vehicle ID
                SetVehicleVirtualWorld(vehicleid, 0);
                //We reset the vehicles virtual world to 0
                SetPlayerVirtualWorld(i, 0);
                //Sets the player to virtual world 0 too
                PutPlayerInVehicle(i, vehicleid, 0);
                //We put the player in the vehicle just in case
            }
            else
            {
                SetPlayerVirtualWorld(i, 0);
                //If the player was on foot we just put him back to virtual
                //world 0.
               
            }
        }
    }
    if(RaceInfo[raceid][originalcount] == 0) return 1;
    //The above code is executed if it's time for the race to start but we seem
    //to have nobody in the race checkpoint with the specified rules. This will
    //just stop the function and the race will not be started and that will be
    //the end of it.
    RaceInfo[raceid][racetimer] = SetTimerEx("RaceTime", 100, true, "d", raceid);
    //We set the 100ms timer that acts as our timeout timer and gives us the
    //amount of time that the race has gone on for. Some people would use the
    //function GetTickCount although if you're server has a long uptime then
    //the script would just break itself. The timer is set at 100ms and the raceid
    //is carried onto the RaceTime function.
    RaceInfo[raceid][racerunning] = true;
    //We set the variable of racerunning to true to show that the race is actually
    //running so it cant be started.
    return 1;
}
The below public function is our repeating timer. This timer sets the race timer higher (used for telling people how long they take in races) and it is also doubled up as our timeout timer which will stop the race if it has gone on too long. This timer repeats over and over every 100ms until the race finishes.

pawn Код:
forward RaceTime(raceid);
public RaceTime(raceid)
{
    RaceInfo[raceid][racetime] ++;
    //The racetime variable holds the current race time in 100ms increments
    //This allows us to give accurate race times to players at the end of the
    //race to .1 of a second.
    if(floatround((RaceInfo[raceid][racetime] / 10), floatround_floor) >= RaceInfo[raceid][racetimeout]) StopRace(raceid);
    //The above code does seem slightly confusing but it isn't. The racetimeout
    //time is in seconds and the racetime is in 0.1 of a second. We just divide
    //the race time by 10 so that we get the total race time in seconds and we
    //use the function Floatround to round downwards so we have no chance to get
    //errors when we try compare a float like this.
    return 1;
}
The StopRace function below will stop the race and reset all of it's variables. StopRace is called if the race runs out of players (they finish, leave, die etc) or if the race times out. This function will also kick anyone that's still in the race (only happens if the race times out).

pawn Код:
stock StopRace(raceid)
{
    KillTimer(RaceInfo[raceid][racetimer]);
    //We kill that timer that is going at 100ms/s acting as our timeout timer
    //and the race time timer
    RaceInfo[raceid][racetime] = 0;
    //Race time is reset to 0 for the race.
    RaceInfo[raceid][racerunning] = false;
    //Race running is set to false for the race
    RaceInfo[raceid][originalcount] = 0;
    //The originalcount is set to 0 for the race.
    if(RaceInfo[raceid][racecount] != 0)
    {
        //The only case that this would happen is a race timeout. This is if we
        //try to stop a race when there is players still in it.
        for(new i; i<MAX_PLAYERS; i++)
        //if you have foreach then foreach (new i : Player) or make your own
        //illiterator for players in races.
        {
            if(InRace[i] != raceid) continue;
            //skips anyone that isn't in the race that we are looking into.
            //continue just skips a certain id. For example if ID 5 wasn't in
            //our race so InRace[playerid] = -1 it would skip his ID and not check
            //any further.
            GameTextForPlayer(i, "~r~You have run out of time for this race!", 3000, 3);
            //Send the player a message telling them they have ran out of time for
            //the race.
            LeaveRace(i, raceid);
            //Calls LeaveRace for any player that is still in the race.
        }
        RaceInfo[raceid][racecount] = 0;
        //The racecount can now be reset as all the players have been kicked
        //from the race if they didn't make it.
    }
    return 1;
}
The LeaveRace function forces a player to leave a specified race whether the player dies, quits, leaves the race or finishes it. LeaveRace also checks if the player is the last to leave the race and cancels the race if they are. This function will be used multiple times throughout the code.
pawn Код:
stock LeaveRace(playerid, raceid)
{
    RaceCheckpoint[playerid] = -1;
    //Race checkpoint reset
    InRace[playerid] = -1;
    //Player is put out of the race
    DisablePlayerRaceCheckpoint(playerid);
    //Players checkpoint is disabled.
    if(IsPlayerInAnyVehicle(playerid))
    //If the player was in a car
    {
        new vehicleid = GetPlayerVehicleID(playerid);
        //We fetch the players vehicle ID
        SetVehicleVirtualWorld(vehicleid, 0);
        //We reset the vehicles virtual world to 0
        SetPlayerVirtualWorld(playerid, 0);
        //Sets the player to virtual world 0 too
        PutPlayerInVehicle(playerid, vehicleid, 0);
        //We put the player in the vehicle just in case
    }
    else
    {
        SetPlayerVirtualWorld(playerid, 0);
        //If the player was on foot we just put him back to virtual
        //world 0.
    }
    if(RaceInfo[raceid][racerunning] == true)
    //If the race has actually started rather than just a player waiting for the
    //race to start leaving the race.
    {
        RaceInfo[raceid][racecount] --;
        //removes a player from the racecount.
        if(RaceInfo[raceid][racecount] == 0) StopRace(raceid);
        //If there is nobody in the race after this person leaves then we have to
        //stop the race from running.
    }
    return 1;
}
Now it's finally time for the large part. What happens when a player enters a race checkpoint? It's pretty basic as long as the player is just continuing along the track of checkpoints up until the last checkpoint. When the player finishes the race a function is called which gives the player their time and position at the end of the race. Every time a player enters a checkpoint along the way a Game Text is also shown with their time and checkpoint # in the race. On race finish we also send a message to everyone telling them that the player has finished the race in x time in x/x position. Things like this can easily be changes - that's what the tutorial is about. You could change this to only send to the player rather than everyone.
pawn Код:
public OnPlayerEnterRaceCheckpoint(playerid)
{
    if(InRace[playerid] != -1)
    //This just makes sure the player is actually in a race. You may be using
    //race checkpoints for something else as well as this race system.
    {
        if(RaceCheckpoint[playerid] > 0)
        //RaceCheckpoint[playerid] = 0 is if the player is in a race but at
        //the starting checkpoint rather than actually racing. We check if the
        //player is at a checkpoint further than CP 1.
        {
            new raceid = InRace[playerid], checkpoint = RaceCheckpoint[playerid];
            //Above variables just makes it easier for us to use and understand
            //the code. We put the raceid that the player is in into the raceid
            //variable... YAY :). We also put the checkpoint id into the
            //checkpoint variable ;).
            if(checkpoint == RaceInfo[raceid][cpnum])
            //If we have the same checkpoint value as the race checkpoints before
            //we add one to our race checkpoint variable then we have gone through
            //the finishing checkpoint which means we should initiate the finish
            //race code and stop it there with a return 1;
            {
                //Player leaves the race.
                new str[128], playername[24];
                //creates the variables for the string we will format and the
                //variable to hold the players name.
                format(str, sizeof(str), "~w~Finished the race~n~~g~Position %d/%d - ~b~Time: %s",  (RaceInfo[raceid][originalcount] - (RaceInfo[raceid][racecount] - 1)), RaceInfo[raceid][originalcount], ReturnTime(RaceInfo[raceid][racetime]));
                //Formats the string that will be sent to the player in a gametext.
                //(RaceInfo[raceid][originalcount] - (RaceInfo[raceid][racecount] - 1))
                //Starts at the original count minus the players in the race
                //minus the player himself (-1).
                GameTextForPlayer(playerid, str, 3000, 3);
                //Sends the player an on screen text of that formatted text above
                GetPlayerName(playerid, playername, sizeof(playername));
                //Gets the players name for the message that will be sent to everyone
                format(str, sizeof(str), "[RACE RESULTS] %s(%d) finished race %s(ID:%d) in %s in the position %d/%d", playername, playerid, RaceInfo[raceid][racename], raceid, ReturnTime(RaceInfo[raceid][racetime]), (RaceInfo[raceid][originalcount] - (RaceInfo[raceid][racecount]-1)), RaceInfo[raceid][originalcount]);
                //Formats a message for everyone with the players race results
                //which include his name, id, race name, raceid, time, and
                //position.
                SendClientMessageToAll(0xFF00AA, str);
                //sends everyone the formatted string.
                LeaveRace(playerid, InRace[playerid]);
                return 1;
            }
            RaceCheckpoint[playerid] ++;
            //Adds one to the players current race checkpoint.
            if(checkpoint != RaceInfo[raceid][cpnum]) SetPlayerRaceCheckpoint(playerid, 0, RaceInfo[raceid][checkpointx][checkpoint],  RaceInfo[raceid][checkpointy][checkpoint],  RaceInfo[raceid][checkpointz][checkpoint], RaceInfo[raceid][checkpointx][checkpoint+1],  RaceInfo[raceid][checkpointy][checkpoint+1],  RaceInfo[raceid][checkpointz][checkpoint+1], 6);
            //Sets the players race checkpoint and the arrow to the next
            //race checkpoint. RaceInfo[raceid][checkpointx][checkpoint+1] is the
            //next x position of the next checkpoint which is what the current
            //checkpoint will point at. if(checkpoint != RaceInfo[raceid][cpnum]
            //means that we are not at the last checkpoint yet. The cpnum variable
            //was used to show us the last array used for our checkpoints back
            //when we were adding checkpoints and it will tell us the last checkpoint.
            if(checkpoint+1 == RaceInfo[raceid][cpnum]) SetPlayerRaceCheckpoint(playerid, 1, RaceInfo[raceid][checkpointx][checkpoint],  RaceInfo[raceid][checkpointy][checkpoint],  RaceInfo[raceid][checkpointz][checkpoint], 0.0, 0.0, 0.0, 6);
            //The above is different from the last time we used
            //if(checkpoint == RaceInfo[raceid][cpnum]) because this time it's
            //after we added 1 to the race checkpoint variable. This now means
            //that we are setting the last checkpoint and the call above will
            //be called when we enter that checkpoint that we are just setting now.
            new RaceInfoString[80];
            format(RaceInfoString, sizeof(RaceInfoString), "~b~%d/%d ~n~~g~%s", RaceCheckpoint[playerid]-2, RaceInfo[raceid][cpnum] - 1, ReturnTime(RaceInfo[raceid][racetime]));
            //This returns a formatted string with the checkpoint number and the
            //current time that they have taken on this course. I have created
            //the function ReturnTime because I used the exact same code when
            //the player had finished the race as well. We have the cpnum minus
            //one because we don't count the starting checkpoint as an actual
            //checkpoint that the player goes through. RaceCheckpint[playerid]-2
            //is there because of the reason above (we don't count the first
            //checkpoint and it shows one before what we actually have so
            //that the second to last checkpoint shows 12/13 rather than 13/13
            GameTextForPlayer(playerid, RaceInfoString, 3000, 3);
            //Sends the gametext that tells the player their checkpoint number
            //and the current time it has taken them. You could always add a
            //loop here comparing with the checkpoint number to find the players
            //position in the race. You could also make a Textdraw that tells
            //the player their time and checkpoint rather than having it show
            //as a gametext.
        }
    }
    return 1;
}
The below code is just used to format our time. The race time is stored in a variable with 100ms increments. We want to give the player their time in minuteseconds:milliseconds and this is what this function returns as a string.
pawn Код:
stock ReturnTime(timevariable)
//Used in the OnPlayerEnterRaceCheckpoint code.
{
    new milliseconds = timevariable, seconds, minutes, string[20];
    while(milliseconds > 9)
    //While we still have 10 or more 100x milliseconds left
    {
        seconds ++;
        //Add to the seconds variable
        milliseconds = milliseconds - 10;
        //Take away 10 from the ms variable
    }
    while(seconds > 59)
    //while we have 60 or more seconds on the timer
    {
        minutes ++;
        //We add to the minutes variable
        seconds = seconds - 60;
        //And take the 60 seconds from the seconds variable.
    }
        format(string, sizeof(string), "%d:%02d.%03d
"
, minutes, seconds, milliseconds);
    //format the return string the 0 before the first %d makes it so the
    //seconds are correct and the zeros after the last %d make it so the
    //milliseconds show correctly.
    }
    return string;
    //return the end result.
}
Now it's time for the /joinrace and /leaverace commands. The joinrace and leaverace commands are pretty small because they just do basic checks such as param checks and checking if a player is in a race or not before executing the JoinRace and LeaveRace commands. If you're using another command processor then you can easily change out the CMD:joinrace(playerid, params[]) and the CMD:leaverace(playerid, params[]) to your command processor.

pawn Код:
CMD:joinrace(playerid, params[])
//The joinrace command using ZCMD
{
    if(!strlen(params)) return SendClientMessage(playerid, 0xFF0000, "Please enter a race id to join or type /races");
    new raceid = strval(params);
    if(raceid > currentraceslot-1) return SendClientMessage(playerid, 0xFF0000, "The race ID you have entered is invalid");
    //Just basic tests to see if there is a race in the array / the number is
    //not too big for the array.
    if(RaceInfo[raceid][racejoinable] == false && RaceInfo[raceid][racerunning] == false && InRace[playerid] == -1) OpenRace(raceid);
    //If the race is not running or is joinable it is opened and the player
    //isn't in a race then it will start the race. If you don't want players
    //to be able to start races like this then remove the line and add an admin
    //command or a timer that choses a random race every x amount of seconds.
    JoinRace(playerid, raceid);
    //The above line will just join the player to the race.
    return 1;
}

CMD:leaverace(playerid, params[])
//The leaverace command using zcmd
{
    if(InRace[playerid] == -1) return SendClientMessage(playerid, 0xFF0000, "You are not in a race");
    //Checks if a player is in a race or not and errors him if he isn't and
    //the code stops there with a return.
    LeaveRace(playerid, InRace[playerid]);
    //player leaves the race.
    GameTextForPlayer(playerid, "~b~You have left the race", 3000, 3);
    //On screen text telling them they have left the race
    return 1;
}
Here are the OnPlayerDeath and the OnPlayerDisconnect publics that will sum up everything that you will need but I've added a few more things that you can add into the tutorial. These pieces of code are pretty self explanatory and they just make the player leave a race if they were in one on death or on disconnect.
pawn Код:
public OnPlayerDeath(playerid, killerid, reason)
{
    if(InRace[playerid] != -1) LeaveRace(playerid, InRace[playerid]);
    //Checks if the player is in a race and removes him / her from it on death
    return 1;
}

public OnPlayerDisconnect(playerid, reason)
{
    if(InRace[playerid] != -1) LeaveRace(playerid, InRace[playerid]);
    //Checks if the player is in a race and removes him from it on disconnect if
    //(s)he is in a race.
    return 1;
}
Continued on the next post (size reasons)
Reply


Messages In This Thread

Forum Jump:


Users browsing this thread: 1 Guest(s)