1
\$\begingroup\$

I have been developing a game project (hobby) for the last 2 to 3 years. The game is using JS on the frontend and PHP on the backend. The game is turn-based and each turn is divided into 4 phases and it's only playable Human vs Human. Basically, the game is very similar (in flow) to a tabletop board game.

The normal procedure is that a player logs in, checks into a game and does the things he can do in that specific phase. He then confirms his order, the stuff is saved in a DB and hopefully another player will do the same.

Whenever a player does logs in, the backend checks all of the games of that are ready for procession (= every players orders are in) and if there is game with all players set, it will process the game's phase. I know it would be better to check and process after a player confirms his order, but its not a priority for now.

Now, I'm a hobby-dev and even though I'm literally hundreds of hours into this project (could be 1000++ T.B.H.), it very likely to suffer from a bunch of fatal flaws, even though I would like to note that the game is working just fine, bugs aside. The game is divided into phases, which set apart the major steps of a turn, where one phase revolves around the resolution of fire orders (starships duking it out based on player orders). One thing that stands out to me is how I handle the resolution of the firing phase.

The game state is entirely handled via a Manager class which upon instantiation queries the DB for all relevant data, assemble the various game objects and either progresses the game (if possible) or forwards them to the client. The important game objects (that is Starships and their smaller cousins as well as every type of weapon) follow OOP hierarchy. I.e. A fighter Flight as well as a Squadron of frigates are both derived from a Mixed class, which is derived from the base Ship class. In the same vein, a HeavyLaser is derived from Weapon, which is derived from a BaseSystem I would say there is a lot of class extension happening.

All DB-stuff is handled by DBmanager->class which can basically return itself via a static call and I'm going to post its content. So basically, when you enter /game.php?gameid=4, the following happens:

include_once 'global.php';
    $gameid = $_GET["gameid"];
    $userid;

    if (isset($_SESSION["userid"])) {
        $userid = $_SESSION["userid"];
    } else $userid = 0;

    $manager = new Manager($userid, $gameid);
    if ($manager->status == "active"){
        $manager->getGameData();
    } else {
        header("Location: lobby.php");
    }

    $post = json_encode($manager->getClientData(), JSON_NUMERIC_CHECK);
    echo "<script>";
    echo "window.game = ".$post.";";
    echo "</script>";
?>


<!DOCTYPE html>
<html>

The Manager (the heart) that is actually supplying the game data to the client:

class Manager {
    public $userid;
    public $gameid;

    public $name;
    public $status;
    public $turn;
    public $phase;
    public $pv;
    public $reinforce;

    public $index = 0;
    public $faction = "";
    public $value = 0;
    public $wave = 2;

    public $ships = array();
    public $ballistics = array();
    public $gd = array();
    public $fires = array();
    public $playerstatus = array();
    public $reinforcements = array();
    public $rdyReinforcements = array();
    public $deploys = array();
    public $incoming = array();
    public $userindex = 0;

    function __construct($userid = 0, $gameid = 0){
        $this->userid = $userid;
        $this->gameid = $gameid;

        if ($this->gameid){
            $this->getGameData();

        }
    }

    public function getGameData(){ // gets data from DB
        $db = DBManager::app();

        $this->setReinforceStatus();
        $this->ships = $this->assembleUnits();
        $this->fires = $db->getUnresolvedFireOrders($this->gameid, $this->turn); // creates FireOrder objects out of DB data
        $this->reinforcements = $db->getAllReinforcements($this->gameid, $this->userid););
        $this->incoming = $db->getIncomingShips($this->gameid, $this->turn);
    }


    public function getClientData(){ // is called by Game.php, reworks the PHP data to the actual client data
        return array(
            "id" => $this->gameid,
            "name" => $this->name,
            "status" => $this->status,
            "turn" => $this->turn,
            "phase" => $this->phase,
            "ships" => $this->getShipData(),
            "reinforcements" => $this->rdyReinforcements,
            "incoming" =>$this->getIncomingData(),
            "const" => $this->const,
            "username" => $this->getUsername(),
            "wave" => $this->wave
        );
    }

Since we are focusing on resolving the firing phase, I will highlight this:

The important thing is here is that all "units" like starships, flights and stuff are stored within $Manager->ships as classes.

Every fireorder (that is the order by the player to fire weapon x at target y) are stored in their table which are stored in $Manager->fires. That way I can avoid looping over all starships and their subsystems to gather them manually.

When we decide the game is ripe for phase advancement:

    public function doAdvance(){
        $time = -microtime(true);

        switch ($this->phase){
            case -1; // from deploy to move
                $this->handleDeploymentPhase();
                $this->startMovementPhase();
                break;
            case 0; // ship moves
                $this->handleMovementPhase();
                $this->startFiringPhase();
                break;
            case 2; // from fireorders to resolve fire
                $this->handleFiringPhase();
                $this->startDamageControlPhase();
                break;
            case 3; // from damage control to NEW TURN - deploymnt
                $this->handleDamageControlPhase();
                $this->endTurn();
                $this->startNewTurn();
                $this->startDeploymentPhase();
                break;
            default:
                break;
        }

        $time += microtime(true); 
        Debug::log("TIME: ".round($time, 3)." seconds.");
        return true;
    }

The thing we focus on is case 2, which leads us to the following function

