Build your first shinyphaser game

This vignette walks through a minimal shinyphaser game where a hedgehog:

1) Basic app structure

A shinyphaser game lives inside a regular Shiny app.

In UI we need to load Phaser.js dependencies and we do it with calling use_phaser() method.

set_shiny_session() method is a helper to set Shiny session inside R6 object private environment as it will be reused by shinyphaser methods many times.

library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()
}

shinyApp(ui, server)

2) Add first image (background)

First we add simply image which will serve as a background to our game. We define x and y to place the image at a specific point on the canvas (in pixels): x is horizontal position and y is vertical position. For add_image(), this point is the image center, so x = 800, y = 300 places the terrain around the middle area of the scene.

floor <- game$add_image(
  name = "floor",
  url = "assets/hedgehog/terrain/grass.png",
  x = 800,
  y = 300
)
Show full code
library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )
}

shinyApp(ui, server)

3) Add sprite (the player hedgehog)

Create a player sprite from a sprite sheet. The frame parameters describe how to cut and play that spritesheet: frame_width and frame_height are the size of a single frame (32x32 px), frame_count = 5 tells shinyphaser how many frames to use from the sheet, and frame_rate = 6 sets animation playback speed to 6 frames per second.

hedgehog <- game$add_sprite(
  name = "hedgehog",
  url = "assets/hedgehog/sprites/hedgehog_32.png",
  x = 140,
  y = 260,
  frame_width = 32,
  frame_height = 32,
  frame_count = 5,
  frame_rate = 6
)
Show full code
library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )

  hedgehog <- game$add_sprite(
    name = "hedgehog",
    url = "assets/hedgehog/sprites/hedgehog_32.png",
    x = 140,
    y = 260,
    frame_width = 32,
    frame_height = 32,
    frame_count = 5,
    frame_rate = 6
  )
}

shinyApp(ui, server)

4) Add player controls

Attach keyboard movement to the sprite. add_player_controls() binds keyboard input to sprite velocity. Here, directions = c("left", "right", "up", "down") enables full 4-direction movement with arrow/WASD-style input, while speed = 220 sets how fast the hedgehog travels (pixels per second). In practice, increase speed for a more arcade-like feel, or decrease it for more precise movement when navigating around obstacles.

hedgehog$add_player_controls(
  directions = c("left", "right", "up", "down"),
  speed = 220
)
Show full code
library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )

  hedgehog <- game$add_sprite(
    name = "hedgehog",
    url = "assets/hedgehog/sprites/hedgehog_32.png",
    x = 140,
    y = 260,
    frame_width = 32,
    frame_height = 32,
    frame_count = 5,
    frame_rate = 6
  )

  hedgehog$add_player_controls(
    directions = c("left", "right", "up", "down"),
    speed = 220
  )
}

shinyApp(ui, server)

5) Add move animations

We would like now to apply animation to our hedgehog when he moves.

Add directional animations and play one as default. The idea is to register one animation per movement direction, then let the player-controls system switch between them automatically while the sprite is moving. We create a vector of direction names (move_left, move_right, move_up, move_down) and loop over it, calling add_animation() each time. The suffix links animation names to direction-specific files, url points to the correct spritesheet, and frame_width/frame_height/frame_rate describe how to read and play each animation strip.

moves <- c("move_left", "move_right", "move_up", "move_down")

for (move in moves) {
  hedgehog$add_animation(
    suffix = move,
    url = paste0("assets/hedgehog/sprites/hedgehog_", move, "_32.png"),
    frame_width = 32,
    frame_height = 32,
    frame_rate = 5
  )
}
Show full code
library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )

  hedgehog <- game$add_sprite(
    name = "hedgehog",
    url = "assets/hedgehog/sprites/hedgehog_32.png",
    x = 140,
    y = 260,
    frame_width = 32,
    frame_height = 32,
    frame_count = 5,
    frame_rate = 6
  )

  hedgehog$add_player_controls(
    directions = c("left", "right", "up", "down"),
    speed = 220
  )

  moves <- c("move_left", "move_right", "move_up", "move_down")

  for (move in moves) {
    hedgehog$add_animation(
      suffix = move,
      url = paste0("assets/hedgehog/sprites/hedgehog_", move, "_32.png"),
      frame_width = 32,
      frame_height = 32,
      frame_rate = 5
    )
  }
}

shinyApp(ui, server)

6) Add overlap with other objects (collect apples)

