Så här kodar du en självhärdad PHP / SQL-leaderboard för ditt spel

I den här artikeln kommer vi att skapa vår första MySQL leaderboard för att vara värd på en webbplats eller webbserver med hjälp av enkel PHP och SQL. Vi gör sedan ett enkelt Unity-exempel i C # med hjälp av GUIText objekt att lägga till nya poäng i vår leaderboard, visa de tio bästa poängen och visa en användares poäng och rankning.


Introduktion

Singelspelare är roliga, men att slå din egen highscore kan bli tråkig. Att lägga till en leaderboard i ditt spel ger verklig motivation för spelare att förbättra sina poäng och spela ditt spel mer, och det kan till och med användas för att ta reda på om ditt spel är för lätt eller svårt. I spel som fortsätter för alltid kan leaderboards vara den enda anledningen till att spelarna spelar. Om du har en egen webbplats eller server kan du vara värd för din egen leaderboard, så du har fullständig kontroll över ditt spel.


Skapa din leaderboard

Först och främst behöver du ha en SQL-databas på din server eller webbplats. Webbplatser levereras ofta med en inbyggd MySQL-databas. Detaljer om detta kommer att variera beroende på vilken tjänst du använder, men du ska kunna hitta din SQL-värd, användarnamn och lösenord (såväl som ditt databasnamn) från din administratörspanel eller registreringsmail.

I det här exemplet används phpMyAdmin för att komma åt databasen (byggd direkt in i adminpanelen). Du vill öppna din databas och öppna SQL-fliken. Om du har mer kontroll över din server kan du skapa en ny databas.

Sätt sedan in följande SQL:

 CREATE TABLE Scores (namn VARCHAR (10) INTE NULL DEFAULT 'Anonym' PRIMARY KEY, poäng INT (5) UNSIGNED NOT NULL DEFAULT '0', ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP) MOTOR = InnoDB;

Detta skapar en tabell med tre variabler:

  • namn, som innehåller dina användares namn, och som lagrar 10 tecken. Detta är vårt bords huvudbeteckning, så det betyder att det bara kan lagra en rad per användarnamn.
  • Göra, som håller varje användares högsta poäng. I det här exemplet är det en osignerad variabel, så det kan bara vara positivt. Om du vill ha negativa poäng måste du ändra det.
  • ts, en tidstämpel vi kan använda för att ändra ordern på vår leaderboard.

Nu, om du använder SQL Server och inte MySQL, kan du fortfarande använda TIDSSTÄMPEL, men för det värde du måste använda GetDate () istället för CURRENT_TIMESTAMP.

En extra sak att tänka på: Om du gör ett mycket enkelt spel kanske du inte vill knyta poäng till namn (för att varje spelare ska ha flera poäng i din Top 10, till exempel). Detta kan dock vara en dålig idé. du kan ha en spelare som är så bra att dom kan dominera hela din 10! I det här fallet vill du inte namn som en primär nyckel, och du vill även lägga till det här:

 ID INT (8) UNSIGNED NOT NULL AUTO_INCREMENT PRIMÄR NYCKEL

Detta kommer att se till att en märkbar ny rad läggs till för varje poäng.

Klick och du är klar! Ditt bord är klart nu.


Ställa in dina PHP-filer

Nu behöver du skapa några PHP-filer. Det här är operatörens mellanmän, vilket ger ett sätt för Unity att komma åt din server. Den första PHP-filen du behöver behöver är AddScore.php. Du måste känna till serverns information från tidigare.

  

(Byta ut SQLHOST, SQLUSER, SQLPASSWORD och YOURDATABASE med din egen information.)

Här har vi bara försökt ansluta till databasen. Om anslutningen misslyckas kommer Unity att informeras om att förfrågan misslyckades. Nu ska du skicka information till servern:

 $ username = mysql_real_escape_string ($ _ GET ['name'], $ db); $ score = mysql_real_escape_string ($ _ GET ['score'], $ db); $ hash = $ _GET ['hash']; $ PrivateKey = "ADDYOURKEY";

