How to Get Timers in Lua Modules?

I have several questions about Nakama that I’d like to get answers to. (I’ve been developing with Nakama for some time and have accumulated all these questions to ask together.)

  1. I’m using Lua to write modules for a turn-based online game, and I have a strong dependency on timer functionality in multiple scenarios – for example, the timed expiration of buffs, the timed completion of quests, and quests/activities that automatically start at specified times. However, I haven’t found a way to implement timers/loops (e.g., a loop that runs every 1 second) where I can write custom code to track time and process time-dependent queues.

  2. Why can’t I access data from other modules within a match? For instance, I have a PlayerManager singleton that contains a Lua table storing data for all online players, but I cannot access this PlayerManager singleton inside a match.

  3. How can a match actively notify external modules to send data? For example, when a battle ends, how can the match inform external modules of this event and send the battle results to them? In short, can a match actively communicate with other modules?

Finally, I want to ask if my current usage approach is recommended. Since I haven’t resolved the second and third questions above, I don’t store any data in matches – I only use matches as room broadcasters. Currently, I treat each map as a separate match and each battle as a separate match. Could this cause issues? Can a player join multiple matches simultaneously? Is this approach advisable?

As an additional question: What is the “first-class” scripting language for Nakama? Do most developers use Lua or Go?

Hi @1609988,

Will reply to each of your questions:

  1. For most time-related activities, storing an expiry time (or time range) alongside the buf/quest/activity is usually enough, having the client only render them (or time their expiration) if non-expired or within their active window, and have server side checks that apply the buf if active (within expiration or time-range). Background workers can somewhat simplify the implementation but are often unnecessary, and at times can create bottlenecks at scale - if the worker becomes overloaded, then the processing may become delayed, causing unwarranted side-effects.
    Nakama also has an event pipeline for background processing, events can be emitted from any runtime, but the processor itself has to be registered via the Go runtime (this is a constraint due to the performance implications/overhead of exposing the processor in Lua/JS).
    Another common alternative to background workers is exposing a S2S RPC that is called periodically via CRON, but you’d have to setup and manage CRON yourself.

  2. In the Lua and JS runtimes, an authoritative match is a dedicated Lua/JS VM, so it has its own global state, which won’t be visible to other VMs. Moreover, the Lua and JS runtimes use a pool of JS/Lua VMs to handle RPCs, this is to amortize their instantiation cost, reduce memory allocations and allow for higher throughput - at the cost of more memory usage (you can tweak the min/max number of vms). Because of this approach, global state should never be used from within Lua and JS. Storing all of the online players state in a global variable is also not a recommendable approach, as it can very quickly become a bottleneck at scale.

  3. To do this, you could use the Event pipeline, or alternatively (if you’d like to avoid the Go runtime) just have some logic as part of your MatchLoop that is called once the match has ended, and it stores whatever state needs to be persisted using the Storage Engine, and then broadcast a message to players from within the match (or via in-app notifications) before shutting the authoritative match VM.

Global state is generally bad practice, and it shouldn’t be relied upon - not only because of how Nakama implements VM pooling across the Lua/JS runtimes, but even in Go (which can have global state across a server instance), this won’t work in a clustered environment if the game ever needs to scale-out. Otherwise, there are no issues with your approach, and yes, there’s nothing preventing a player from joining several matches (unless single_match is set).

We consider all the runtimes to be first-class; however, given that the server is written in Go, using the Go runtime has the advantage that it’ll run more efficiently (as there’s no JS/Lua interpreter overhead), which allows you to squeeze more from the hardware. This also gives you access to all of the Go features, stdlib, and 3rd-party modules.
However, there are also some disadvantages: server and plugin dependency alignment can be a bit painful, having to compile the plugin for every code change (easy to automate, but still slower than just restarting the server), no sandboxing, and others. There should be other topics covering this matter around the forum.
I believe the community tends to lean more on JS simply because it’s the most pervasive language out there, and I don’t really have an answer for how it splits between Go vs. Lua. What I can say is that all three languages can be used successfully to build games that scale; however, customers that have stricter performance requirements tend to lean on Go more.

Hope this clarifies.

Thank you very much for your patient response. After comparing multiple frameworks, I am most satisfied with Nakama as I find it sufficiently simple yet powerful. However, I am still troubled by the following issues, which are the biggest challenges I am currently facing:

  1. My game was previously developed using a very niche framework (C + Lua). The design approach was that after all players log in, their data is converted into player objects, which are stored and managed by a player_manager (it contains a table that stores all players with their IDs as keys). The player scale is approximately 3,000 concurrent online users. Now I plan to migrate the game to Nakama for redevelopment, and I would like to know what the more recommended design pattern is? The only solution I can think of is to use a player_manager singleton to store global online players.

  2. Of course, the end result of a battle match can be broadcast to the client, but my game is server-authoritative (a traditional online game). The problem I encounter is: for example, after a battle ends, I need external modules to settle accounts based on the battle results and update the data of player objects (such as increasing experience points and granting rewards). Therefore, external modules must be informed of the battle results immediately. I am struggling with the inability of matches to communicate with other modules. The design idea I have come up with so far is as follows:

    1. When a battle starts, pass the player’s basic information into the match and send the match_id to the client. The client then joins the match to enter the battle (I have not found a way for the server to actively add the client to a match).

    2. During the battle, the client communicates directly with the match (e.g., sending round instructions/calculating round damage), and the client plays the process animations.

    3. After the battle ends, the client actively sends an RPC to the external module to notify it of the end. Then, in the RPC, check if the match exists; if it does, actively send a signal to the match to obtain the results, and then close the match.

