Anmelden

browsergames tutorials

Der Weg zum eigenen Browsergame - Ressourcenproduktion

Zu einem guten Strategie-Browsergame gehört auch ein interessantes Ressourcenmanagement. Denn gerade hier lässt sich strategisch viel bewegen. So muss vor einer großen Schlacht genügend Ressourcen vorhanden sein um die Bevölkerung ausreichend lange zu ernähren, um genügend neue Waffen und Rüstungen herzustellen, oder um die beschädigten Gebäude wieder reparieren zu können.

Unser Ziel

Wir wollen eine Ressource haben, deren Menge stetig zunimmt (=Produktion), in der auch mal eine Menge X auf einen Schlag hinzukommt (z.B. bei Lieferungen), eine Menge Y wegfällt (z.B. beim Gebäudeausbau) und für die man mittendrin die Produktionsrate ändern kann.

Der erste Ansatz

Um eine stetig anwachsende Produktion zu erreichen, brauchen wir einen Startzeitpunkt, eine Produktion pro Sekunde und einen Maximalwert, an dem die Produktion stoppen soll.

Angenommen wir haben den Zeitpunkt Jetzt, der genau 10 Sekunden hinter dem Startzeitpunkt der Produktion liegt. Bei einer Produktion von 1,5 Stück pro Sekunde berechnet sich der aktuelle Wert folgendermaßen:

Produktion pro Sekunde = 1,5
Seit dem Startzeitpunkt vergangene Zeit = 10 Sekunden

Ressourcenanzahl = 10 Sekunden * 1,5 = 15

Zum Zeitpunkt Jetzt liegen also 15 Stück der Ressource in unserem Lager. Läge der Maximalwert bei 13, müsste man im nächsten Schritt die berechnete Menge auf den Maximalwert reduzieren.

WENN Ressourcenanzahl > Maximalwert DANN Ressourcenanzahl = Maximalwert

Die Problematik

Das mit der Ressourcenproduktion klingt ja erstmal unproblematisch.
Wenn man sich aber die Verhaltensweise von PHP bei der Berechnung (oder beim Vergleich) von Fließkommazahlen genauer betrachtet, kann man schon erahnen wo hier die Problematik liegt. Zitat aus der online Sprachreferenz:

