What are your thoughts and experiences regarding rotating shops?

Rotating shops in video games only sell a small sample of the purchasable items in a game and update their items at a regular interval – often daily or weekly. On Roblox, I have seen them used in RoBeats, Bad Business, and Arsenal. Outside of Roblox, a few games that use them are Fall Guys, Valorant, and Apex Legends.

Screenshots from those games

RoBeats

Bad Business

Arsenal

Fall Guys

Apex Legends

Valorant

As a developer, this style of shop appeals to me because

  1. players are incentivized to return regularly,
  2. new items immediately become exclusive after going off sale, creating FOMO,
  3. and it keeps the shop UI simple.

The only drawback that I can foresee on the development end is learning how to set up an external server that updates the items automatically with the option for curation.

However, I see few upsides to this shop model for players, which is reflected in the number of negative threads you can find by searching “apex legends store reddit” and “valorant store reddit”. The consensus among players seems to be that waiting day after day for a certain item to appear in the shop sucks. Some commenters were even frustrated that it prevented them from spending more money.

So, what do you all think of rotating shops – as players and as developers? Are there any pros or cons I haven’t considered? And if you have used a rotating shop in your game, did you find it to be more effective than a traditional shop?

4 Likes

Oversimplifying rotating shops to a tradeoff, “retention over profit”, is missing some key ideas.

By limiting what players can purchase, you might lower profits that day by preventing players from buying the items they desire.

However, it keeps them coming back to look at the shop. Every day, just to be sure they don’t miss it. And that earns you more money.
People coming back to your game explicitly to open the store is an excellent way to get them to buy other things! You’re making players get their wallets ready and look at your products! Sure, they might be annoyed that the skin they wanted isn’t up right now, but heyyy that other one looks pretty good and it’s cheaper than the one they were looking for! Then a week later the original skin comes through and they excitedly buy it and feel rewarded for their patience and vigilance paying off.

There’s a reason rotating shops are in some of the most profitable games around.

After a meteoric rise to popularity in 2018, Fortnite generated more than $5 billion in its first year for Epic Games, new financial documents reveal. (Source)

They’re excellent monetization strategies that boost your sales volume and your retention in one fell swoop.

4 Likes

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 Random:NextInteger(1, #set).

  • 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.

Screenshots


(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.

7 Likes

Thank you for the thorough reply!

Wow that is super clever. As somebody with zero server and networking knowledge, this method is quite appealing. I appreciate all of your tips for how to customize the RNG for better results.

This is something I was debating as well. @boatbomber also made the argument for why one should favor retention over profit, so I don’t think I will show the next items.

I recall reading a few angry Valorant threads about seeing items that are already owned or seeing the same items twice in a row. I think the first part is simple enough to implement: keep picking random items until the player doesn’t own one. Guaranteeing different items each day is a bit trickier, so that is probably best left to chance. /shrug

What was your reasoning behind this?

2 Likes

To be honest, If I recall correctly I think we just originally decided that on a whim haha :stuck_out_tongue:
In the past we used to have a global shop but at one point one of us asked “wait, isn’t the shop supposed to be different for each player?”, so we changed it without much thought. Later, when we were reviewing that decision, we decided to keep the change, because having different shops meant that players which were frustrated with their selection could trade with others for items they wanted.

While this could open up space for abuse using alt accounts, it is not too much of an issue because we do not allow currency trading, meaning that players end up having to spend on their alt accounts the same time and/or Robux that they would spend on their main account, so they are only able to progress in one of them at a time.

I think this slightly increases the speed at which our players will consume our content as the item diffusion is higher, but since the game is still in development I have not tested this yet (and if this turns out to be an issue we can control it by limiting which items get selected in shops).

I thought about this and a way to do it could be by only changing the seeds once every two or three days, picking N items in day 1, 2N items in day 2, and 3N items in day 3 and discarding the previous days’ items (the first N items in day 2 and the first 2N items in day 3), refilling the item pool if it gets depleted. This is a bit more complex than what we are aiming for so we will probably not do it, but it’s definitely possible (although I agree that this is tricky, yep)

3 Likes