Hasan används för att kryptera dina data och hindra personer från att hacka din leaderboard. Det genereras med en dold nyckel i Unity och här, och om de två hasarna matchar får du tillgång till din databas.

 $ expected_hash = md5 ($ användarnamn. $ poäng. $ privateKey); om ($ expected_hash == $ hash) 

Här genererar vi hasen oss själva och kontrollerar att den hash vi skickar från Unity är identisk med den hash som vi förväntar oss. Om det är kan vi skicka vår fråga!

 $ query = "INSERT INTO Scores SET-namn = '$ name', score = '$ poäng', ts = CURRENT_TIMESTAMP

Det här är den första halvan av vår SQL-fråga. Det borde vara ganska självförklarande; Den inlämnade poängen och användarnamnet läggs till i tabellen, och tidsstämpeln uppdateras. Den andra halvan är mer komplicerad:

 OM DUPLICATE KEY UPDATE ts = om ('$ score'> poäng, CURRENT_TIMESTAMP, ts), värdera = om ('$ score'> poäng, '$ poäng', poäng); ";

Vi kontrollerar först om användarnamnet (vår primära nyckel) redan har en rad. Om det gör, istället för att infoga en ny post, uppdateras vår post. Vi vill uppdatera tidstämpeln och poängen, men bara om det nya resultatet är högre!

Använder sig av om uttalanden, vi ser till att de nya värdena endast används om det nya värdet är större än nuvarande poäng, annars används de ursprungliga värdena.

 $ result = mysql_query ($ query) eller dö ('Query failed:'. mysql_error ()); ?>

Slutligen kör vi vår fråga och stänger vår PHP.

Den här filen går på vår server. Du måste komma ihåg webbadressen. På samma sätt måste vi göra två fler PHP-filer med olika frågor, som vi ringer till TopScores.php och GetRank.php.

De Toppresultat fråga är helt enkelt:

 VÄLJ * FRÅN SCORER ORDER med poäng DESC, ts ASC LIMIT 10

Detta kommer att ta de 10 bästa värdena baserat på poäng, och för bundna ställen sätts spelare som fått poängen längst upp på bordet. Den här gången vill vi också extrahera data, så lägger vi också till:

 $ result_length = mysql_num_rows ($ resultat); för ($ i = 0; $ i < $result_length; $i++)  $row = mysql_fetch_array($result); echo $row['name'] . "\t" . $row['score'] . "\n"; 

Detta kommer att extrahera våra resultat och tabulera dem på ett sätt som vi kan placera dem i arrays i Unity.

Slutligen har vi GrabRank:

 SELECT uo. *, (VÄLJ COUNT (*) FRÅN Scores ui WHERE (ui.score, -ui.ts)> = (uo.score, -uo.ts)) AS rang FRÅN Scores uo WHERE name = '$ name' ;

Detta ger oss vår spelares rang i resultattavlan. Vi kan sedan extrahera det genom att eko $ Rad [ 'rank'].

Vår källkod innehåller även en sanitiseringsfunktion som hindrar användarna från att lägga in svärord i din leaderboard eller försöka göra en SQL-injektionsattack.


Göra en enkel minigame i enighet

Nu behöver vi ett spel att använda vår highscore board med! Vi ska bara testa hur många klick varje användare kan göra på tio sekunder, men du kan lägga till din leaderboard i vilket spel som helst.

Layout

Vi börjar med att göra fyra GUIText objekt. Dessa bör förankras om mittencentret för bekvämlighet. Du kan justera dessa med pixelförskjutning för att få dem på rätt ställe, men om du vill att de ska anpassa sin position för någon upplösning är det enklare att ändra X och Y position (mellan 0 och 1); annars måste du justera dem vid start.

