Skip to main content

Shop

Open this Example

How can I make a shop where a player can buy items or upgrades in-game?

This example shows you how to make a shop, assuming you have some source of money in your game. See the Coins example for a simple way to pick up coins in your game.

Step 1: Define prices

In our example, players can make a little aquarium and buy fish to put in it. Each fish has a different price. We can store these costs in a Map.

pub const FishCosts = {
blue = 20,
brown = 30,
green = 15,
grey = 10,
pink = 5,
red = 10,
}

Step 2: Define a ShopDialog

Now we will create a ShopDialog that will display the available fish and their prices.

pub fn dialog.ShopDialog([owner]) {
OverlayContent {
ImagePanel(frameImage=@panel_brown_damaged.png, padding=2, align=Align:Center) {
HStack(wrap=true, width=25, align=Align:Center) {
ShopChoiceButton($blue, image=@fish_blue.png)
ShopChoiceButton($brown, image=@fish_brown.png)
ShopChoiceButton($green, image=@fish_green.png)
ShopChoiceButton($grey, image=@fish_grey.png)
ShopChoiceButton($pink, image=@fish_pink.png)
ShopChoiceButton($red, image=@fish_red.png)
}
}
}
}
pub fn ShopChoiceButton(choice, image, [owner, ui]) {
let cost = FishCosts[choice]
FlatButton(PressIntent<buy>(choice), hoverColor=#cba, opacity=(Money >= cost ? 1 : 0.25)) {
VStack(align=Align:Center, gap=0) {
Image(image, height=5)
P(color=#411, stroke=false) { "$" + cost }
}
}
}
  • We have chosen to use a ImagePanel frame for our shop to give it a nice background. When you are making panels in your own game, you may want to start with just a simple Panel and style it later.

  • To give every option the same style, we have created a ShopChoiceButton function that takes the choice and image as parameters. Each option has a PressIntent that will trigger when the player clicks on it. We will handle this intent in the next step to make the player buy the item.

Step 3: Buying from the ShopDialog

When the player clicks on an option, we want to make the player buy the item, if they have enough money.

pub fn dialog.ShopDialog([owner]) {
// ...

once Pressed<buy> choice {
let cost = FishCosts[choice]
if Money >= cost {
Money -= cost
match choice {
$blue => Spawn unit { Fish(image=@fish_blue.png, radius=0.75, extent=1) },
$brown => Spawn unit { Fish(image=@fish_brown.png, radius=1.5, extent=0) },
$green => Spawn unit { Fish(image=@fish_green.png, radius=0.5, extent=1) },
$grey => Spawn unit { Fish(image=@fish_grey.png, radius=0.5, extent=1) },
$pink => Spawn unit { Fish(image=@fish_pink.png, radius=0.5, extent=1) },
$red => Spawn unit { Fish(image=@fish_red.png, radius=1, extent=0) },
}
}
Expire
}
}
  • This uses a match block to spawn the correct fish unit based on the choice the player made. In your own game, this might be where you give the player a new weapon, upgrade their stats, or unlock a new ability.

Step 4: Opening the ShopDialog

In our simple game, the player opens the shop by just clicking a button.

We will make a ShopSystem that displays a "Shop" button at the top of the screen, and opens the ShopDialog when the player clicks it.

pub prop owner.PreviousShopDialog
pub fn this.ShopSystem([owner]) {
TopContent {
RaisedButton(PressIntent<shop>, backgroundColor=Color:Primary) { "Shop" }
}

on Pressed<shop> {
PreviousShopDialog ???= Subspawn dialog {
ShopDialog
}
}
}

Now we just need to add the ShopSystem to each player:

    SpawnEachPlayer owner {
// ...

ShopSystem
}
  • The PreviousShopDialog property is used to keep track of the shop dialog that was spawned, so that we don't spawn it twice if the player is already in the shop.

  • When the player closes the shop dialog, PreviousShopDialog will still point to an entity, but it will no longer exist. The ???= operator is the "existential assignment" operator and will only execute its right-hand side if the left-hand side does not exist. This is how it opens a new shop dialog when the previous one has been closed.

  • In other games, you might trigger the shop by walking up to a shopkeeper, or by pressing a key when near a shop. Whatever method, you just need to Subspawn dialog at the right time.

Full Code Listing

pub const BoundaryHalfWidth = 16
pub const BoundaryHalfHeight = 9

pub prop owner.Money = 0

pub game fn World.Main() {
Camera(@(0, 0), radius=@(BoundaryHalfWidth, BoundaryHalfHeight))
Viewport(aspectRatio=BoundaryHalfWidth/BoundaryHalfHeight)
SolidBackground(#a6d6e6)

SpawnEachPlayer owner {
Money = 100

BottomContent {
InsetPanel {
P(bold=true) {
with Money { "You have: $" + Money }
}
}
}

ShopSystem
}
}

pub prop owner.PreviousShopDialog
pub fn this.ShopSystem([owner]) {
TopContent {
RaisedButton(PressIntent<shop>, backgroundColor=Color:Primary) { "Shop" }
}

on Pressed<shop> {
PreviousShopDialog ???= Subspawn dialog {
ShopDialog
}
}
}

pub fn dialog.ShopDialog([owner]) {
OverlayContent {
ImagePanel(frameImage=@panel_brown_damaged.png, padding=2, align=Align:Center) {
HStack(wrap=true, width=25, align=Align:Center) {
ShopChoiceButton($blue, image=@fish_blue.png)
ShopChoiceButton($brown, image=@fish_brown.png)
ShopChoiceButton($green, image=@fish_green.png)
ShopChoiceButton($grey, image=@fish_grey.png)
ShopChoiceButton($pink, image=@fish_pink.png)
ShopChoiceButton($red, image=@fish_red.png)
}
}
}

once Pressed<buy> choice {
let cost = FishCosts[choice]
if Money >= cost {
Money -= cost
match choice {
$blue => Spawn unit { Fish(image=@fish_blue.png, radius=0.75, extent=1) },
$brown => Spawn unit { Fish(image=@fish_brown.png, radius=1.5, extent=0) },
$green => Spawn unit { Fish(image=@fish_green.png, radius=0.5, extent=1) },
$grey => Spawn unit { Fish(image=@fish_grey.png, radius=0.5, extent=1) },
$pink => Spawn unit { Fish(image=@fish_pink.png, radius=0.5, extent=1) },
$red => Spawn unit { Fish(image=@fish_red.png, radius=1, extent=0) },
}
}
Expire
}
}
pub fn ShopChoiceButton(choice, image, [owner, ui]) {
let cost = FishCosts[choice]
FlatButton(PressIntent<buy>(choice), hoverColor=#cba, opacity=(Money >= cost ? 1 : 0.25)) {
VStack(align=Align:Center, gap=0) {
Image(image, height=5)
P(color=#411, stroke=false) { "$" + cost }
}
}
}

pub const FishCosts = {
blue = 20,
brown = 30,
green = 15,
grey = 10,
pink = 5,
red = 10,
}

pub prop unit.Facing = 1
pub fn unit.Fish(image, radius, extent) {
use body=this, shape=Capsule(radius=, extent=)
Body(pos=@(SignedRandom*BoundaryHalfWidth, SignedRandom*BoundaryHalfHeight), noRotation=true)
PolygonCollider(category=Category:Default)
DecaySpeed(0.1)

with Facing {
ImageSprite(image=, radius=radius+extent, bodyScale=@(Facing, 1))
}

on Tick(1s) {
let target = @(SignedRandom*BoundaryHalfWidth, SignedRandom*BoundaryHalfHeight)

loop {
let delta = target - Pos
Facing = delta.X >= 0 ? 1 : -1
if delta.Length < 1 { break }

let direction = delta.Direction
if Velocity.Dot(direction) < 2.5 {
Velocity += 0.2 * direction
}
await BeforePhysics
}
}

on Tick(10s*Random) {
repeat 3 {
ImageSpark(image=PickRandom(@bubble_*.png), radius=1, velocity=@(0, -2), splatter=1, dissipate=3s, layer=-1)
await Tick(0.25s)
}
}
}
Open in Editor