Use overlap when two objects can share space and trigger events. In this step, apples act like collectibles: the hedgehog can move through them, and each touch fires a callback instead of blocking movement. We first create an apples static group and place a few apples on the map, then register add_overlap() between "hedgehog" and "apples". Inside callback_fun, we hide the collected apple (apples$disable(evt)), increment score, and refresh on-screen text. In short: hedgehog meets apple, apple disappears, score goes up — because every hedgehog knows apples are the true quest objective.

score <- reactiveVal(0)
apples <- game$add_static_group("apples", "assets/hedgehog/perks/apple_20.png")

apples$create(260, 140)
apples$create(640, 180)
apples$create(730, 390)

score_text <- game$add_text(text = "Score: 0", id = "score", x = 20, y = 20)

game$add_overlap(
  object_one = "hedgehog",
  group = "apples",
  callback_fun = function(evt) {
    apples$disable(evt)        # hide collected apple
    score(score() + 1)
    score_text$set(paste("Score:", score()))
  },
  input = input
)
Show full code
library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )

  hedgehog <- game$add_sprite(
    name = "hedgehog",
    url = "assets/hedgehog/sprites/hedgehog_32.png",
    x = 140,
    y = 260,
    frame_width = 32,
    frame_height = 32,
    frame_count = 5,
    frame_rate = 6
  )

  hedgehog$add_player_controls(
    directions = c("left", "right", "up", "down"),
    speed = 220
  )

  moves <- c("move_left", "move_right", "move_up", "move_down")

  for (move in moves) {
    hedgehog$add_animation(
      suffix = move,
      url = paste0("assets/hedgehog/sprites/hedgehog_", move, "_32.png"),
      frame_width = 32,
      frame_height = 32,
      frame_rate = 5
    )
  }

  score <- reactiveVal(0)
  apples <- game$add_static_group("apples", "assets/hedgehog/perks/apple_20.png")

  apples$create(260, 140)
  apples$create(640, 180)
  apples$create(730, 390)

  score_text <- game$add_text(text = "Score: 0", id = "score", x = 20, y = 20)

  game$add_overlap(
    object_one = "hedgehog",
    group = "apples",
    callback_fun = function(evt) {
      apples$disable(evt)        # hide collected apple
      score(score() + 1)
      score_text$set(paste("Score:", score()))
    },
    input = input
  )
}
shinyApp(ui, server)

7) Add collision with other objects (rocks)

Use collision when objects should block each other. Unlike overlap (collect-and-pass-through), collision adds physical blocking. We enable terrain collision for the hedgehog, create a rocks static group, and then attach add_collider() so the player cannot walk through rocks. This gives the level real navigation constraints: apples are pickups, rocks are barriers.

game$enable_terrain_collision("hedgehog")

rocks <- game$add_static_group(
  name = "rocks",
  url = "assets/hedgehog/obstacles/rock.png"
)

rocks$create(
  x = 400,
  y = 400
)
rocks$create(
  x = 600,
  y = 500
)
game$add_collider(
  object_one = "hedgehog",
  group = "rocks"
)
Show full code
library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )

  hedgehog <- game$add_sprite(
    name = "hedgehog",
    url = "assets/hedgehog/sprites/hedgehog_32.png",
    x = 140,
    y = 260,
    frame_width = 32,
    frame_height = 32,
    frame_count = 5,
    frame_rate = 6
  )

  hedgehog$add_player_controls(
    directions = c("left", "right", "up", "down"),
    speed = 220
  )

  game$enable_terrain_collision("hedgehog")

  moves <- c("move_left", "move_right", "move_up", "move_down")

  for (move in moves) {
    hedgehog$add_animation(
      suffix = move,
      url = paste0("assets/hedgehog/sprites/hedgehog_", move, "_32.png"),
      frame_width = 32,
      frame_height = 32,
      frame_rate = 5
    )
  }

  score <- reactiveVal(0)
  apples <- game$add_static_group("apples", "assets/hedgehog/perks/apple_20.png")

  apples$create(260, 140)
  apples$create(640, 180)
  apples$create(730, 390)

  rocks <- game$add_static_group(
    name = "rocks",
    url = "assets/hedgehog/obstacles/rock.png"
  )

  rocks$create(400, 200)
  rocks$create(200, 300)

  score_text <- game$add_text(text = "Score: 0", id = "score", x = 20, y = 20)

  game$add_overlap(
    object_one = "hedgehog",
    group = "apples",
    callback_fun = function(evt) {
      apples$disable(evt)        # hide collected apple
      score(score() + 1)
      score_text$set(paste("Score:", score()))
    },
    input = input
  )

  game$add_collider(
    object_one = "hedgehog",
    group = "rocks"
  )
}
shinyApp(ui, server)

8) Add enemy sprites

