Skip to contents

An enemy can wander until the hero gets close, show an alert, and then move toward the hero. Sprite$start_approach_on_sight() handles these reactions in the browser, so you do not need an R timer or custom JavaScript.

Swamps RPG uses this behavior for its mushroom enemies. With the current development version of shinyphaser installed, run the game from the repository root:

shiny::runApp("inst/examples/swamps_rpg")

Create the hero and an enemy

Here is a complete small app using the artwork bundled with Swamps RPG. Save it as app.R in a Shiny app directory. Move with the arrow keys and approach the mushroom enemy. It shows a yellow ! when it notices the hero.

library(shiny)
library(shinyphaser)

addResourcePath("approach-assets", system.file(
  "examples", "swamps_rpg", "www", package = "shinyphaser"
))

game <- PhaserGame$new(width = 960, height = 600)
ui <- tagList(
  tags$p("Move with the arrow keys. Get close to the mushroom, then move away."),
  game$use_phaser()
)

server <- function(input, output, session) {
  game <- PhaserGame$new(width = 960, height = 600)
  game$set_shiny_session(session)
  game$set_world_bounds(960, 600)

  hero <- game$add_sprite(
    "hero", "approach-assets/sprites/hero/orc/hero_orc_idle.png",
    x = 120, y = 300, frame_width = 100, frame_height = 100,
    frame_count = 29, frame_rate = 4
  )
  enemy <- game$add_sprite(
    "mushroom", "approach-assets/sprites/enemies/mushroom_man/mushroom_man_idle.png",
    x = 700, y = 300, frame_width = 100, frame_height = 100,
    frame_count = 8, frame_rate = 8
  )

  for (direction in c("left", "right", "up", "down")) {
    hero$add_animation(
      paste0("move_", direction),
      sprintf("approach-assets/sprites/hero/orc/hero_orc_move_%s.png", direction),
      frame_width = 100, frame_height = 100, frame_count = 6, frame_rate = 8
    )
    enemy$add_animation(
      paste0("move_", direction),
      sprintf("approach-assets/sprites/enemies/mushroom_man/mushroom_man_walk_%s.png", direction),
      frame_width = 100, frame_height = 100, frame_count = 2, frame_rate = 8
    )
  }
  hero$add_player_controls(speed = 220)

  status <- game$add_text("Approach the mushroom", "status", 24, 24)
  status$set_scroll_factor(0)
  status$set_depth(10000)
  game$add_overlap("hero", "mushroom", browser_action = browser_actions({
    status$set("Contact! Move away to escape.")
  }))

  enemy$start_approach_on_sight(
    target_name = "hero", sight_range = 300, speed = 100, distance = 60,
    check_interval = 250, alert_duration = 1200, wander_interval = 1500
  )
}

shinyApp(ui, server)

The target is identified by the sprite name, "hero", rather than the R variable holding it. Give each enemy a unique name if you create several. Directional animations use the suffixes move_left, move_right, move_up, and move_down. The runtime selects an available movement animation, falling back to move or idle when directional animations are absent.

Configure the approach

Call the method once during enemy setup, after binding the game to its Shiny session. Its sight checks wait until both sprites exist and are active.

Argument Meaning
target_name Name passed to add_sprite() for the target
sight_range Detection radius in pixels, measured between sprite positions
speed Movement speed in pixels per second, for approach and wandering
distance Maximum length of each movement step in pixels
check_interval Milliseconds between detection checks
alert_duration Milliseconds over which the yellow ! fades
wander_interval Minimum milliseconds between attempts to wander while out of range

Use positive values for distances, speed, and timing. A smaller distance produces shorter steps, allowing the enemy to adjust direction more often. A lower check_interval makes detection more responsive, but does not restart an approach step that is already running. Calling the method again for the same enemy replaces its sight-check timer with the new settings.

Understand the behavior

  • Outside the radius: the enemy periodically chooses a random horizontal or vertical direction and wanders using the configured speed and step size.
  • Entering the radius: the enemy interrupts its wandering movement and displays a yellow ! above its head.
  • While approaching: the enemy moves toward the hero in steps, choosing a new direction between steps. A step is capped at the current distance to the hero. Forced animations, such as an attack or damage reaction, postpone new approach steps until they finish.
  • Leaving the radius: the enemy returns to wandering. Re-entering the radius can trigger another alert.

Here, “sight” means a distance check. It does not check the enemy’s facing direction or test whether a wall or tree hides the hero. Movement is a direct approach, not pathfinding around obstacles. Configure terrain and collision separately, and do not assume that this method will find a route through a maze.

Keep contact and combat separate

Approaching does not automatically damage the hero or end the game. The small app above uses add_overlap() to show a contact message. In a combat game, a separate server action can update health, with a throttled sustained overlap:

# Inside your server, after creating the sprites:
life <- reactiveVal(100)
game$add_overlap(
  "hero", "mushroom", input = input,
  mode = "stay", interval = 1000,
  server_action = function(event) {
    life(max(0, life() - 5))
  }
)

This subtracts five health at most once per second while the sprites overlap. Connect life() to your own health display. See Choose between browser and server actions for more on splitting immediate feedback from R-side game rules.

To combine enemies with trees and bushes, see Walk behind scenery and block its footprint. set_depth_by_y() controls their drawing order; it does not change detection range or add pathfinding.