In our in-dev game, we opted to implement a rotating shop for all the reasons you have outlined and a couple others. One of those reasons was that we wished to avoid using loot boxes as our content system, which we are ethically opposed to and also because they were (and still are) in a grey area, both legally and ethically.
Although there are some thematic similarities between both (the two systems still show items randomly and expect the user to interact with them frequently and repeatedly if they do not get the items they want), we found it was easier to compensate for these issues in the rotating shop system.
From an implementation point of view, the tech required for the rotating shop can range from extremely simple to very complex, depending on how deep you want to go down this rabbit hole; for our implementation we did not use external servers to control our shop and we did not choose a 100% curated system, but we instead opted for a simpler seeded procedural-generated approach, which made the game require less maintenance on our part and which went more or less like this:
- Whenever a player joins, take the UTC timestamp returned by
os.date("!*t") and convert it into a seed unique to each player. In our case, the code was more or less like:
local today = os.date("!*t") -- get an UTC timestamp
local base_seed = today.yday + today.year * 366 -- convert it into a monotonically increasing number which changes each day
base_seed = base_seed * (user_id * 2 % PRIME_NUMBER) -- mangle it a bit so the results aren't that predictable
After this, we create a new
Random object with that seed and then always generate our items in a predictable order. This works because the only non-constant term in our seed is the day, which changes… daily!
In our shop, we opted to display eight items in total, 4 for each of our currency types, so the code first picks 4 indices from the set of all sellable soft currency items and then 4 more indices from the set of all sellable hard currency items using
We send these indices to the player alongside how much time is left until the shop rotates again, and the player looks the indices up for more details and constructs the shop UI.
Each minute or so, the server iterates through all players and does this process again, updating if necessary.
And it worked! The shop synced throughout servers and rotated consistently every 0:00 UTC.
(N.B. the same gun skins were selected twice because at the time we tested it we only had few skins available, the texture was still loading at the time of the first screenshot, UI icons were temporary, etc etc)
While our implementation is pretty basic and has some limitations, we could make it more complex and curated if we wanted, often with simple changes:
Our current method returns items based on the ordering and number of items in our item pool. This means that if either the order or the number of items changes, the shop’s daily selection may also change, and any players which were expecting the items to be there will be surprised if they are not. Since we only change that infrequently (on releases), however, in practice it does not end up being much of an issue.
If we wanted to keep the current day rotation, we could either wait until later hours to release an update, we could save the shop selection per player and load from there, or we could code in an exception which uses the old item sets until our desired release time.
It also depends on the Random state and the order on which we select the items. If we wanted to pick 4 hard currency items and then 4 soft currency items (as opposed to 4 soft and then 4 hard), the items shown would be different. Ultimately, this is a different flavor of the issue above.
This can be solved by either making different Random objects with the same seed for every step or by using a noise-based approach instead.
With our current code, we’re restricted to choosing from our entire item set, but we could straightforwardly change it to use certain pre-configured item subsets during certain days of the week, certain times of the day, on certain holidays, among others. The process would be the same, what would change is the sets we select from.
If a player already has an item and we wanted to not show it again, there is currently no code to remove this item from the item set without affecting the rest of the shop (again due to issue #1).
However, if we needed a rotating shop which “resupplies” itself as the items are bought, we could keep track of how many items have been purchased and call Random::NextInteger N more times, ignoring the redundant (and already purchased) N items at the beginning.
Currently, we cycle the shop at 0:00 UTC as a side effect of how we calculate the current day, but this could be changed easily by comparing what
os.date("!*t") returns in
date.hour and incrementing/decrementing the day depending on whether it crosses a threshold we choose.
(To us, 0:00 UTC seemed like a good time because at that point it is either evening or night in most of America and Europe and so the change occurs without much disruption.)
We opted to go for player-unique shops but we could simply just not factor the UserId in the seed code if we wanted to have an universal shop.
If we wanted to emulate item rarities, we could either select a fixed number of items from a rare items set and the rest from a common items set, or we could use other loot table selection algorithms.
While this is not exactly a curated shop, we can more or less shape it into a form with which we are satisfied.
Aside that, from an economy design perspective, it is important to keep in mind that, just like loot boxes, rotating shops limit the speed at which your users can consume your content, and that waiting for a wanted item to show up in the shop can be as frustrating (if not more, from my personal perspective as a player) as grinding for rare loot in RNG-heavy games. There are ways you can further tune these variables if you are not satisfied with them:
- Having season passes, quests, locations and/or achievements which reliably give items which are also sold in the shop is an option which lets you tie item progression to game progression while calming your players down;
- Having trading systems can disperse items faster among your playerbase if each player has a different shop;
- Showing what items the shop will sell in the next few days is also an option if you are procedurally generating your items.
All of these choices have tradeoffs and downsides (for one, showing the next items can make it so players join your game less often to check for goodies, and implementing a season pass costs time and effort), so you should think about whether you want to implement them or not in light of your game economy. In general, though, we are generally satisfied with this system and we plan on using it on future games too.