Du måste dock justera typstorleken vid start om du vill köra i alla upplösningar. Ett snabbt sätt att göra det här är att basera dem på skärmens höjd. Vi kan göra det genom att göra en klass som gör det här och bifogar det till alla våra textobjekt, men det är mycket enklare att göra allt från en klass.

Det spelar ingen roll om vilket objekt vi väljer som vår "chef", så vi kan bara sätta den här klassen på vår klickräknare. Så i vår första klass skriver vi:

 void Start () foreach (GUIText chosentext i FindObjectsOfType (typeof (GUIText)) som GUIText []) chosentext.blah.fontSize = Mathf.FloorToInt (Screen.height * 0,08f); 

Detta kommer att hitta varje textobjekt i scenen och skala det till en förnuftig storlek.

Nu vill vi att klickräknaren är större än den andra texten, så om vi håller den här klassen där har vi den extra bonusen att vi också kan kontrollera om guiText i fråga är den som är knuten till detta GameObject:

 om (blah == guiText) blah.fontSize = Mathf.FloorToInt (Screen.height * 0.18f); annat [etc.]

gameplay

Klickkomponenten i spelet kommer att vara väldigt enkelt. I början vill vi inte att timern räknar ner till det första klicket, så vi gör två privata bools i vår klass - firstClick och allowedToClick. I Start() vi kan ställa in firstClick till falsk och allowedToClick till Sann.

Nu behöver vi disken för att faktiskt spela in klick, och det finns ett par sätt att göra detta. Vi kan hålla en heltal variabel som följer poängen, eller vi kan göra det lite mindre effektiv men i en rad (och med något så enkelt vi inte behöver optimera, men det är bra). Så vi registrerar klicket i Uppdatering() funktionen och öka värdet genom att läsa strängen.

 void Update () if (allowedToClick && Input.GetMouseButtonUp (0)) om (! firstClick) firstClick = true; StartCoroutine (Countdown ());  guiText.text = (System.Int32.Parse (guiText.text) + 1) .ToString (); 

Som du kan se här uppnås inkrementet genom att läsa strängen som ett heltal, lägga till en och sedan konvertera tillbaka till en sträng. Du ser också här att vi har kört en koroutin så snart användaren först klickar, vilket börjar nedräkningen.

Vi använder recursion i den här funktionen. Återigen kan vi använda ett heltal som håller nedräkningsvärdet för effektivitet, men vi kommer att använda strängmanipulation igen.

 IEnumerator Countdown () yield returnera nya WaitForSeconds (1); counter.guiText.text = (System.Int32.Parse (counter.guiText.text) - 1) .ToString (); if (counter.guiText.text! = "0") StartCoroutine (Countdown ());  else allowedToClick = false; GetComponent() .Setscore (System.Int32.Parse (guiText.text)); toptext.guiText.text = "Ange ditt användarnamn."; GetComponent() .enabled = true; 

Obs! Det är viktigt att vi använde StartCoroutine () och kallade inte bara den här funktionen eftersom den är en IEnumerator. De avkastning uttalande får det att vänta en sekund innan några åtgärder vidtas. Det tar bort en från räknaren, och om värdet inte är noll kallar det sig igen. På så sätt räknas funktionen ner tills den når 0.

Namninmatning

Efter det stoppar användaren att klicka, frågar efter användarnamnet och får tillgång till vår andra och tredje klass (som vi ska skriva!). Vi ska ta en titt på vad dessa gör nu, från och med Namnange.

I Namnange () Vi ska tillåta en användare att skriva in deras användarnamn, med några begränsningar. Ursprungligen vill vi visa understreckskaraktären _, som kommer att raderas så fort de börjar skriva sitt namn. Dessutom vill vi inte att de ska kunna använda tecken som \ eller ', eftersom dessa skulle förstöra våra SQL-frågor.

