--- title: "Build your first shinyphaser game" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Build your first shinyphaser game} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` 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. ```{r eval=FALSE} 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. ```{r eval=FALSE} floor <- game$add_image( name = "floor", url = "assets/hedgehog/terrain/grass.png", x = 800, y = 300 ) ```
Show full code ```{r eval=FALSE} 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) ```
```{r, echo=FALSE} knitr::include_graphics("assets/first_game_2_add_background.png") ``` ## 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. ```{r eval=FALSE} 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 ) ``` ```{r, echo=FALSE} knitr::include_graphics("assets/hedgehog_32.png") ```
Show full code ```{r eval=FALSE} 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) ```
```{r, echo=FALSE} knitr::include_graphics("assets/first_game_3_add_sprite.gif") ``` ## 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. ```{r eval=FALSE} hedgehog$add_player_controls( directions = c("left", "right", "up", "down"), speed = 220 ) ```
Show full code ```{r eval=FALSE} 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) ```
```{r, echo=FALSE} knitr::include_graphics("assets/first_game_4_player_controls.gif") ``` ## 5) Add move animations We would like now to apply animation to our hedgehog when he moves. ```{r, echo=FALSE} knitr::include_graphics("assets/hedgehog_move_right_32.png") knitr::include_graphics("assets/hedgehog_move_left_32.png") ``` 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. ```{r eval=FALSE} 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 ```{r eval=FALSE} 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) ```
```{r, echo=FALSE} knitr::include_graphics("assets/first_game_5_move_animation.gif") ``` ## 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. ```{r eval=FALSE} 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 ```{r eval=FALSE} 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) ```
```{r, echo=FALSE} knitr::include_graphics("assets/first_game_6_overlap.gif") ``` ## 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. ```{r eval=FALSE} 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 ) ``` ```{r eval=FALSE} game$add_collider( object_one = "hedgehog", group = "rocks" ) ```
Show full code ```{r eval=FALSE} 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) ```
```{r, echo=FALSE} knitr::include_graphics("assets/first_game_7_collide.gif") ``` ## 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. ```{r eval=FALSE} 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 ) }) ``` ```{r, echo=FALSE} knitr::include_graphics("assets/first_game_8_full_example.gif") ``` ## 9) Full minimal app Put all pieces together: ```{r eval=FALSE} 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: