SkillAgentSearch skills...

Ggpacman

A `ggplot2` and `gganimate` Version of Pac-Man

Install / Use

/learn @mcanouil/Ggpacman
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<!-- README.md is generated from README.Rmd. Please edit that file -->

A ggplot2 and gganimate Version of Pac-Man <img src="man/figures/ggpacman.gif" align="right" width="120" />

<!-- badges: start -->

Lifecycle:
maturing GitHub
tag R build
status CRAN
status

<!-- badges: end -->

The goal of ggpacman is to …
Make a GIF of the game Pac-Man (not to develop an R version of Pac-Man …).

Installation

# Install ggpacman from CRAN:
install.packages("ggpacman")

# Or the the development version from GitHub:
# install.packages("remotes")
remotes::install_github("mcanouil/ggpacman")

Pac-Man in action

library(ggpacman)
animate_pacman(
  pacman = pacman,
  ghosts = list(blinky, pinky, inky, clyde),
  font_family = "xkcd"
)

<!-- -->

The Story of ggpacman

It started on a Saturday evening …

It was the 21<sup>st</sup> of March (for the sake of precision), around 10 pm CET (also for the sake of precision and mostly because it is not relevant). I was playing around with my data on ‘all’ the movies I have seen so far (mcanouil/IMDbRating) and looking on possibly new ideas of visualisation on twitter using #ggplot2 and #gganimate (by the way the first time I played with gganimate was at useR-2018 (Brisbane, Australia), just before and when @thomasp85 released the actual framework). The only thing on the feed was “contaminated/deaths and covid-19” curves made with ggplot2 and a few with gganimate … Let’s say, it was not as funny and interesting as I was hoping for … Then, I’ve got an idea, what if I can do something funny and not expected with ggplot2 and gganimate? My first thought, was let’s draw and animate Pac-Man, that should not be that hard!