Vi ska använda en strängbyggare för att skapa detta. Först lägger vi några variabler högst upp i vår klass:

 privat int MaxNameLength = 10; privat StringBuilder playerName; privat bool backspacepossible; privat bool initialpress;

De MaxNameLength bör sättas i samma längd som du använde för din VARCHAR längd när du gjorde ditt bord. Här har vi vår strängbyggare, spelarnamn, och två Booleans. Den första, backspacepossible, är att styra användarens förmåga att hålla ner backspace för att radera tecken. Den andra är att ange om de har börjat skriva sitt namn än.

I Start(), vi måste ta hand om några saker. Vi inaktiverar all text utom den som heter Toptext; det kan vi göra i en för varje slinga, som tidigare.

 void Start () foreach (GUIText text i FindObjectsOfType (typeof (GUIText)) som GUIText []) if (text.name! = "Toptext") text.guiText.enabled = false;  GetComponent() .enabled = false; playerNameTemp = new StringBuilder (); playerNameTemp.Append ( "_"); backspacepossible = true; initialpress = false; 

Här kan du se att vi har gjort några saker. Vi har inaktiverat vår första klass (ClickTimes) eftersom vi inte använder det längre. Vi har också skapat en förekomst av playerNameTemp och bifogade den med _, så spelarna kan se var deras namn går och vi har initialiserat våra variabler.

Nu måste vi låta spelaren faktiskt ange sitt namn. I slutet av Uppdatering() vi placerar följande kod:

 guiText.text = playerNameTemp.ToString ()

Detta kommer att se till att texten visar vad vår strängbyggare registrerar.