Create one or more enemies and move them. If enemy overlaps the player, end game. Here we add a simple enemy loop: create a badger sprite, define hedgehog-badger overlap as a “game over” event, and periodically change enemy direction with set_in_motion(). The invalidateLater(700, session) timer makes the badger pick a new direction about every 0.7 seconds, which creates a lightweight patrol/chase feeling without writing a full AI system.

enemy <- game$add_sprite(
  name = "badger",
  url = "assets/hedgehog/sprites/badger_move_left_50.png",
  x = 700,
  y = 300,
  frame_width = 50,
  frame_height = 50,
  frame_count = 1,
  frame_rate = 1
)

game$add_overlap(
  object_one = "hedgehog",
  object_two = "badger",
  callback_fun = function(evt) {
    shinyalert::shinyalert(
      title = "Game over", type = "error",
      closeOnClickOutside = FALSE, showCancelButton = FALSE,
      callbackR = function(value) shiny::stopApp()
    )
  },
  input = input
)

shiny::observe({
  shiny::invalidateLater(700, session)
  dir <- sample(list(c(-1, 0), c(1, 0), c(0, -1), c(0, 1)), 1)[[1]]
  enemy$set_in_motion(
    dir_x = dir[1],
    dir_y = dir[2],
    speed = 70,
    distance = 150,
    lag = 0
  )
})

9) Full minimal app

Put all pieces together:

library(shiny)
library(shinyphaser)

game <- PhaserGame$new(width = 1500, height = 800)

ui <- tagList(
  game$use_phaser()
)

server <- function(input, output, session) {
  game$set_shiny_session()

  floor <- game$add_image(
    name = "floor",
    url = "assets/hedgehog/terrain/grass.png",
    x = 800,
    y = 300
  )

  hedgehog <- game$add_sprite(
    name = "hedgehog",
    url = "assets/hedgehog/sprites/hedgehog_32.png",
    x = 140,
    y = 260,
    frame_width = 32,
    frame_height = 32,
    frame_count = 5,
    frame_rate = 6
  )

  hedgehog$add_player_controls(
    directions = c("left", "right", "up", "down"),
    speed = 220
  )

  game$enable_terrain_collision("hedgehog")

  moves <- c("move_left", "move_right", "move_up", "move_down")

  for (move in moves) {
    hedgehog$add_animation(
      suffix = move,
      url = paste0("assets/hedgehog/sprites/hedgehog_", move, "_32.png"),
      frame_width = 32,
      frame_height = 32,
      frame_rate = 5
    )
  }

  score <- reactiveVal(0)
  apples <- game$add_static_group("apples", "assets/hedgehog/perks/apple_20.png")

  apples$create(260, 140)
  apples$create(640, 180)
  apples$create(730, 390)

  rocks <- game$add_static_group(
    name = "rocks",
    url = "assets/hedgehog/obstacles/rock.png"
  )

  rocks$create(400, 200)
  rocks$create(200, 300)

  score_text <- game$add_text(text = "Score: 0", id = "score", x = 20, y = 20)

  game$add_overlap(
    object_one = "hedgehog",
    group = "apples",
    callback_fun = function(evt) {
      apples$disable(evt)
      score(score() + 1)
      score_text$set(paste("Score:", score()))
    },
    input = input
  )

  game$add_collider(
    object_one = "hedgehog",
    group = "rocks"
  )

  enemy <- game$add_sprite(
    name = "badger",
    url = "assets/hedgehog/sprites/badger_move_left_50.png",
    x = 700,
    y = 300,
    frame_width = 50,
    frame_height = 50,
    frame_count = 1,
    frame_rate = 1
  )

  game$add_overlap(
    object_one = "hedgehog",
    object_two = "badger",
    callback_fun = function(evt) {
      shinyalert::shinyalert(
        title = "Game over", type = "error",
        closeOnClickOutside = FALSE, showCancelButton = FALSE,
        callbackR = function(value) shiny::stopApp()
      )
    },
    input = input
  )

  shiny::observe({
    shiny::invalidateLater(700, session)
    dir <- sample(list(c(-1, 0), c(1, 0), c(0, -1), c(0, 1)), 1)[[1]]
    enemy$set_in_motion(
      dir_x = dir[1],
      dir_y = dir[2],
      speed = 70,
      distance = 150,
      lag = 0
    )
  })
}
shinyApp(ui, server)

10) Deployment

For now, you can deploy shinyphaser apps the same way as standard Shiny apps. For example, you can publish on shinyapps.io using rsconnect::deployApp() from your app directory (or the Publish button in RStudio).

See the official shinyapps.io deployment guide: https://docs.posit.co/shinyapps.io/guide/getting_started/