DEV Community

Caleb Anthony
Caleb Anthony

Posted on • Originally published at cardboardmachete.com

The benefits and pitfalls of using Laravel to make a persistent browser-based game

I can almost hear game devs screaming in my ear...

Why on earth would you encourage anyone to use a web framework to create a game? Don't you know there are game engines?

To which I would say....yeah! Game engines are great. They solve a lot of specific game-related development problems and make life as a game developer WAY easier.

Is your game super graphical? You might be better off with a game engine.

But if your game is more text-based, has limited graphical elements, or is primarily targeting the browser... a web framework might be the better choice for you.

You're nuts!

Probably. However, I've successfully released and run Elethor which is built entirely using Laravel. The process has honestly been a joy and I've become a better developer for it.

Not just a better game dev, but a better web dev too.

All that aside, if you're still not with me at this point, just skip down to the pitfalls section and have a gloat.

The Benefits

1. Scaffolding

Out of the box you get authentication, user sessions, validation, error handling, and a whole lot more.

No setup.

No configuration.

The amount of headache this removes is a huge benefit.

If your project is purely a learning experience and you want to re-invent the authentication wheel for the 808th time, be my guest.

But if your goal is to create and release a playable game, you don't want to be spending any more time than necessary on non-game stuff.

2. Community

Laravel is super popular and only getting more popular.

Pretty much any issue / error / question you have has been asked and answered already.

You won't be spending time digging through source code or knocking your head against cryptic documentation. Spend time building your game, not debugging your lack of understanding of how a certain feature should work.

The handful of issues that I've come across that haven't already been solved can usually be tackled by a quick walk through the issues on GitHub.

3. Queues & Horizon

A lot of games handle rapid, heavy calculations. Being able to offload this work to a secondary process is immensely helpful for keeping your game feeling snappy. Plus, combined with Laravel Echo and event broadcasting, you can handle a lot of your core logic asynchronously.

Horizon works by booting up several instances of your application, storing it in memory, and then processing individual bits of code on-demand. Because you aren't having to boot your application for every request, these processes are fast. Add in a few queue workers all working side-by-side on the same server (or across several servers!) and you have a computing powerhouse on your hands.

This is Elethor running on 24 processes spread across 2 servers.

Elethor Horizon

As an example in Elethor, there is a lot going on in the Corporation (think guilds) logic. Let's take a player who has just left their corporation. A bunch of stuff happens automagically:

  1. They have their corporation permissions revoked.
  2. They leave the corporation chat.
  3. They leave all active Corp Groups they are in for group content.
  4. They are disqualified from any currently-running Corp Events.
  5. They lose their existing Corp Buffs, and Corp Buffs are recalculated for remaining members.
  6. They leave the corporation.
  7. A log item gets added to the Corp letting everyone know when the player left.

If all of these things were calculated on the same request, the player would have to wait a non-trivial amount of time.

Instead, all these actions are dispatched as jobs on the Horizon queue. This means the code looks like this:

RevokePermissions::for(Auth::user());
LeaveCorpThread::for(Auth::user());
DisintegrateGroups::for(Auth::user());
DisintegrateCorpEvents::for(Auth::user());
DisintegrateBuffs::for(Auth::user());
Track::event('corpRoster')->viaCorp(Auth::user()->corporation)->withMessage(...);

Auth::user()->corporation()->dissociate();
Enter fullscreen mode Exit fullscreen mode

Not only is this code incredibly expressive and easily readable, but each job is dispatched as an asynchronous process after the player has received a response.

Can you write this type of system on your own? Sure. But with Laravel it comes built in to the framework, which is a huge win.

4. Task Scheduling

Cron jobs are great for scheduling regular tasks, but they can make maintenance a bit more complicated. You aren't just editing your codebase, but also editing the cron file on your server. What if you have multiple servers?

Laravel lets you manage all those scheduled jobs with an expressive syntax in one central place in your code. It works by setting up a single cron job on the server that hits your code, and Laravel takes care of the rest.

Here is a small snippet of some of Elethor's scheduled jobs:

// Give players Mastery exp
$schedule->job(new Learn, 'default')->everyFiveMinutes();
// Recycle old Resource Nodes by refreshing them
$schedule->job(new CycleResourceNodes)->hourly();
// Removes expired market listings and returns items to players
$schedule->job(new RemoveOldListings)->daily();
// Gets a daily snapshot of some useful administrative statistics
$schedule->job(new TrackUsefulStats)->daily();
Enter fullscreen mode Exit fullscreen mode

Or as a more robust example, here is how the Charitable Giving Corporation event gets automatically scheduled:

$schedule->call(fn () => (new CharitableGiving)->start())
    ->name('cg:start')
    ->weekly()
    ->tuesdays();

$schedule->call(fn () => (new CharitableGiving)->end())
    ->name('cg:end')
    ->weekly()
    ->sundays();
Enter fullscreen mode Exit fullscreen mode

I don't even have to explain this code it's so expressive.

This is spread out across several servers and I don't have to worry about tasks being duplicated or getting dropped. Plus I can spin up another server with the same codebase and be off and running with a single cron entry.

Glorious.

5. Eloquent / Query Builder

Eloquent is Laravel's ORM. It provides a great syntax for fetching, manipulating, and extending your database models.

You don't have to write raw SQL in your application because Eloquent handles all that behind the scenes.

On the home page of Elethor, I fetch the latest announcements to show to players who are logging in.

News::orderBy('updated_at', 'desc')
    ->where('type', 'announcement')
    ->get();
Enter fullscreen mode Exit fullscreen mode

Dead simple, and I'm off to the races.