public function handleFiringPhase(){
        $this->setupShips();

        $this->setFireOrderDetails();
        $this->sortFireOrders();
        $this->resolveShipFireOrders();
        $this->resolveFighterFireOrders();
        $this->resolveBallisticFireOrders();
        $this->testCriticals();

        $this->handleResolvedFireData();
        return true;
    }

What does it do ? Basically there are a ton of weapon shots that need to be calculated based on distance and angle within the different ships.

$this->setupShips(); will tell each ships its accurate distance to every other ship / unit as well as the angle of any incoming shots from any other ships - so you don't calculate distance and angle 50 times per unit.

And now we finally arrive at the most important thing. What actually IS a FireOrder ? It's an object that is a 1:1 copy of a database entry. It holds the order id, shooter id, target id, weapon id etc.

class FireOrder {
    public $id;
    public $gameid;
    public $turn;
    public $shooterid;
    public $targetid;
    public $x;
    public $y;
    public $weaponid;
    public $shots;
    public $req;
    public $rolls = array();
    public $notes;
    public $hits;
    public $resolved;
    public $cc = false;

    public $shooter = false;
    public $target = false;
    public $weapon = false;
    public $dist = false;
    public $angle = false;
    public $singleid = false;

    function __construct($id, $gameid, $turn, $shooterid, $targetid, $x,$y, $weaponid, $shots, $req, $notes, $hits, $resolved){
        $this->id = $id;
        $this->gameid = $gameid;
        $this->turn = $turn;
        $this->shooterid = $shooterid;
        $this->targetid = $targetid;
        $this->x = $x;
        $this->y = $y;
        $this->weaponid = $weaponid;
        $this->shots = $shots;
        $this->req = $req;
        $this->notes = $notes;
        $this->hits = $hits;
        $this->resolved = $resolved;
    }

As you can see, a lot of the props are directly derived on construction (= database values). There are however placeholder props at the bottom ($shooter, $target, $fire) This is where the other functions of our "phase advancement" routine come in.

public function setFireOrderDetails(){
        for ($i = sizeof($this->fires)-1; $i >= 0; $i--){
            $this->fires[$i]->shooter = $this->getUnit($this->fires[$i]->shooterid);
            $this->fires[$i]->weapon = $this->fires[$i]->shooter->getSystem($this->fires[$i]->weaponid);
            $this->fires[$i]->shots = $this->fires[$i]->weapon->getShots($this->turn);
            $this->fires[$i]->target = $this->getUnit($this->fires[$i]->targetid);
        }
    }

Basically, we now reference our "simple" FireOrder-objects with the actual game objects. And this is where the trouble starts.

    public function resolveShipFireOrders(){
        // resolve ship vs ship / fighter
        for ($i = 0; $i < sizeof($this->fires); $i++){
            if ($this->fires[$i]->resolved){continue;}
            if ($this->fires[$i]->shooter->flight){continue;}

            $this->fires[$i]->target->resolveFireOrder($this->fires[$i]);
        }
    }

For a variety of reasons, I decided to resolve any 'FireOrder' within the scope of the 'target'. That way I can have different functions that determine toHit or Damage, based on the target (A starship might be easier to hit, or more prone to weapon X, than a flight). So, what I'm doing here is passing each 'fireOrder' to the target of said fireOrder, which in turn is already pointed to by the fireOrder...

And this is troubling me greatly. THe game is working fine, it's playable and the performance seems okay to me. However, I would like to make it cleaner, especially in regards to resolveFireOrders and the scoping. IF it is possible. Further down the road, within the context of $target->resolveFireOrder($fire), I'm also occasionally switching the scope from within '$target' to '$fire->weapon->doDamage($this)'

It is my impression that my approach, even though it is working just fine for me, is pretty strange, if not outright fatalistic. I think there must be some smart way to resolve the $this->fireOrders[$i]->shooter = $this->getUnit($this->fireOrders[$i]->shooterid)

Can someone advise on this front?

\$\endgroup\$
2
\$\begingroup\$

I would suggest constants be defined for the values of the Manager::phase - e.g.

const PHASE_FROM_DEPLOY_TO_MOVE = -1;
const PHASE_SHIP_MOVES = 0;
const PHASE_FROM_FIREORDERS_TO_RESOLVE_FIRE = 2;
const PHASE_FROM_DAMAGE_CONTROL_TO_NEW_TURN_AND_DEPLOYMENT = 3

And use those in constant names in place of the values. That way, somebody looking at the logic can have an idea what those values mean - and you don't have to repeat those comments everywhere the value is used...


I notice in the sample implementation of handleFiringPhase() that true is returned, but doAdvance() doesn't appear to do anything with the return value. You should re-evaluate the need to return a value. Also, would one of the methods called by that method ever return false to signify failure or something along those lines?


For your implementation of setFireOrderDetails I would suggest using foreach to make the syntax cleaner - I know you are looping in the reverse order so for that, perhaps array_reverse() would suffice?


Bearing in mind that it has been 9 months since you posted this code, have you made any updates to it? If so, were there any updates to the main code you asked the question about (i.e. resolving the fireorder within the scope of the "target")?

\$\endgroup\$
  • \$\begingroup\$ Good, thats a surprise. Yes, i have continued developing the game and i refactored large amounts of code. I also changed the scope on which some functions resolve, specifically fireorders. FYI, the reason why im looping fireorders in reverse is that this allows to splice in the fiy, should a fireorder be resolved against a target which was destroyed by an earlier fireorder. Thanks for your time in replying this late. \$\endgroup\$ – user431806 Nov 11 '18 at 12:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.