Well, it was not that easy after-all … But, I am going to go through my code here (you might be interested to actually look at the commits history.

<blockquote class="twitter-tweet"> <p lang="en" dir="ltr"> Maybe I went too far with <a href="https://twitter.com/hashtag/ggplot2?src=hash&amp;ref_src=twsrc%5Etfw">\#ggplot2</a> and <a href="https://twitter.com/hashtag/gganimate?src=hash&amp;ref_src=twsrc%5Etfw">\#gganimate</a> …😅<br>What do you think <a href="https://twitter.com/hadleywickham?ref_src=twsrc%5Etfw">@hadleywickham</a> & <a href="https://twitter.com/thomasp85?ref_src=twsrc%5Etfw">@thomasp85</a> , did I go too far or not enough ? (I am planning to add the ghosts 😎) <a href="https://t.co/nkfbti1Etd">pic.twitter.com/nkfbti1Etd</a> </p> — Mickaël CANOUIL (@mickaelcanouil) <a href="https://twitter.com/mickaelcanouil/status/1241760925499170824?ref_src=twsrc%5Etfw">March 22, 2020</a> </blockquote>
  1. The packages
  2. The maze layer
    1. The base layer
    2. The grid layer
    3. The bonus points layer
  3. Pac-Man character
  4. The Ghosts characters
    1. Body
    2. Eyes
    3. Ghost shape
  5. How Pac-Man interacts with the maze?
    1. Bonus points
    2. Ghost "weak" and "eaten" states
  6. Plot time

The packages

library("stats")
library("utils")
library("rlang")
library("magrittr")
library("dplyr")
library("tidyr")
library("purrr")
library("ggplot2")
library("ggforce")
library("gganimate")
library("ggtext")

The maze layer

The base layer

First thing first, I needed to set-up the base layer, meaning, the maze from Pac-Man. I did start by setting the coordinates of the maze.

base_layer <- ggplot() +
  theme_void() +
  theme(
    legend.position = "none",
    plot.background = element_rect(fill = "black", colour = "black"),
    panel.background = element_rect(fill = "black", colour = "black"),
  ) +
  coord_fixed(xlim = c(0, 20), ylim = c(0, 26))

For later use, I defined some scales (actually those scales, where defined way after chronologically speaking). I am using those to define sizes and colours for all the geometries I am going to use to achieve the Pac-Man GIF.

map_colours <- c(
  "READY!" = "goldenrod1",
  "wall" = "dodgerblue3", "door" = "dodgerblue3",
  "normal" = "goldenrod1", "big" = "goldenrod1", "eaten" = "black",
  "Pac-Man" = "yellow",
  "eye" = "white", "iris" = "black",
  "Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
  "Pinky" = "pink", "Pinky_weak" = "blue", "Pinky_eaten" = "transparent",
  "Inky" = "cyan", "Inky_weak" = "blue", "Inky_eaten" = "transparent",
  "Clyde" = "orange", "Clyde_weak" = "blue", "Clyde_eaten" = "transparent"
)
base_layer <- base_layer +
  scale_size_manual(values = c("wall" = 2.5, "door" = 1, "big" = 2.5, "normal" = 0.5, "eaten" = 3)) +
  scale_fill_manual(breaks = names(map_colours), values = map_colours) +
  scale_colour_manual(breaks = names(map_colours), values = map_colours)

<!-- -->

My base_layer here is not really helpful, so I temporarily added some elements to help me draw everything on it. Note: I won’t use it in the following.

base_layer +
  scale_x_continuous(breaks = 0:21, sec.axis = dup_axis()) +
  scale_y_continuous(breaks = 0:26, sec.axis = dup_axis()) +
  theme(
    panel.grid.major = element_line(colour = "white"),
    axis.text = element_text(colour = "white")
  ) +
  annotate("rect", xmin = 0, xmax = 21, ymin = 0, ymax = 26, fill = NA)

<!-- -->

Quite better, isn’t it?!

The grid layer

Here, I am calling “grid”, the walls of the maze. For this grid, I started drawing the vertical lines on the left side of the maze (as you may have noticed, the first level is symmetrical).

left_vertical_segments <- tribble(
  ~x, ~y, ~xend, ~yend,
  0, 0, 0, 9,
  0, 17, 0, 26,
  2, 4, 2, 5,
  2, 19, 2, 20,
  2, 22, 2, 24,
  4, 4, 4, 7,
  4, 9, 4, 12,
  4, 14, 4, 17,
  4, 19, 4, 20,
  4, 22, 4, 24,
  6, 2, 6, 5,
  6, 9, 6, 12,
  6, 14, 6, 20,
  6, 22, 6, 24,
  8, 4, 8, 5,
  8, 9, 8, 10,
  8, 12, 8, 15,
  8, 19, 8, 20,
  8, 22, 8, 24
)
base_layer +
  geom_segment(
    data = left_vertical_segments,
    mapping = aes(x = x, y = y, xend = xend, yend = yend),
    lineend = "round",
    inherit.aes = FALSE,
    colour = "white"
  )

<!-- -->

Then, I added the horizontal lines (still only on the left side of the maze)!

left_horizontal_segments <- tribble(
  ~x, ~y, ~xend, ~yend,
  0, 0, 10, 0,
  2, 2, 8, 2,
  0, 4, 2, 4,
  8, 4, 10, 4,
  0, 5, 2, 5,
  8, 5, 10, 5,
  2, 7, 4, 7,
  6, 7, 8, 7,
  0, 9, 4, 9,
  8, 9, 10, 9,
  8, 10, 10, 10,
  0, 12, 4, 12,
  8, 12, 10, 12,
  0, 14, 4, 14,
  8, 15, 9, 15,
  0, 17, 4, 17,
  6, 17, 8, 17,
  2, 19, 4, 19,
  8, 19, 10, 19,
  2, 20, 4, 20,
  8, 20, 10, 20,
  2, 22, 4, 22,
  6, 22, 8, 22,
  2, 24, 4, 24,
  6, 24, 8, 24,
  0, 26, 10, 26
)

left_segments <- bind_rows(left_vertical_segments, left_horizontal_segments)
base_layer +
  geom_segment(
    data = left_segments,
    mapping = aes(x = x, y = y, xend = xend, yend = yend),
    lineend = "round",
    inherit.aes = FALSE,
    colour = "white"
  )

<!-- -->

The maze is slowly appearing, but surely. As I wrote earlier, the first level is symmetrical, so I used my left lines left_segments to compute all the lines on the right right_segments.

right_segments <-  mutate(
  .data = left_segments,
  x = abs(x - 20),
  xend = abs(xend - 20)
)
base_layer +
  geom_segment(
    data = bind_rows(left_segments, right_segments),
    mapping = aes(x = x, y = y, xend = xend, yend = yend),
    lineend = "round",
    inherit.aes = FALSE,
    colour = "white"
  )

<!-- -->

The middle vertical lines were missing, i.e., I did not want to plot them twice, which would have happen, if I added these in left_segments. Also, the “door” of the ghost spawn area is missing. I added the door and the missing vertical walls in the end.

centre_vertical_segments <- tribble(
  ~x, ~y, ~xend, ~yend,
  10, 2, 10, 4,
  10, 7, 10, 9,
  10, 17, 10, 19,
  10, 22, 10, 26
)
door_segment <- tibble(x = 9, y = 15, xend = 11, yend = 15, type = "door")

Finally, I combined all the segments and drew them all.

maze_walls <- bind_rows(
  left_segments,
  centre_vertical_segments,
  right_segments
) %>%
  mutate(type = "wall") %>%
  bind_rows(door_segment)
base_layer +
  geom_segment(
    data = maze_walls,
    mapping = aes(x = x, y = y, xend = xend, yend = yend),
    lineend = "round",
    inherit.aes = FALSE,
    colour = "white"
  )

<!-- -->

The maze is now complete, but no-one can actually see the door, since it appears the same way as the walls. You may have noticed, I added a column named type. type can currently hold two values: "wall" and "door". I am going to use type as values for two aesthetics, you may already have guessed which ones. The

View on GitHub
GitHub Stars61
CategoryDevelopment
Updated3mo ago
Forks2

Languages

R

Security Score

82/100

Audited on Dec 6, 2025

No findings