But Eloquent isn't always the ideal tool for the job.

Now this one is a bit controversial because to be honest, Eloquent is sometimes heavy-handed, especially in game dev. There have been several situations where Eloquent technically gives me what I want but is much slower at getting it than writing my own SQL would be.

Which is where the Query Builder comes into play. I still don't have to write raw SQL in my game because I can just use the incredibly flexible query builder.

As an example, players in Elethor have several Companions. Companions are level-able NPCs that give a bunch of buffs. These Companions gain exp on a regular basis, so it needs to be lightweight to give this exp.

Using Eloquent, I could get this:

Companion::where('user_id', $this->userId)
    ->get()
    ->each(function (Companion $companion) {
        $companion->experience += $this->exp;
        $companion->save();
    });
Enter fullscreen mode Exit fullscreen mode

This does exactly what I want, but it also fetches a lot of unnecessary data and hydrates each Companion model. That's overhead that I don't need and can't afford.

So instead I use the Query Builder.

DB::table('companions')
    ->where('user_id', $this->userId)
    ->increment('experience');
Enter fullscreen mode Exit fullscreen mode

Now I'm not fetching anything from the database. I'm just updating the companions with a single SQL statement.

Now this has a few caveats (which is probably a whole blog post on its own) but having the option to use the expressive Laravel syntax without being forced into a bulky ORM is brilliant.

6. Notifications

This ties in great to game development. Laravel makes it dead easy to send notifications via Discord, Email, SMS or several dozen other options as well.

Has your player just reached a new level milestone?

Notification::send($user, new LevelMilestone($user->level));
Enter fullscreen mode Exit fullscreen mode

Has their kingdom just been attacked?

Notification::send($attackedUser, new UnderAttack($kingdom));
Enter fullscreen mode Exit fullscreen mode

Is there an event starting soon? You get the picture.

Write a single notification class (like a new event starting) and then identify all your channels on that one notification. Then easily pick and choose which channels the notification should go to on case-by-case basis.

It's so easy.

// UnderAttack.php
public function via($notifiable)
{
    return $notifiable->prefers_discord ? ['discord'] : ['mail', 'sms'];
}
Enter fullscreen mode Exit fullscreen mode

Instead of having to set up your own integration, just use Laravel. Now you're back to writing the code you care about (which is your game code, if you were curious).

The Pitfalls

This whole section can partially be summed up in "Laravel isn't a game engine".

But you already knew that.

While Laravel is great at a lot, especially when it comes to building browser-based games, it has its own pitfalls as well.

1. Eloquent

A benefit and a pitfall? Yeah, but honestly I would still rule it as mostly a benefit. A benefit with caveats. A double-edged sword cuts both ways. I'll show you what I mean...

When it comes to game development there can be a lot of calculations happening in a short span of time. Involving lots of various models.

Being able to easily fetch models via Eloquent is a great tool. But if you're not careful you can end up with poorly performing code that is quite confusing.

The earlier Companions example is a great starting point:

Companion::where('user_id', $this->userId)
    ->get()
    ->each(function (Companion $companion) {
        $companion->experience += $this->exp;
        $companion->save();
    });
Enter fullscreen mode Exit fullscreen mode

Once you get familiar with Eloquent, you quickly learn that ->get() is significant. It's the point where your records are fetched from the database.

There are a couple of other key words like this too, such as ->first() and ->all().

If you're not aware, you could accidentally do something like this:

Companion::all()
    ->where('user_id', $this->userId)
    ->each(...);
Enter fullscreen mode Exit fullscreen mode

While this looks similar to the earlier code, it's critically different.

This example fetches every single Companion from the database.

Then it filters those models down to the ones you want.

With 20 players? No problem.

With 5,000? Fatal error: Out of memory (allocated...

When you run into these gradual slowdowns, it isn't immediately clear where the issue is. Even if you isolated the issue, it's not immediately clear what is wrong with that code.

Is that a fault of Eloquent itself? Not...necessarily.

But it absolutely qualifies as a pitfall.

2. Websockets

You can do native websockets with PHP, but it's not straightforward with Laravel. Laravel Echo helps handle the client-side of websockets, but you need to manage the server side on your own.

Initially, I used an intermediary NodeJS server, then switched to handling websockets natively in PHP, and am about to switch back to a NodeJS server.

Is this the fault of Laravel? Not necessarily. But if they had a clearer setup process and recommended (open-source) packages it would make this a bit less of a potential pitfall.

3. Request / Response

PHP is built around a request/response structure. Processes are short-lived and data isn't stored in memory between requests.

For the vast majority of web applications, this is great.

For game development, it can be less than ideal. A lot of games want to store certain data in memory and re-use it between requests.

Caching can help, but if your cache is on a different server you have to deal with network overhead.

Laravel has a solution for this called Octane that works on top of something like Swoole. This gives you concurrency, faster cache management, and some other goodies. But it has its own caveats.

It's a bit of a time investment to get something that other languages (or game engines) get out of the box.

Is it a reason to avoid Laravel altogether? Not in my opinion, but it is a potential pitfall to be aware of.

4. Realtime

This isn't a fault of Laravel as a framework, but more a shortcoming of PHP. PHP just wasn't designed to do realtime.

If your game is going to rely on realtime at all, it's probably best to find another tech to work with.

Right tool for the job, and all that.

Summary

I personally have been quite happy with my decision to use Laravel to create my games thus far.

Is it always the best solution? Obviously not.

But for the types of games I have made, it has been great and has helped me get up and running quickly.

Should you use Laravel for your next project? That's entirely up to you, but hopefully this article gave you a few tidbits to help make the best decision.

Top comments (0)