"Fließkommazahlen haben eine begrenzte Präzision. [...] bei Durchführung von mehreren Operationen können sich die Fehler addieren."
(Quelle: http://php.net/manual/de/language.types.float.php)

Betrachten wir mal folgendes kleines Beispiel:

 
// x ist (0,1 + 0,7) mal 10, abgerundet auf eine Ganzzahl.
$x = floor( (0.1 + 0.7) * 10 );
echo $x; // Man würde eine 8 erwarten. Es wird aber eine 7 ausgegeben.
 

PHP (und auch andere Skriptsprachen/Hochsprachen) verstehen beispielsweise eine 0,7 nicht 100%ig als 0,7, sondern eher als einen Näherungswert, z.B. 0,69999999999999.
Berechnet man jetzt auf Basis dieses Näherungswertes einen weiteren Wert, so wird dieser "noch falscher" sein ... usw. usw.

Das alles soll uns aber nicht davon abhalten präzise Berechnungen durchzuführen.

Keine Probleme, nur Lösungen

Die Macher von PHP haben sich über diese Problematik natürlich auch schon Gedanken gemacht und uns die Standardbibliothek "BC Math" ab PHP Version 4.0.4 zur Verfügung gestellt.

Aber ACHTUNG! Wir müssen bei den Funktionen dieser Bibliothek folgendes beachten.
Hier wird bei den Berechnungen ausschließlich mit Strings gearbeitet. Sobald wir in die Berechnung eine Fließkommazahl einfließen lassen, haben wir wieder das oben beschriebene Problem. Heißt also für uns, künftig, bei Berechnungen, nur noch mit Strings zu arbeiten. Fällt anfangs etwas schwer, man gewöhnt sich aber schnell dran.

Die Ressource Gold

Genug Theorie. Produzieren wir Gold.

Wie bereits oben beschrieben, benötigen wir für unsere Produktion ein paar Informationen, die üblicherweise in einer Datenbank gespeichert werden. In diesem Tutorial verwende ich dafür, der Einfachheit wegen, globale Variablen.

$GOLD_START_TIME = Zeitpunkt an dem die Produktion begonnen hat als Zeitstempel.
$GOLD_PPS = Produktion pro Sekunde
$MAX_VALUE = Maximalwert der zu produzierenden Ressource.
$NOW = Aktueller Zeitpunkt für den die Anzahl der produzierten Ressourcen berechnet werden soll als Zeitstempel.

Betrachten wir uns einmal die Funktion die uns den aktuellen Ressourcenstand zurückgibt.

 
bcscale('3'); // Die Berechnungsgenauigkeit soll bis zur 3. Stelle hinter dem Komma reichen (Millisekunden genau).
function getProductionValue($start_time, $now, $pps, $max_value)
{
    $time_diff = bcsub((string)$now, (string)$start_time); // Vergangene Zeit = Berechnungszeitpunkt - Produktionsstartzeitpunkt
    $value = bcmul($time_diff, (string)$pps); // Aktueller Ressourcenstand = Vergangene Zeit * Produktion pro Sekunde
    if ( bccomp($value, (string)$max_value)==1 ) $value = (string)$max_value; // WENN Aktueller Ressourcenstand > Maximaler Ressourcenstand DANN Aktueller Ressourcenstand = Maximaler Ressourcenstand
    return $value;
};
 

Man beachte, dass wir hier zur Berechnung ausschließlich Funktionen der "BC Math"-Bibliothek verwenden. Alle Werte sind hier vom Typ String.

Wenn wir diese Funktion mit dem aktuellen Zeitstempel füttern, liefert sie uns die Anzahl an Gold die bis jetzt produziert wurde.

 
echo 'Im Lager befindet sich <b>'.bcadd(getProductionValue($GOLD_START_TIME, $NOW, $GOLD_PPS, $MAX_VALUE), '0', 0).'</b> Gold.';
 

Die Funktion getProductionValue() liefert uns einen String der eine Zahl mit Nachkommastellen darstellt. Um diese "Nachkommastellen" zu beseitigen, addieren wir zu diesem Wert mittels der "BC Math"-Funktion bcadd() eine 0 und geben als dritten Parameter die Anzahl der Nachkommastellen an, also 0. Dadurch erhalten wir einen ganzzahligen Wert. Selbst an dieser Stelle ist es wichtig den von der Funktion zurückgelieferten Wert nicht als Zahl zu betrachten, sonst werden die Werte verfälscht.

Um nicht jedesmal mit der bcadd()-Funktion arbeiten zu müssen, könnten wir uns auch eine Funktion basteln, die uns künftig die umständliche Tipparbeit erspart:

 
function getGold($now)
{
    global $GOLD_START_TIME, $GOLD_PPS, $MAX_VALUE;
    // Für die Ausgabe ohne Nachkommastellen
    return bcadd(getProductionValue($GOLD_START_TIME, $now, $GOLD_PPS, $MAX_VALUE), '0', 0);
};
 
echo 'Im Lager befindet sich <b>'.getGold($NOW).'</b> Gold.';
 

Mit Gold bezahlen

Was nutzt uns diese tolle Ressource wenn wir damit nichts kaufen können? Nichts.

Mit der Funktion getProductionValue() erhalten wir immer den produzierten Wert. Was ist aber wenn sich der Wert mittendrin durch einen Kauf ändert?

Nehmen wir mal zwei globale Variablen hinzu.

$GOLD_UPDATE_TIME = Zeitpunkt an dem der Wert $GOLD_VALUE zuletzt berechnet wurde. Zu Anfang ist $GOLD_UPDATE_TIME = GOLD_START_TIME.
$GOLD_VALUE = Der zuletzt berechnete Wert. Startwert is 0.

Mit diesen zwei globalen Variablen können wir uns die letzte Änderung des Ressourcenwertes merken. Diese Werte sollten bei Änderung ebenfalls in einer Datenbank abgespeichert werden, sonst gehen diese Informationen verloren.

Modifizieren wir unsere kleine Funktion getGold() ein bisschen und fügen zwei Weitere hinzu:

 
$GOLD_UPDATE_TIME = $GOLD_START_TIME; // Oder gespeicherter Wert aus der Datenbank
$GOLD_VALUE = 0; // Oder gespeicherter Wert aus der Datenbank
 
function getGold($now)
{
    // Für die Ausgabe ohne Nachkommastellen
    return bcadd(getGoldValue($now), '0', 0);
};
function getGoldValue($now)
{
    global $GOLD_START_TIME, $GOLD_PPS, $MAX_VALUE, $GOLD_UPDATE_TIME, $GOLD_VALUE;
 
    // Wenn das letzte Update jetzt stattgefunden hat,
    // können wir direkt den zuletzt berechneten Wert liefern
    if (bccomp($GOLD_UPDATE_TIME, $now)==0) return $GOLD_VALUE;
 
    // WENN GOLD_START_TIME >= $GOLD_UPDATE_TIME DANN
    if (bccomp($GOLD_START_TIME, $GOLD_UPDATE_TIME)>=0)
    {
        // Es hat noch kein Update stattgefunden,
        // also berechnen wir die Ressourcen von Anfang an.
        $time = $GOLD_START_TIME;
    }
    else
    {
        // Es hat bereits ein Update stattgefunden.
        // Es müssen also die Ressourcen berechnet werden die nach diesem Zeitpunkt hinzukamen.
        $time = getLaterTime($GOLD_UPDATE_TIME);
    };
    $value = bcadd($GOLD_VALUE, getProductionValue($time, $now, $GOLD_PPS, $MAX_VALUE));
    if ( bccomp($value, $MAX_VALUE)==1 ) $value = $MAX_VALUE; // WENN Aktueller Ressourcenstand > Maximaler Ressourcenstand DANN Aktueller Ressourcenstand = Maximaler Ressourcenstand
    if ( bccomp($value, 0)==-1 ) $value = 0; // WENN Aktueller Ressourcenstand < 0 DANN Aktueller Ressourcenstand = 0
    return $value;
 
};
function getLaterTime($time)
{
    return bcadd((string)$time, '0.001'); // Eine Millisekunde mehr = Zeit + 0,001
};
 

Damit haben wir die Grundlage für die kommenden Ressourcenmanipulationen geschaffen. Im Folgenden betrachten wir uns die Funktionen zum hinzufügen/abziehen von Gold, sowie die Änderung der Produktion pro Sekunde (z.B. hervorgerufen durch eine Gebäudeupgrade).

 
function addGold($value, $now)
{
    global $GOLD_UPDATE_TIME, $GOLD_VALUE;
 
    $value = (string)$value;
 
    if ($value[0]=='-')
    {
        $value = substr($value, 1); // Absolutwert
        // $GOLD_VALUE = Wert bis zum Zeitpunkt $now minus dem abzuziehenden Wert $value
        $GOLD_VALUE = bcsub(getGoldValue($now), $value);
    }
    else
    {
        // $GOLD_VALUE = Wert bis zum Zeitpunkt $now plus dem hinzuzufügenden Wert $value
        $GOLD_VALUE = bcadd(getGoldValue($now), $value);
    };
    // Das letzte Update fand zum Zeitpunkt $now statt
    $GOLD_UPDATE_TIME = $now;
};
function changeGoldPPS($pps, $now)
{
    global $GOLD_UPDATE_TIME, $GOLD_VALUE, $GOLD_PPS;
    // Wir merken uns den Goldwert für den Zeitpunkt $now
    $GOLD_VALUE = getGoldValue($now);
    // Das letzte Update fand zum Zeitpunkt $now statt
    $GOLD_UPDATE_TIME = $now;
    // Neue Produktion pro Sekunde ab $now festlegen    
    $GOLD_PPS = (string)$pps;
};
 

Mit addGold() lässt sich Gold hinzufügen oder abziehen. Die Funktion changeGoldPPS() ändert die Produktion pro Sekunde. Die neue Produktionsrate gilt aber erst ab dem angegebenen Zeitpunkt. Das bis dahin produzierte Gold wird mit der alten Produktionsrate berechnet und in $GOLD_VALUE gespeichert.

Und fertig ist die Ressourcenproduktion

Fassen wir mal alles in einem großen Beispiel zusammen.

Der folgende Code kann ohne Anpassungen direkt ausgeführt werden:

 
function getProductionValue($start_time, $now, $pps, $max_value)
{
    $time_diff = bcsub((string)$now, (string)$start_time); // Vergangene Zeit = Berechnungszeitpunkt - Produktionsstartzeitpunkt
    $value = bcmul($time_diff, (string)$pps); // Aktueller Ressourcenstand = Vergangene Zeit * Produktion pro Sekunde
    if ( bccomp($value, (string)$max_value)==1 ) $value = (string)$max_value; // WENN Aktueller Ressourcenstand > Maximaler Ressourcenstand DANN Aktueller Ressourcenstand = Maximaler Ressourcenstand
    return $value;
};
function getGold($now)
{
    // Für die Ausgabe ohne Nachkommastellen
    return bcadd(getGoldValue($now), '0', 0);
};
function getGoldValue($now)
{
    global $GOLD_START_TIME, $GOLD_PPS, $MAX_VALUE, $GOLD_UPDATE_TIME, $GOLD_VALUE;
 
    // Wenn das letzte Update jetzt stattgefunden hat,
    // können wir direkt den zuletzt berechneten Wert liefern
    if (bccomp($GOLD_UPDATE_TIME, $now)==0) return $GOLD_VALUE;
 
    // WENN GOLD_START_TIME >= $GOLD_UPDATE_TIME DANN
    if (bccomp($GOLD_START_TIME, $GOLD_UPDATE_TIME)>=0)
    {
        // Es hat noch kein Update stattgefunden,
        // also berechnen wir die Ressourcen von Anfang an.
        $time = $GOLD_START_TIME;
    }
    else
    {
        // Es hat bereits ein Update stattgefunden.
        // Es müssen also die Ressourcen berechnet werden die nach diesem Zeitpunkt hinzukamen.
        $time = getLaterTime($GOLD_UPDATE_TIME);
    };
    $value = bcadd($GOLD_VALUE, getProductionValue($time, $now, $GOLD_PPS, $MAX_VALUE));
    if ( bccomp($value, $MAX_VALUE)==1 ) $value = $MAX_VALUE; // WENN Aktueller Ressourcenstand > Maximaler Ressourcenstand DANN Aktueller Ressourcenstand = Maximaler Ressourcenstand
    if ( bccomp($value, 0)==-1 ) $value = 0; // WENN Aktueller Ressourcenstand < 0 DANN Aktueller Ressourcenstand = 0
    return $value;
 
};
function getLaterTime($time)
{
    return bcadd((string)$time, '0.001'); // Eine Millisekunde mehr = Zeit + 0,001
};
function addGold($value, $now)
{
    global $GOLD_UPDATE_TIME, $GOLD_VALUE;
 
    $value = (string)$value;
 
    if ($value[0]=='-')
    {
        $value = substr($value, 1); // Absolutwert
        // $GOLD_VALUE = Wert bis zum Zeitpunkt $now minus dem abzuziehenden Wert $value
        $GOLD_VALUE = bcsub(getGoldValue($now), $value);
    }
    else
    {
        // $GOLD_VALUE = Wert bis zum Zeitpunkt $now plus dem hinzuzufügenden Wert $value
        $GOLD_VALUE = bcadd(getGoldValue($now), $value);
    };
    // Das letzte Update fand zum Zeitpunkt $now statt
    $GOLD_UPDATE_TIME = $now;
};
function changeGoldPPS($pps, $now)
{
    global $GOLD_UPDATE_TIME, $GOLD_VALUE, $GOLD_PPS;
    // Wir merken uns den Goldwert für den Zeitpunkt $now
    $GOLD_VALUE = getGoldValue($now);
    // Das letzte Update fand zum Zeitpunkt $now statt
    $GOLD_UPDATE_TIME = $now;
    // Neue Produktion pro Sekunde ab $now festlegen    
    $GOLD_PPS = (string)$pps;
};
 
$GOLD_START_TIME = (string)mktime(12, 20, 10, 3, 5, 2011); // = 05.03.2011 12:20:10 (alternativ ein geladener Wert aus der Datenbank)
$GOLD_PPS = '0.1'; // Alle 10 Sekunden gibt es ein Gold dazu
$GOLD_UPDATE_TIME = $GOLD_START_TIME; // Oder ein gespeicherter Wert aus der Datenbank
$GOLD_VALUE = 80; // Oder ein gespeicherter Wert aus der Datenbank
$MAX_VALUE = 100; // Das Lager fasst maximal 100 Gold
 
bcscale('3'); // Millisekundengenaue Berechnung
 
echo date("H:i:s", (int)$GOLD_START_TIME).' | Start der Goldproduktion. Im Lager: '.getGold($GOLD_START_TIME).'<hr />';
 
$NOW = bcadd($GOLD_START_TIME, '10') ;    // 10 Sekunden nach $GOLD_START_TIME
echo date("H:i:s", (int)$NOW).' | Gold im Lager: '.getGold($NOW).'<hr />';
 
$NOW = bcadd($NOW, '10') ;    // 10 Sekunden später
addGold(-50, $NOW); // Ziehen wir 50 Gold ab
echo date("H:i:s", (int)$NOW).' | Gold im Lager nach Bezahlung von 50 Gold: '.getGold($NOW).'<hr />';
 
$NOW = bcadd($NOW, '10') ;    // 10 Sekunden später
changeGoldPPS('1', $NOW); // Wir erhöhen die Produktion auf 1 Gold pro Sekunde
echo date("H:i:s", (int)$NOW).' | Gold im Lager: '.getGold($NOW).'<hr />';
 
$NOW = bcadd($NOW, '10') ;    // 10 Sekunden später
echo date("H:i:s", (int)$NOW).' | Gold im Lager nach 10 Sekunden mit erhöhter Produktion (1 pro Sekunde): '.getGold($NOW).'<hr />';
 
$NOW = bcadd($NOW, '60') ;    // 1 Minute später
echo date("H:i:s", (int)$NOW).' | Gold im Lager nach einer weiteren Minute mit erhöhter Produktion: '.getGold($NOW).'<hr />';
 

Schlusswort

Ein oft gemachter Fehler ist, dass man die PHP eigene Fließkommazahlenberechnung überschätzt und ihr zu viel Glauben schenkt. Mit der integrierten "BC Math"-Bibliothek (http://de.php.net/manual/en/ref.bc.php) kann man diese Probleme leicht vermeiden. Allerdings muss man auch erst mal darauf kommen das hier überhaupt Handlungsbedarf herrscht.

Nehmt dieses Tutorial als Grundlage eurer eigenen Codes und packt den Krams am besten erst mal in eine Klasse.

Viel Erfolg!