Are there any more recommended solutions?

One additional point to supplement: does the server not have a similar method to directly invoke the client, such as an RPC registered by the client that can be called by the server? Currently, outside of matches, I send various notifications to clients via Notifications, distinguishing different types by different codes and using the subject field as the notification content. Is this the most recommended approach?

I’m glad you’re having a positive experience with Nakama :slight_smile:.

I have some followup questions:

Why is this player_manager needed - does it have some special functionality to track online players? Does it track all existing players? What happens if the server restarts?

What is an “external module” in this scenario? Can’t the business logic be exposed as simple functions that can be called from within the match handler and update some Storage Objects? Where and how is “experience” and “rewards” currently being tracked?

Because the Player object lives outside the Match context. For example, as you mentioned, if we update data inside the Match, the Match has no way to access the Player object. Additionally, we cannot pass the Player object into the Match during initialization, since the Match environment is isolated from other modules. I’m not sure whether the Player object would be passed as a reference or a duplicated copy once injected, but I assume it would most likely be a copy.

Let me propose a hypothetical workflow:

After a battle concludes, we need to add 10,000 to player.exp and 1,000 to various other member variables of the Player object (player.xxx). How should this be implemented?

To answer your question:

A PlayerManager is absolutely necessary for managing all online Player objects. For example, if Player A clicks the trade button and targets Player B, a trade RPC method will be sent with Player B’s user_id as a parameter. This RPC method will use Player B’s user_id to retrieve their Player object via the PlayerManager, then perform a series of anti-cheat validations — such as checking whether the distance between Player A and Player B is too far, and whether the trade is allowed.

For these reasons, I believe the PlayerManager is indispensable: it allows for fast, direct access to Player objects whenever needed.

My current code structure works like this: When a player logs in, the server retrieves all of the player’s data from the database and serializes it into an object. For example, the Player object contains an Items object, which in turn has multiple tables for storing items in different locations—such as the inventory, bag, warehouse, and so on. Each item is also an independent object, and new items can be created and added via the player.items.add_item() method. The Player object also includes an AttributeManager, SkillManager, PetManager, AchievementManager, and other manager classes.

Battles will affect data across multiple modules: for instance, they increase the player’s experience, the experience of pets participating in the battle, grant in-game currency and items, and trigger the recording of battle data (for the achievement system).

The reason for having so many dedicated managers is that the game’s content system is quite extensive, and this design makes maintenance and expansion simpler and more clear in my opinion. My game was built on a previous engine, and its current code base is around 100,000 lines of code with a wide range of features—it’s an MMORPG-style online game.

Of course, I’m open to any better suggestions you might have to share.

Right now, the biggest issue I’m facing is that a battle match cannot actively interact with other modules, and I’m unsure how to send the battle results and related data to other modules once a battle ends.

My current solution is: When a battle ends on the client side, it sends an RPC to the server. The server then sends a signal to the match to confirm the battle’s end, retrieves the results and data from it, and finally shuts down the match.

A global PlayerManager singleton simply won’t work in Nakama, what I’d do is rework it into a class that lazy loads the state from database whenever it doesn’t have it in memory for a given player (or players), applies all the changes to the objects in memory and then have a separate method that actually flushes all the changes to database. This allows you to chain multiple methods that apply changes in-memory but only write to database once all the changes have been applied (to coalesce changes and reduce db writes and round-trips).
You’ll need to make sure you use the Storage Engine version field on every db write, ideally with a retry mechanism that re-reads from Storage, reapplies the changes and then retries to write if a conflict surfaces, rolling back (reset the client state based on authoritative state) if all retries fail.
The class wouldn’t be kept around but instead be discarded as soon as you’ve made all the changes needed, so that if it’s reinstantiated elsewhere it loads the latest state from database before applying any changes.
You can use notifications (with persistent=false) to communicate any state changes to the client.

2.1 This is the correct flow to handle matches and joining.

2.3 Why is an RPC needed here? The client could just send a message with an opcode to signal that the match ended, and have the match logic itself handle any persisted state update plus broadcast it back to the client (or via notifications).

There is no such a thing as “client side RPCs”, using notifications is fine.