04.01.2017, 21:54
(
Last edited by Freaksken; 11/01/2018 at 08:30 PM.
)
How to create a speedometer
This tutorial is in response to this thread in which I offered to create a tutorial. Most of the tutorials on speedometers are pretty old (some from 2010), don't use per-player textdraws and are thus seriously outdated. The following tutorial is based on Fallout's SpeedoMeter I came across long ago. This tutorial assumes you know basic pawn. That is variable declaration, variable initialization, arrays, loops, tags, callbacks and functions. If you don't know what some of these mean, maybe you know the concepts, but you just didn't knew their names. So just read on.This is what we'll be creating (sort of):
1. Introduction:
(Note: it seems that this introduction has enough information, that it has become its own tutorial.)
This tutorial involves textdraws so we'll need to know what those are first. Even though this information can be found on the wiki, what follows are some things that I find important enough to include in this tutorial. If you already know what the difference is between a global and a per-player textdraw, skip to 2. But I do recommend just reading it quickly, maybe you find out something you didn't knew.
1.1 What is a textdraw:
A textdraw is text that can be drawn anywhere on a player's screen for an indefinite period of time. It can be simple text like a clock, a website address, a score and so on. It can also be a something complex like a health bar, show preview models, show sprites and so on. The text can be drawn in a box. We can also only use the box, by providing empty text. When you search on ******, you can find some nice examples of what is possible with textdraws.
1.2 Kinds of textdraws:
There are two kinds of textdraws: global and per-player, which we'll explore in the following sections.
1.2.1 Global textdraw:
A global textdraw is the same for every player, for example a clock. Either it is shown to a player or it is not. You can only create 2048 global textdraws! A solution to this problem is explored in the following section about Per-player textdraws. Continue first with this section though, so the next one will be easier to understand.
Example:
- We have a clock textdraw that shows 10:50.
- Player0 sees the textdraw.
- Player1 sees the textdraw.
- Player2 doesn't see the textdraw.
- When the time changes, we want this textdraw to show the new time, and every player currently seeing the textdraw will see the updated time.
Code:
new Text:myFancyTD;
Code:
new Float:myFancyFloat;
1.2.1.2 Initialization:
We also need to initialize the variable with something, otherwise this textdraw might conflict with others as you add more. It is actually very important that you do this, otherwise the first textdraw you create will work fine, but when you add more and more some might start to show information of others and have some other weird behaviour. It's really hard to debug and to find where the problem lies, because the compiler will give you no error or warning. So don't forget to do this.
Code:
new Text:myFancyTD = Text:INVALID_TEXT_DRAW;
1.2.1.3 Functions:
A list of the functions can be found here. Note that they all have 'TextDraw' in their name. You can not use a global textdraw function on a per-player textdraw, or vice versa. Otherwise the compiler will give you a tag mismatch warning.
1.2.2 Per-player textdraw:
A per-player textdraw is unique for every player, for example the speed of his vehicle. That means that every player can see the textdraw with personal information in it, or not see the textdraw. You can create 256 per-player textdraws for every player! That's a whole lot more than just 2048 global ones.
Example:
- We have a score textdraw that shows the player's score.
- Player0 has score 50, so his textdraw needs to show 50.
- Player1 has score 100, so his textdraw needs to show 100.
- Player2 doesn't see his textdraw.
- When player0's score changes we only want his textdraw to change, not other player's textdraws.
Code:
new PlayerText:myFancyTD[MAX_PLAYERS];
1.2.2.2 Initialization:
Again, we need to initialize the variable with something to avoid conflicts. This time, we need to initialize every elemnent of the array. This can be done with the {, ...} syntax.
Code:
new PlayerText:myFancyTD[MAX_PLAYERS] = {PlayerText:INVALID_TEXT_DRAW, ...};
1.2.2.3 Functions:
A list of the functions can be found here. Note that they all have 'PlayerTextDraw' in their name, not just 'TextDraw'! Again, you can not use a global textdraw function on a per-player textdraw, or vice versa. Otherwise the compiler will give you a tag mismatch warning.
1.3 Tool to simplify the creation of textdraws:
Since textdraws can only be created with code, it can be quite difficult to get some things right. So to develop your textdraws visually, there exists a handy tool by adri1. Just keep on reading for now, you can play with that tool later when you understand how everything works.
2. Declaring and initializing the necessary variables:
We'll be using both kinds of textdraws in this tutorial. Global textdraws will be used to create a nice looking UI. They are definitely not needed to make the speedometer work, but I wanted to show both kinds of textdraws. Per-player textdraws will be used to show the personal information. If you don't understand what some of following code does, go back to 1.
The following code should be placed at the top of your script, under the includes and defines.
2.1 Global textdraw variables:
We need 5 global textdraws: 4 black lines and 1 transparent background (see picture). The black lines and background are the same for every player, so that's why we can use global textdraws here.
Code:
new Text:SpeedoBackground = Text:INVALID_TEXT_DRAW; new Text:SpeedoLineAbove = Text:INVALID_TEXT_DRAW; new Text:SpeedoLineLeft = Text:INVALID_TEXT_DRAW; new Text:SpeedoLineRight = Text:INVALID_TEXT_DRAW; new Text:SpeedoLineUnder = Text:INVALID_TEXT_DRAW;
We need 1 per-player textdraw, that holds the information about the vehicle the player is currently in (see picture).
Code:
new PlayerText:SpeedoCarInfo[MAX_PLAYERS] = {PlayerText:INVALID_TEXT_DRAW, ...};
We also need to keep track if the speedometer is shown to a player. For example, we don't need to hide it when it's already hidden. Therefore we need an array of bools, and thus the 'bool' tag. We also use array initialization to initialize every element of the array.
Code:
new bool:SpeedoShown[MAX_PLAYERS] = {false, ...};
At last, we need one timer that will work for all players. No need to use a timer for each player.
Code:
new SpeedoStatusTimer = -1;
I'm not going to explain the functions and parameters, since this tutorial is already longer than I wanted it to be and this is not a tutorial on textdraws or timers. Refer to the wiki to know what every function does.
3.1 Global textdraws:
Since global textdraws are the same for every player, we only need to create them once. We will do this when the server starts, more specifically, under OnGameModeInit. All functions have 'TextDraw' in their name, not 'PlayerTextDraw'!
3.1.1 OnGameModeInit:
None of the global textdraws have text. We can create a textdraw without text by using an underscore as the text. We can make a textdraw box transparent by using the alpha values of the color.
Code:
SpeedoBackground = TextDrawCreate(481.0,353.0,"_"); //No text TextDrawUseBox(SpeedoBackground,1); TextDrawBoxColor(SpeedoBackground,0x00000033); //20% visible, so very transparent TextDrawTextSize(SpeedoBackground,596.0,0.0); TextDrawAlignment(SpeedoBackground,0); TextDrawBackgroundColor(SpeedoBackground,0x000000ff); TextDrawFont(SpeedoBackground,3); TextDrawLetterSize(SpeedoBackground,1.0,9.5); TextDrawColor(SpeedoBackground,0xffffffff); TextDrawSetOutline(SpeedoBackground,1); TextDrawSetProportional(SpeedoBackground,1); TextDrawSetShadow(SpeedoBackground,1); SpeedoLineAbove = TextDrawCreate(481.0,353.0,"_"); //No text TextDrawUseBox(SpeedoLineAbove,1); TextDrawBoxColor(SpeedoLineAbove,0x000000ff); //100% visible, so solid TextDrawTextSize(SpeedoLineAbove,596.0,0.0); TextDrawAlignment(SpeedoLineAbove,0); TextDrawBackgroundColor(SpeedoLineAbove,0x000000ff); TextDrawFont(SpeedoLineAbove,3); TextDrawLetterSize(SpeedoLineAbove,1.0,-0.10); TextDrawColor(SpeedoLineAbove,0xffffffff); TextDrawSetOutline(SpeedoLineAbove,1); TextDrawSetProportional(SpeedoLineAbove,1); TextDrawSetShadow(SpeedoLineAbove,1); SpeedoLineLeft = TextDrawCreate(481.0,353.0,"_"); //No text TextDrawUseBox(SpeedoLineLeft,1); TextDrawBoxColor(SpeedoLineLeft,0x000000ff); //100% visible, so solid TextDrawTextSize(SpeedoLineLeft,480.0,0.0); TextDrawAlignment(SpeedoLineLeft,0); TextDrawBackgroundColor(SpeedoLineLeft,0x000000ff); TextDrawFont(SpeedoLineLeft,3); TextDrawLetterSize(SpeedoLineLeft,1.0,9.50); TextDrawColor(SpeedoLineLeft,0xffffffff); TextDrawSetOutline(SpeedoLineLeft,1); TextDrawSetProportional(SpeedoLineLeft,1); TextDrawSetShadow(SpeedoLineLeft,1); SpeedoLineRight = TextDrawCreate(597.0,353.0,"_"); //No text TextDrawUseBox(SpeedoLineRight,1); TextDrawBoxColor(SpeedoLineRight,0x000000ff); //100% visible, so solid TextDrawTextSize(SpeedoLineRight,596.0,0.0); TextDrawAlignment(SpeedoLineRight,0); TextDrawBackgroundColor(SpeedoLineRight,0x000000ff); TextDrawFont(SpeedoLineRight,3); TextDrawLetterSize(SpeedoLineRight,1.0,9.50); TextDrawColor(SpeedoLineRight,0xffffffff); TextDrawSetOutline(SpeedoLineRight,1); TextDrawSetProportional(SpeedoLineRight,1); TextDrawSetShadow(SpeedoLineRight,1); SpeedoLineUnder = TextDrawCreate(481.0,440.0,"_"); //No text TextDrawUseBox(SpeedoLineUnder,1); TextDrawBoxColor(SpeedoLineUnder,0x000000ff); //100% visible, so solid TextDrawTextSize(SpeedoLineUnder,596.0,0.0); TextDrawAlignment(SpeedoLineUnder,0); TextDrawBackgroundColor(SpeedoLineUnder,0x000000ff); TextDrawFont(SpeedoLineUnder,3); TextDrawLetterSize(SpeedoLineUnder,1.0,-0.10); TextDrawColor(SpeedoLineUnder,0xffffffff); TextDrawSetOutline(SpeedoLineUnder,1); TextDrawSetProportional(SpeedoLineUnder,1); TextDrawSetShadow(SpeedoLineUnder,1);
It's always good to clean up everything, especially when you use the '/rcon gmx' command. Destroy the global textdraws and reset the variables.
Code:
TextDrawDestroy(SpeedoBackground); TextDrawDestroy(SpeedoLineAbove); TextDrawDestroy(SpeedoLineLeft); TextDrawDestroy(SpeedoLineRight); TextDrawDestroy(SpeedoLineUnder); SpeedoBackground = Text:INVALID_TEXT_DRAW; SpeedoLineAbove = Text:INVALID_TEXT_DRAW; SpeedoLineLeft = Text:INVALID_TEXT_DRAW; SpeedoLineRight = Text:INVALID_TEXT_DRAW; SpeedoLineUnder = Text:INVALID_TEXT_DRAW;
Since per-player textdraws are unique for every player and are automatically destroyed in OnPlayerDisconnect (see wiki), we need to create them every time a player joins. So, that is under OnPlayerConnect. All functions have 'PlayerTextDraw' in their name, not just 'TextDraw'!
3.2.1 OnPlayerConnect:
This textdraw will contain text, but not at the start, so we just provide an empty string with the underscore in the meantime. Note that for some reason we need to create a per-player textdraw with CreatePlayerTextDraw instead of PlayerTextDrawCreate. All other functions do have 'PlayerTextDraw' in front. Since 'SpeedoCarInfo' is an array of per-player textdraws, we need to provide which element of the array we are using in every function. At last there is also an additional parameter that we need in every function, to know which player we are talking about. If you don't understand this, read on.
Code:
SpeedoCarInfo[playerid] = CreatePlayerTextDraw(playerid, 485.0, 355.0, "_"); //No text PlayerTextDrawBackgroundColor(playerid, SpeedoCarInfo[playerid], 0x000000ff); PlayerTextDrawAlignment(playerid, SpeedoCarInfo[playerid],0); PlayerTextDrawFont(playerid, SpeedoCarInfo[playerid], 1); PlayerTextDrawLetterSize(playerid, SpeedoCarInfo[playerid], 0.30, 1.0); PlayerTextDrawColor(playerid, SpeedoCarInfo[playerid], 0xffffffff); PlayerTextDrawSetOutline(playerid, SpeedoCarInfo[playerid], 1); PlayerTextDrawSetProportional(playerid, SpeedoCarInfo[playerid], 1); PlayerTextDrawSetShadow(playerid, SpeedoCarInfo[playerid], 1);
Code:
TextDrawFont(SpeedoBackground, 3); //Global textdraw PlayerTextDrawFont(playerid, SpeedoCarInfo[playerid], 1); //Per-player textdraw
When you use 'SpeedoCarInfo[playerid]' you're using a variable of the PlayerText type. When you use 'playerid' you're using a variable of the Integer type. Remember that a per-player textdraw is unique for each player? In other words, it is linked to that player. So the 'playerid' parameter is needed to know which player the textdraw belongs to and the 'SpeedoCarInfo[playerid]' is needed to know which textdraw of that player we mean. Here's another explanation:
- SpeedoCarInfo is an array.
- Every element of the array contains a PlayerText textdraw.
- So the array is of type PlayerText.
- To access an element of the array, we use an index.
- An index is an integer.
- We've chosen a logical system in which the textdraw of player with ID 0 is located in the array at the element with index 0.
3.2.2 OnPlayerDisconnect:
Destroy the per-player textdraw and reset the variable.
Code:
PlayerTextDrawDestroy(playerid, SpeedoCarInfo[playerid]); SpeedoCarInfo[playerid] = PlayerText:INVALID_TEXT_DRAW;
3.3 Bool variable:
We need to reset the variable when a player joins, because the speedometer is not shown at that moment.
3.3.1 OnPlayerConnect:
Initialize the variable when the player connects.
Code:
SpeedoShown[playerid] = false;
Reset the variable when the player disconnects.
Code:
SpeedoShown[playerid] = false;
3.4 Timer variable:
We have one global timer that should be run always.
3.4.1 OnGameModeInit:
Start a timer that should tick once every tenth of a second (that's definitely fast enough) and that should repeat forever. We don't need timer parameters, since we will loop through all players.
Code:
SpeedoStatusTimer = SetTimer("SpeedoStatus", 100, 1); //The name of the function we will use later
Kill the timer and reset the variable.
Code:
KillTimer(SpeedoStatusTimer); SpeedoStatusTimer = -1;
Now, finally, what we've been working towards. The timer that will handle everything.
The following code should be placed somewhere in your script. I always put it at the bottom. But it doesn't really matter.
4.1 Forwarding and implementation:
We have a timer that calls a function without parameters. So we need to use empty parentheses. As always with public functions, you also need to forward.
Code:
forward SpeedoStatus(); //Remember this name and no parameters from 3.4.1? public SpeedoStatus() { //Same name and parameters as forward //Loop (see next part) }
Since this timer will handle every player, we need to use a loop. We don't need to do anything if the player is not online, so don't forget to include the 'IsPlayerConnected' check.
Code:
for(new playerid = 0, highestPlayerid = GetPlayerPoolSize(); playerid <= highestPlayerid; playerid++) { if(IsPlayerConnected(playerid)) { //In vehicle check (see next part) } }
Don't use the following.
Code:
for(new playerid = 0; playerid < MAX_PLAYERS; playerid++) { if(IsPlayerConnected(playerid)) { } }
Also don't use the following.
Code:
for(new playerid = 0; playerid <= GetPlayerPoolSize(); playerid++) { if(IsPlayerConnected(playerid)) { } }
4.3 Checking if the player is in a vehicle:
If the player is not in a vehicle, we need to hide the speedometer, otherwise we need to show it.
Code:
if(IsPlayerInAnyVehicle(playerid)) { //Not shown check (see next part) } else { //Shown check (see next part) }
We only need to show the speedometer when it is not shown yet. But we do need to update the content always, so that must be outside of the check. Don't forget to put the boolean to true, which lets us know that the speedometer is now shown.
Code:
if(!SpeedoShown[playerid]) { //Show (see next part) SpeedoShown[playerid] = true; } //Update the content (explained at the end)
We only need to hide the speedometer when it is not hidden yet. Don't forget to put the boolean to false, which lets us know that the speedometer is now hidden.
Code:
if(SpeedoShown[playerid]) { //Hide (see next part) SpeedoShown[playerid] = false; }
Show both the global and per-player textdraws.
Code:
TextDrawShowForPlayer(playerid, SpeedoLineAbove); TextDrawShowForPlayer(playerid, SpeedoLineLeft); TextDrawShowForPlayer(playerid, SpeedoLineRight); TextDrawShowForPlayer(playerid, SpeedoLineUnder); TextDrawShowForPlayer(playerid, SpeedoBackground); PlayerTextDrawShow(playerid, SpeedoCarInfo[playerid]);
Hide both the global and per-player textdraws.
Code:
TextDrawHideForPlayer(playerid, SpeedoLineAbove); TextDrawHideForPlayer(playerid, SpeedoLineLeft); TextDrawHideForPlayer(playerid, SpeedoLineRight); TextDrawHideForPlayer(playerid, SpeedoLineUnder); TextDrawHideForPlayer(playerid, SpeedoBackground); PlayerTextDrawHide(playerid, SpeedoCarInfo[playerid]);
Finally the content itself.
Code:
//We need to get in which vehicle the player is new vehicleid = GetPlayerVehicleID(playerid); //We need to get what the velocity of that vehicle is new Float:VelX, Float:VelY, Float:VelZ; GetVehicleVelocity(vehicleid, VelX, VelY, VelZ); //We do some calculations with those values, got to this thread for more information new Float:Speed; Speed = (floatsqroot(((VelX*VelX)+(VelY*VelY))+(VelZ*VelZ))* 181.5); //Put the information in a string new String[8+ 1]; format(String ,sizeof(String), "%ikm/h", floatround(Speed, floatround_floor)); //floatround rounds the float to an integer //Put the string in the textdraw PlayerTextDrawSetString(playerid, SpeedoCarInfo[playerid], String);
5. End word:
Damn this took longer than I expected it would take... Anyway, as always, constructive criticism is definitely welcome.