This vignette walks through a minimal shinyphaser game where a hedgehog:
- moves with arrow keys,
- plays animations,
- collects apples (overlap),
- collides with rocks,
- avoids enemy sprites.
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/