In dit artikel gaan we ons eerste MySQL-leaderboard maken om te hosten op een website of webserver met behulp van eenvoudige PHP en een beetje SQL. We zullen dan een eenvoudig Unity-voorbeeld maken in C # using GUIText
objecten om nieuwe scores aan ons leaderboard toe te voegen, de top tien scores weer te geven en de score en rang van een gebruiker weer te geven.
Single-player games zijn leuk, maar het verslaan van je eigen highscore kan saai worden. Het toevoegen van een leaderboard aan je spel biedt echte motivatie voor spelers om hun scores te verbeteren en je spel meer te spelen, en het kan zelfs worden gebruikt om erachter te komen of je spel te gemakkelijk of te moeilijk is. In games die voor altijd doorgaan, kunnen leaderboards de enige reden zijn waarom je spelers spelen. Als je een eigen website of server hebt, wil je misschien je eigen scorebord hosten, zodat je volledige controle over je spel hebt.
Allereerst moet je een SQL-database op je server of site hebben. Websites worden vaak geleverd met een ingebouwde MySQL-database. Details hiervan zijn afhankelijk van de service die u gebruikt, maar u moet uw SQL-host, gebruikersnaam en wachtwoord (evenals uw databasenaam) kunnen vinden in uw beheerdersvenster of registratie-e-mailadres..
In dit voorbeeld wordt phpMyAdmin gebruikt om toegang te krijgen tot de database (ingebouwd in het admin-paneel). U wilt uw database openen en het SQL-tabblad openen. Als u meer controle hebt over uw server, kunt u een nieuwe database maken.
Voeg vervolgens de volgende SQL in:
MAKEN TAFEL Scores (naam VARCHAR (10) NIET NULL DEFAULT 'Anoniem' PRIMAIRE SLEUTEL, score INT (5) ONGESCHAKELD NIET NULL DEFAULT '0', ts TIMESTAMP NIET NULL DEFAULT CURRENT_TIMESTAMP) MOTOR = InnoDB;
Hiermee wordt een tabel met drie variabelen gemaakt:
naam
, die de naam van uw gebruikers bevat en die 10 tekens zal opslaan. Dit is de hoofd-ID van onze tabel, dus dat betekent dat het slechts één rij per gebruikersnaam kan opslaan.partituur
, die de hoogste score van elke gebruiker bevat. In dit voorbeeld is het een niet-ondertekende variabele, dus deze kan alleen positief zijn. Als je negatieve scores wilt hebben, moet je dat veranderen.ts
, een tijdstempel die we kunnen gebruiken om de volgorde van ons scorebord te wijzigen.Nu, als u SQL Server gebruikt en niet MySQL, kunt u nog steeds gebruiken TIMESTAMP
, maar voor de waarde die je moet gebruiken KRIJG DATUM()
in plaats van CURRENT_TIMESTAMP
.
Een extra ding om in gedachten te houden: als je een heel eenvoudig spel maakt, wil je misschien niet scores aan namen koppelen (om elke speler toe te laten meerdere scores in je Top 10 te hebben, bijvoorbeeld). Dit kan echter een slecht idee zijn; je hebt misschien een speler die zo goed is dat ze je hele Top 10 kunnen domineren! In dit geval zult u niet willen naam
als een primaire sleutel, en u wilt dit ook toevoegen:
id INT (8) UNSIGNED NOT NULL AUTO_INCREMENT PRIMAIRE SLEUTEL
Dit zorgt ervoor dat voor elke score een waarneembare nieuwe rij wordt toegevoegd.
Klik Gaan en je bent klaar! Je tafel is nu helemaal klaar.
Nu moet je wat PHP-bestanden maken. Dit zijn de middelste mannen van de operatie, die Unity een manier bieden om toegang te krijgen tot uw server. Het eerste PHP-bestand dat je nodig hebt is AddScore.php
. U moet de serverinformatie van tevoren kennen.
(Vervangen
SQLHOST
,SQLUSER
,SQLPASSWORD
enYOURDATABASE
met uw eigen informatie.)Hier hebben we zojuist geprobeerd verbinding te maken met de database. Als de verbinding mislukt, wordt Unity geïnformeerd dat het verzoek niet is gelukt. Nu wil je wat informatie doorgeven aan de server:
$ username = mysql_real_escape_string ($ _ GET ['name'], $ db); $ score = mysql_real_escape_string ($ _ GET ['score'], $ db); $ hash = $ _GET ['hash']; $ PrivateKey = "ADDYOURKEY";De hash wordt gebruikt om uw gegevens te versleutelen en voorkomt dat mensen uw scorebord hacken. Het wordt gegenereerd met een verborgen sleutel in Unity en hier, en als de twee hashes overeenkomen, krijgt u toegang tot uw database.
$ expected_hash = md5 ($ gebruikersnaam. $ score. $ privateKey); if ($ expected_hash == $ hash)Hier genereren we de hash zelf en controleren we dat de hash die we van Unity indienen identiek is aan de hash die we verwachten. Als dat zo is, kunnen we onze vraag verzenden!
$ query = "INSERT INTO scores SET naam = '$ naam', score = '$ score', ts = CURRENT_TIMESTAMPDit is de eerste helft van onze SQL-query. Het moet redelijk zelfverklarend zijn; de ingediende score en gebruikersnaam worden toegevoegd aan de tabel en de tijdstempel wordt bijgewerkt. De tweede helft is ingewikkelder:
OP DUPLICATE KEY UPDATE ts = if ('$ score'> score, CURRENT_TIMESTAMP, ts), score = if ('$ score'> score, '$ score', score); ";We controleren eerst of de gebruikersnaam (onze primaire sleutel) al een rij bevat. Als dat het geval is, wordt onze invoer bijgewerkt in plaats van een nieuw item in te voegen. We willen de tijdstempel bijwerken en scoren, maar alleen als de nieuwe score hoger is!
Gebruik makend van
als
statements, we zorgen ervoor dat de nieuwe waarden alleen worden gebruikt als de nieuwe score groter is dan de huidige score, anders worden de originele waarden gebruikt.$ result = mysql_query ($ query) of die ('Query failed:'. mysql_error ()); ?>Eindelijk voeren we onze vraag uit en sluiten we onze PHP.
Dit bestand gaat op onze server. U moet de URL onthouden. Evenzo moeten we nog twee PHP-bestanden maken met verschillende zoekopdrachten, die we zullen noemen
TopScores.php
enGetRank.php
.De
Topscores
zoekopdracht is gewoon:SELECT * FROM Scores BESTELLEN op score DESC, ts ASC LIMIT 10Dit neemt de top 10-waarden op basis van de score, en voor gekoppelde rangen plaatst de speler die de score het verst heeft bereikt de tafel op. Deze keer willen we ook gegevens extraheren, dus we voegen ook toe:
$ result_length = mysql_num_rows ($ resultaat); voor ($ i = 0; $ i < $result_length; $i++) $row = mysql_fetch_array($result); echo $row['name'] . "\t" . $row['score'] . "\n";Hiermee worden onze resultaten geëxtraheerd en op een zodanige manier getabelleerd dat we ze in arrays in Unity kunnen plaatsen.
Eindelijk, we hebben
GrabRank
:SELECT uo. *, (SELECT COUNT (*) FROM Scores ui WHERE (ui.score, -ui.ts)> = (uo.score, -uo.ts)) AS rank FROM Scores uo WHERE name = '$ name' ;Dit geeft ons de rang van onze speler in het scorebord. We kunnen het dan extraheren door te echoën
$ Row [ 'rang']
.Onze broncode bevat ook een ontsmettingsfunctie, die voorkomt dat gebruikers scheldwoorden invoeren op je scorebord, of een poging tot een SQL-injectie probeert.
Een eenvoudige minigame in eenheid maken
Nu hebben we een spel nodig om ons highscore bord te gebruiken! We gaan gewoon testen hoeveel klikken elke gebruiker in tien seconden kan maken, maar je kunt je scorebord toevoegen aan elke game.
lay-out
We beginnen met het maken van vier
GUIText
voorwerpen. Deze moeten voor het gemak in het midden worden verankerd. U kunt deze aanpassen met pixel-offset om ze op de juiste plaats te krijgen, maar als u wilt dat ze hun positie aanpassen voor elke resolutie, is het eenvoudiger om deX
enY
positie (tussen0
en1
); anders moet u ze bij het opstarten aanpassen.U zult echter de lettergrootte bij het opstarten moeten aanpassen als u bij alle resoluties wilt werken. Een snelle manier om dit te doen is door ze te baseren op de hoogte van het scherm. We kunnen dit doen door een klasse te maken die dit doet en deze aan al onze tekstobjecten te koppelen, maar het is veel eenvoudiger om dit allemaal vanuit één klasse te doen.
Het maakt niet echt uit welk object we kiezen als onze "manager", dus we kunnen deze klasse gewoon op onze klikbalie plaatsen. Dus in onze eerste klas schrijven we:
void Start () foreach (GUIText chosentext in FindObjectsOfType (typeof (GUIText)) als GUIText []) chosentext.blah.fontSize = Mathf.FloorToInt (Screen.height * 0.08f);Dit vindt elk tekstobject in de scène en schaalt dit naar een verstandige grootte.
Nu willen we dat de klikmeter groter is dan de andere tekst, dus als we deze klasse daar ophangen, hebben we de toegevoegde bonus dat we ook kunnen controleren of de
guiText
in kwestie is degene die hieraan is gehechtGameObject
:if (blah == guiText) blah.fontSize = Mathf.FloorToInt (Screen.height * 0.18f); anders [etc.]gameplay
Het klikgedeelte van de game is heel eenvoudig. In het begin willen we niet dat de timer aftelt tot de eerste klik, dus we maken er twee privé
bools
in onze klas -firstClick
enallowedToClick
. InBegin()
we kunnen instellenfirstClick
naarvals
enallowedToClick
naarwaar
.Nu hebben we de teller nodig om de klikken daadwerkelijk vast te leggen en er zijn een aantal manieren om dit te doen. We zouden een integer-variabele kunnen behouden die de score bijhoudt, of we zouden het iets minder efficiënt kunnen maken, maar op één regel (en met zoiets eenvoudigs hoeven we niet echt te optimaliseren, maar het is een goede gewoonte). Dus we registreren de klik in de
Bijwerken()
functie, en verhoog de waarde door de string te lezen.void Update () if (allowedToClick && Input.GetMouseButtonUp (0)) if (! firstClick) firstClick = true; StartCoroutine (Countdown ()); guiText.text = (System.Int32.Parse (guiText.text) + 1) .ToString ();Zoals u hier kunt zien, wordt de incrementatie bereikt door de tekenreeks als een geheel getal te lezen, er een toe te voegen en vervolgens terug te converteren naar een tekenreeks. U ziet ook hier dat we een coroutine hebben uitgevoerd zodra de gebruiker voor het eerst klikt, waardoor het aftellen begint.
We gebruiken recursie in deze functie. Nogmaals, we zouden een geheel getal kunnen gebruiken dat de aftelwaarde voor efficiëntie bevat, maar we zullen stringmanipulatie opnieuw gebruiken.
IEnumerator Countdown () rendement opleveren nieuwe 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 = "Voer uw gebruikersnaam in."; GetComponent () .enabled = true; Opmerking: het is belangrijk dat we het hebben gebruikt
StartCoroutine ()
en noemde deze functie niet gewoon, omdat het een isIEnumerator
. Deopbrengst
verklaring zorgt ervoor dat het een seconde wacht voordat er actie wordt ondernomen. Het verwijdert een van de teller en als de waarde niet nul is, roept deze zichzelf opnieuw op. Op deze manier telt de functie af totdat deze bereikt is0
.Naam invoeren
Hierna stopt het de gebruiker om te klikken, vraagt het om de gebruikersnaam en krijgt toegang tot onze tweede en derde klas (die we gaan schrijven!). We zullen kijken naar wat deze nu doen, te beginnen met
NameEnter
.In
NameEnter ()
we gaan een gebruiker toestaan om zijn gebruikersnaam in te typen, met enkele beperkingen. Aanvankelijk willen we het onderstrepingsteken weergeven_
, die worden gewist zodra ze hun naam beginnen te typen. Bovendien willen we niet dat ze karakters zoals kunnen gebruiken\
of'
, omdat deze onze SQL-query's zouden verknoeien.We gaan een stringbuilder gebruiken om dit te maken. Eerst plaatsen we enkele variabelen bovenaan onze klasse:
private int MaxNameLength = 10; private StringBuilder playerName; private bool backspaceopunt; private bool initialpress;De
MaxNameLength
moet op dezelfde lengte worden ingesteld als je hebt gebruikt voor jeVARCHAR
lengte wanneer je je tafel hebt gemaakt. Hier hebben we onze snarenbouwer,Naam speler
, en tweeBooleans
. De eerste,backspacepossible
, is om de mogelijkheid van de gebruiker om de backspace ingedrukt te houden om tekens te wissen, te regelen. De tweede is om aan te geven of ze hun naam al zijn gaan typen.In
Begin()
, we moeten voor een paar dingen zorgen. We schakelen alle tekst uit, behalve degene die is gebeldToptext
; we kunnen dat doen in aforeach
loop, zoals eerder.void Start () foreach (GUIText-tekst in FindObjectsOfType (typeof (GUIText)) als GUIText []) if (text.name! = "Toptext") text.guiText.enabled = false; GetComponent() .enabled = false; playerNameTemp = new StringBuilder (); playerNameTemp.Append ( "_"); backspacepossible = true; initialpress = false; Hier kunt u zien dat we een paar dingen hebben gedaan. We hebben onze initiële klasse uitgeschakeld (
ClickTimes
) omdat we het niet meer gebruiken. We hebben ook een instantie gemaakt vanplayerNameTemp
en voegde eraan toe_
, zodat spelers kunnen zien waar hun naam naartoe gaat en we onze variabelen hebben geïnitialiseerd.Nu moeten we toestaan dat de speler zijn naam daadwerkelijk invoert. Aan het einde van
Bijwerken()
we plaatsen het volgende fragment:guiText.text = playerNameTemp.ToString ()Dit zorgt ervoor dat de tekst weergeeft wat onze tekenreeksbouwer opneemt.
Vervolgens verwerken we tekeninvoer:
if (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);Dus, op voorwaarde dat de lengte van de tekenreeksbouwer kleiner is dan de maximale naamlengte, en zolang de gebruiker tekens invoert die letters, cijfers, spaties of onderstrepingstekens zijn (hoewel u er misschien alleen voor kiest om alfanumerieke tekens toe te staan), zal de tekenreeks toegevoegd met het nieuwe cijfer. In het geval dat dit de eerste keer is, wordt het oorspronkelijke onderstrepingsteken verwijderd voordat de nieuwe letter wordt toegevoegd.
Volgende:
if (playerNameTemp.Length> 0) if (Input.GetKeyDown (KeyCode.Backspace)) if (! initialpress) initialpress = true; backspacepossible = false; StartCoroutine (BackspaceInitialHold ()); playerNameTemp.Remove (playerNameTemp.Length - 1, 1); else if (backspacepossible && Input.GetKey (KeyCode.Backspace)) backspacepossible = false; StartCoroutine (BackspaceConstantHold ()); playerNameTemp.Remove (playerNameTemp.Length - 1, 1);Zolang er geen tekens over zijn in onze tekenreeksbuilder en backspace mogelijk is, kan de gebruiker tekens verwijderen. Let op het verschil tussen de eerste en tweede stelling. Het eerste gebruikt
GetKeyDown ()
, terwijl de laatste gebruiktGETKEY ()
(en controleert onze bool). Het onderscheid is dat we een karakter moeten wissen elke keer dat de gebruiker op de backspace drukt, maar niet constant terwijl de gebruiker hem tegenhoudt.De coroutines
BackspaceInitialHold ()
en()
gewoon wachten0.15
en0.05
seconden, en stel vervolgens inbackspacepossible
naarwaar
. Dus nadat onze gebruiker backspace heeft ingedrukt0.15
seconden, zolang ze nog steeds een spatie bevatten, wordt er elk teken gewist0.05
seconden (zolang delengte
is groter dan code> 0).Ook bepalen we dat als dit de eerste knop is die de gebruiker drukt,
initialpress
wordt geactiveerd (dus het zal niet proberen een personage te verwijderen als ze eenmaal op iets anders drukken).Als klap op de vuurpijl moeten we toestaan dat de gebruiker op drukt terugkeer om de naaminvoer te voltooien.
if (playerNameTemp.Length> 0 && initialpress) if (Input.GetKeyDown (KeyCode.Return)) foreach (GUIText-tekst in FindObjectsOfType (typeof (GUIText)) als GUIText []) text.guiText.enabled = false; GetComponent() .SetName (guiText.text); GetComponent () .enabled = true; enabled = false; Zolang de gebruiker een soort van invoer heeft gemaakt en de
lengte
is groter dan0
, de naam wordt geaccepteerd. Al onze tekstobjecten worden verwijderd, we schakelen deze klasse uit en we schakelen onze derde klas in,Hoogste score
. Alle drie onze klassen moeten in de editor op ons object worden geplaatst.We hebben net gebeld
Hoogste score
'sSetName ()
functie, en eerder hebben we gebeldSetScore ()
. Elk van deze functies stelt eenvoudig de waarden in van privévariabelen die we nu voorleggen aan ons leaderboard.
Toegang tot uw Leaderboard in Unity
Bovenop
Hoogste score
we willen enkele variabelen declareren. Eerste:openbare GameObject BaseGUIText;Dit is de
GUIText
prefab dat we ons leaderboard zullen baseren. Zorg ervoor dat de tekst is verankerd aan de middelste linker en linker uitgelijnde tekst. U kunt hier ook een lettertype kiezen.private string privateKey = "DE SLEUTEL DIE U VOORAF HEEFT GEGENEREERD"; private string AddScoreURL = "http://yoursite.com/AddScore.php?"; private string TopScoresURL = "http://yoursite.com/TopScores.php"; private string RankURL = "http://yoursite.com/GrabRank.php?"; privé int highscore; eigen string gebruikersnaam; privé int rang;We hebben al onze waarden van tevoren nodig: de sleutel die u heeft gegenereerd, de URL's waarnaar u uw PHP-bestanden hebt geüpload, enzovoort. De
hoogste score
engebruikersnaam
variabelen worden ingesteld met behulp van twee openbare functies genaamdSetScore ()
enSetName ()
, die we in het vorige gedeelte hebben gebruikt.Tip: Het is erg belangrijk dat je vraagtekens achterlaat
AddScore.php
enGrabRank.php
! Hiermee kunt u variabelen doorgeven aan uw PHP-bestanden.We moeten gebruiken
IEnumerators
hier om onze SQL-query's af te handelen, omdat we moeten wachten op een antwoord. We beginnen onze eerste coroutine,AddScore ()
, zodra de klasse is ingeschakeld.IEnumerator AddScore (string naam, int score) string hash = Md5Sum (naam + score + privateKey); WWW ScorePost = nieuwe WWW (AddScoreURL + "name =" + WWW.EscapeURL (naam) + "& score =" + score + "& hash =" + hash); opbrengst rendement ScorePost; if (ScorePost.error == null) StartCoroutine (GrabRank (naam)); else Fout ();Eerst maken we onze hash met de private key, gebruiken we een functie om een MD5-hash te maken op dezelfde manier als PHP's
md5 ()
. Er is een voorbeeld hiervan op de communitywiki van Unity.Hier, als de server om welke reden dan ook ontoegankelijk is, voeren we een
Fout()
functie. U kunt kiezen wat u in uw foutenbehandelaar wilt doen. Als de score correct wordt gepost, lanceren we onze volgende coroutine:GrabRank ()
.IEnumerator GrabRank (stringnaam) WWW RankGrabAttempt = new WWW (RankURL + "name =" + WWW.EscapeURL (name)); rendement rendement RankGrabAttempt; if (RankGrabAttempt.error == null) rank = System.Int32.Parse (RankGrabAttempt.text); StartCoroutine (GetTopScores ()); else Fout ();Nogmaals, we hebben toegang tot de site en deze keer slaan we de rang op als deze met succes wordt genomen.
Nu kunnen we onze laatste coroutine gebruiken. Deze zal alles vastbinden. We beginnen met het benaderen van de URL voor een laatste keer:
IEnumerator GetTopScores () WWW GetScoresAttempt = new WWW (TopScoresURL); opbrengst rendement GetScoresAttempt; if (GetScoresAttempt.error! = null) Error (); elseMaar deze keer willen we de gegevens die we ontvangen opsplitsen in een array van strings. Allereerst kunnen we een string split gebruiken zoals:
string [] textlist = GetScoresAttempt.text.Split (nieuwe reeks [] "\ n", "\ t", System.StringSplitOptions.RemoveEmptyEntries);Dit zorgt ervoor dat elk resultaat een nieuw element in onze reeksreeksen is, zolang een nieuwe regel of een tabblad wordt gevonden (wat het ook zal zijn!). We gaan dit nu splitsen in twee nieuwe arrays genaamd
namen
enscores
.string [] Names = new string [Mathf.FloorToInt (textlist.Length / 2)]; string [] Scores = nieuwe string [Names.Length]; voor (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];We hebben nu twee nieuwe arrays gemaakt die elk half zo groot zijn als de eerste array. Vervolgens splitsten we elke eerste reeks in onze
namen
array en elke seconde in onzescores
rangschikking.Nu willen we deze als tekst laten zien, dus we moeten onze drie kolommen positioneren. We gaan ze op het scherm schalen, zodat ze in elke resolutie passen. Eerst zullen we de beginposities aangeven waar onze titeltekst zal komen:
Vector2 LeftTextPosition = new Vector2 (0.22f, 0.85f); Vector2 RightTextPosition = new Vector2 (0.76f, 0.85f); Vector2 CentreTextPosition = new Vector2 (0.33f, 0.85f);En nu zijn we klaar om onze tekstobjecten te maken op basis van onze
BaseGUIText
prefab. We instantiëren de titels individueel en stellen hun tekst in - bijvoorbeeld:GameObject Scoresheader = Instantiate (BaseGUIText, nieuwe Vector2 (0.5f, 0.94f), Quaternion.identity) als GameObject; Scoresheader.guiText.text = "Hoge scores"; Scoresheader.guiText.anchor = TextAnchor.MiddleCenter; Scoresheader.guiText.fontSize = 35;Zodra we dit voor al onze titels hebben gedaan, passen we onze posities aan zodat de nieuwe tekst lager wordt weergegeven.
LeftTextPosition - = new Vector2 (0, 0.062f); RightTextPosition - = new Vector2 (0, 0.062f); CentreTextPosition - = new Vector2 (0, 0.062f);Vervolgens voeren we een
voor
loop die door onze hele top 10 lijst zal herhalen, de naam, rang en score zal instellen (en ervoor zal zorgen dat de tekst op een verstandige manier verankerd is), en dan de posities opnieuw zal aanpassen. Elke iteratie controleren we of de rang van de gebruiker gelijk is aan de weergegeven rang, en zo ja, verkleinen we de tekst zodat de score van de gebruiker geel wordt gemarkeerd:voor (int i = 0; iEn dan, ten slotte, controleren we of de rang van de gebruiker hoger is dan 10. Als dat zo is, plaatsen we hun score onderaan in het elfde vakje, samen met hun rang, en kleurt het geel.
if (rangorde> 10) GameObject Score = Instantiate (BaseGUIText, RightTextPosition, Quaternion.identity) als GameObject; Score.guiText.text = "" + highscore; Score.guiText.anchor = TextAnchor.MiddleCenter; GameObject Name = Instantiate (BaseGUIText, CentreTextPosition, Quaternion.identity) als GameObject; Name.guiText.text = gebruikersnaam; GameObject Rank = Instantiate (BaseGUIText, LeftTextPosition, Quaternion.identity) als GameObject; Rank.guiText.text = "" + (rang); Rank.guiText.anchor = TextAnchor.MiddleCenter; Score.guiText.material.color = Color.yellow; Name.guiText.material.color = Color.yellow; Rank.guiText.material.color = Color.yellow;
Conclusie
Voila; ons leaderboard is voltooid! In de bronbestanden heb ik ook een PHP-bestand toegevoegd dat de rangorde boven en onder de gebruikersnaam van de gebruiker zal halen, zodat je ze precies kunt laten zien waar ze zich op het bord bevinden.