Nästa hanterar vi teckeninmatning:

 om (playerNameTemp.Length < MaxNameLength)  foreach (char c in Input.inputString)  if (char.IsLetterOrDigit(c) || c == '_' || c ==")  if (!initialpress)  initialpress = true; playerNameTemp.Remove(0, 1);  playerNameTemp.Append(c);   

Så länge som användaren skriver in tecken som är antingen bokstäver, siffror, mellanslag eller understreck (även om du bara väljer att tillåta alfanumeriska tecken) så kommer strängen att vara bifogad med den nya siffran. Om det här är den första trycket, kommer den ursprungliga underskriften att tas bort innan den nya bokstaven läggs till.

Nästa:

 om (playerNameTemp.Length> 0) if (Input.GetKeyDown (KeyCode.Backspace)) om (! initialpress) initialpress = true;  backspacepossible = false; StartCoroutine (BackspaceInitialHold ()); playerNameTemp.Remove (playerNameTemp.Length - 1, 1);  annars om (backspacepossible && Input.GetKey (KeyCode.Backspace)) backspacepossible = false; StartCoroutine (BackspaceConstantHold ()); playerNameTemp.Remove (playerNameTemp.Length - 1, 1);

Så länge som det inte finns några tecken kvar i vår strängbyggare och backspace är möjligt, kan användaren ta bort tecken. Notera skillnaden mellan första och andra uttalandena. Den tidigare användningen GetKeyDown (), medan den senare använder Getkey () (och kontrollerar vår bool). Skillnaden är att vi ska radera en karaktär varje gång användaren trycker backspace, men inte ständigt medan användaren håller ner den.

Coroutinesna BackspaceInitialHold () och () vänta bara 0,15 och 0,05 sekunder respektive, och ställ sedan in backspacepossible till Sann. Så efter att användaren har hållit ner backspace för 0,15 sekunder, så länge som de fortfarande håller backspace, raderas ett tecken varje gång 0,05 sekunder (så länge som längd är större än kod> 0).

Vi föreslår också att om detta är den första knappen trycker användaren, initialpress utlöses (så det kommer inte att försöka ta bort ett tecken när de trycker på något annat).

För att sluta allt, måste vi tillåta användaren att trycka på Lämna tillbaka för att slutföra namninmatningen.

 if (playerNameTemp.Length> 0 && initialpress) if (Input.GetKeyDown (KeyCode.Return)) foreach (GUIText-text i FindObjectsOfType (typeof (GUIText)) som GUIText []) text.guiText.enabled = false;  GetComponent() .SetName (guiText.text); GetComponent() .enabled = true; aktiverad = false; 

Så länge som användaren har gjort någon form av inmatning och längd är större än 0, namnet kommer att accepteras Alla våra textobjekt blir raderade, vi inaktiverar den här klassen och vi slår på vår tredje klass, Rekord. Alla tre klasserna måste läggas på vårt objekt i redaktören.

Vi ringde just Rekord's Ange namn() funktion, och tidigare vi ringde SetScore (). Vart och ett av dessa funktioner anger helt enkelt värdena för privata variabler som vi skickar nu till vår leaderboard.


Få åtkomst till din leaderboard i enhet

På toppen av Rekord Vi vill förklara några variabler. Först:

 allmän GameObject BaseGUIText;

Det här är GUIText prefab att vi ska basera vår leaderboard på. Du bör se till att texten är förankrad till mitten till vänster och vänsterjusterad. Du kan också välja en typsnitt här också.

 privat sträng privatKey = "Nyckeln du genererade före"; privat sträng AddScoreURL = "http://yoursite.com/AddScore.php?"; privat sträng TopScoresURL = "http://yoursite.com/TopScores.php"; privat sträng RankURL = "http://yoursite.com/GrabRank.php?"; privat int highscore; privat sträng användarnamn; privat int rang

Vi behöver alla våra värden från tidigare - nyckeln du genererade, webbadresserna du laddade upp dina PHP-filer till och så vidare. De rekord och Användarnamn variabler ställs in med två offentliga funktioner som heter SetScore () och Ange namn(), som vi använde i föregående avsnitt.

Tips: Det är verkligen viktigt att du ställer frågor efter AddScore.php och GrabRank.php! Dessa tillåter dig att skicka variabler till dina PHP-filer.

Vi måste använda IEnumerators här för att hantera våra SQL-frågor, eftersom vi måste vänta på ett svar. Vi börjar vår första koroutin, AddScore (), så snart klassen är aktiverad.

 IEnumerator AddScore (strängnamn, int poäng) string hash = Md5Sum (namn + poäng + privatKey); WWW ScorePost = ny WWW (AddScoreURL + "name =" + WWW.EscapeURL (namn) + "& score =" + poäng + "& hash =" + hash); avkastning avkastning ScorePost; om (ScorePost.error == null) StartCoroutine (GrabRank (namn));  annat Fel (); 

Först skapar vi vår hash med den privata nyckeln, med hjälp av en funktion för att skapa en MD5-hash på samma sätt som PHP md5 (). Det finns ett exempel på detta på Unitys community wiki.

Här, om servern är otillgänglig av någon anledning, kör vi en Fel() fungera. Du kan välja vad du vill hända i din felhanterare. Om poängen skrivs korrekt, lanserar vi dock vår nästa koroutin: GrabRank ().

 IEnumerator GrabRank (strängnamn) WWW RankGrabAttempt = new WWW (RankURL + "name =" + WWW.EscapeURL (namn)); avkastning RankGrabAttempt; om (RankGrabAttempt.error == null) rank = System.Int32.Parse (RankGrabAttempt.text); StartCoroutine (GetTopScores ());  annat Fel (); 

Återigen får vi tillgång till webbplatsen, och den här gången lagrar vi rangordningen om den är framgångsrikt taget.

Nu kan vi använda vår senaste koroutin. Den här kommer att binda upp allt. Vi börjar med att komma åt webbadressen för en sista gång:

 IEnumerator GetTopScores () WWW GetScoresAttempt = ny WWW (TopScoresURL); avkastning GetScoresAttempt; om (GetScoresAttempt.error! = null) Error ();  annat 

Men den här gången vill vi dela upp de data vi får i en rad strängar. Först och främst kan vi använda en strängspalt som så:

 sträng [] textlista = GetScoresAttempt.text.Split (ny sträng [] "\ n", "\ t", System.StringSplitOptions.RemoveEmptyEntries);

Detta kommer att se till att varje resultat blir ett nytt element i vår strängmatris, så länge en ny rad eller en flik finns (vilken den blir!). Vi kommer nu att dela upp det i två ytterligare arrays som heter namn och Scores.

 sträng [] Namn = ny sträng [Mathf.FloorToInt (textlist.Length / 2)]; sträng [] Scores = ny sträng [Namn.Längd]; för (int i = 0; i < textlist.Length; i++)  if (i % 2 == 0)  Names[Mathf.FloorToInt(i / 2)] = textlist[i];  else Scores[Mathf.FloorToInt(i / 2)] = textlist[i]; 

Vi har nu gjort två nya arrays som är vardera hälften av den första arrayens storlek. Vi delar sedan varje första sträng i vår namn array och varannan i vår Scores array.

Nu vill vi visa dem som text, så vi måste placera våra tre kolumner. Vi ska skala dem till skärmen så att de passar alla upplösningar. Först ska vi förklara de första positionerna där vår titeltext kommer att gå:

 Vector2 LeftTextPosition = ny Vector2 (0.22f, 0.85f); Vector2 RightTextPosition = ny vektor2 (0.76f, 0.85f); Vector2 CentreTextPosition = ny Vector2 (0.33f, 0.85f);

Och nu är vi redo att skapa våra textobjekt, baserat på vårt BaseGUIText prefab. Vi ordnar titlarna individuellt och anger deras text - till exempel:

 GameObject Scoresheader = Instantiate (BaseGUIText, ny Vector2 (0.5f, 0.94f), Quaternion.identity) som GameObject; Scoresheader.guiText.text = "High Scores"; Scoresheader.guiText.anchor = TextAnchor.MiddleCenter; Scoresheader.guiText.fontSize = 35;

När vi har gjort det här för alla våra titlar, justerar vi våra positioner så att den nya texten kommer att visas nedre ner.

 LeftTextPosition - = ny Vector2 (0, 0,062f); RightTextPosition - = ny Vector2 (0, 0,062f); CenterTextPosition - = ny vektor2 (0, 0,062f);

Nästa kör vi a för slinga som kommer att iterera genom hela vår 10-lista, ställa in namn, rang och poäng (och se till att texten förankras skäligt) och sedan justera positionerna ännu en gång. Varje iteration kontrollerar vi om användarens rang är lika med rankningen som visas, och om så faller vi texten om så att användarens poäng är markerad i gul:

 för (int i = 0; i 

Och sen äntligen kontrollerar vi om användarens rang är över 10. Om det är så lägger vi poängen längst ner i elfte spåret tillsammans med deras rang och färgar den gul.

 om (rank> 10) GameObject Score = Instantiate (BaseGUIText, RightTextPosition, Quaternion.identity) som GameObject; Score.guiText.text = "" + highscore; Score.guiText.anchor = TextAnchor.MiddleCenter; GameObject Name = Instantiate (BaseGUIText, CentreTextPosition, Quaternion.identity) som GameObject; Name.guiText.text = användarnamn; GameObject Rank = Instantiate (BaseGUIText, LeftTextPosition, Quaternion.identity) som GameObject; Rank.guiText.text = "" + (rank); Rank.guiText.anchor = TextAnchor.MiddleCenter; Score.guiText.material.color = Color.yellow; Name.guiText.material.color = Color.yellow; Rank.guiText.material.color = Color.yellow; 

Slutsats

voilà; vår leaderboard är komplett! I källfilerna har jag också inkluderat en PHP-fil som tar tag i ledningarna ovanför och under användarens användarnamn, så att du kan visa dem exakt var de finns på tavlan.