And thanks for all the fish! 🐟
In this project, we'll code a simple 2D game to get familiar with the mlx library and UI in general. In my case, it will be Pac-Man.
The game we choose must follow a set of rules. The executable so_long
will receive a map as the only argument, and this map will have a .ber
filetype.
The file also must follow these rules:
- Only
P
(pacman),1
(wall),0
(empty),C
(collectible), andE
(exit) will be accepted characters in our map (except if you add enemies as bonus) - The map must be rectangular, i.e. all rows must have the same length
- There must be at least one exit, one player, and one collectible on the map
- The map must be closed, i.e. surrounded by walls
If any of these checks fail, the game must end with Error\n
followed by a custom message.
The goal is for the player(s) to collect all the collectibles on the map before going to an exit in the least possible number of steps
For the graphics part of the project we used a library called minilibx
. It's fairly basic and somewhat limited, but was still fun to use in the project.
In this part of the code I checked that the given map was properly opened, that it had a .ber
filetype, and then continued by reading the file one line at a time with get_next_line.
Once that was done I filled a struct t_lay
with some basic map attributes like number of players, exits, collectibles, number of rows and columns, etc. During the reading process I also created a string containing the entire map, whcih was later useful when creating a matrix with ft_split
with \n
as the separator.
For this part I took the t_lay
and my map matrix to build my main t_game
struct. This struct includes more detailed information about my game state, such a list of pacmans, a list of ghosts, the number of loop repetitions (frames), and other info. From here I initialized a window with the mlx function and started drawing the static elements of the map (walls, spaces and collectibles) on the window. The mlx library, as other graphic libraries, has an infinite loop where the game events happen. From here I check the state of the game to re-print certain elements of the map or to manage special events, such as when Pacman dies or when ghosts are in panic mode.
The mlx
library has hooks, which essentially link certain events on the computer (keypresses, mouse clicks, etc) with functions we implement. In my case I 'hooked' the end of the game function to pressing the x
button to close the window. Also, I used a key hook to link keypresses with pacman's movements.
When the ESC
/Q
key is pressed, the game ends. If the arrow keys or the W
. A
, S
, D
keys are pressed, every pacman on the map changes its direction and tries to move in that direction. Also, every so often all pacmans will all try to move in the same direction they're headed. This way the arrow/WASD keys just change pacmans' directions to make them move forward till they hit a wall.
Ghosts behave similarly (they're the first bonus: enemies), but instead of responding the keypress, they use a basic algorithm to find the closest pacman and try to catch it. Whenever a pacman is caught by a ghost, the attribute pac_dying
is set to 1 in the t_game
struct and all pacmans die with a short animation.
Ghosts load in seven different colors, and each new ghost will have a different color. To do this, I had to load every sprite of every color with the mlx library and assign a new color to every new ghost with ghost_number % number_of_colors
, thus rotating through the list of colors when there are more ghosts than available colors.
Lastly, if a pacman reaches an exit and there are no collectibles left, it will be deleted from the list of players. If all pacmans reach the exit, the game ends and the final score is displayed.
There are several animations throughout the game (it's the other bonus in the project), from pacman's gobbling animation or ghosts' panic mode to pacman's death animation. They are all animated in a similar way: with linked lists. Every node in the list corresponds to an image of the animation. Every so often a function is called which will move to the next image, and if the last one is found, the node resets to the top of the linked list.
Pacman and ghosts move on the map matrix, which is much smaller than the mlx window. For this reason and to make movement smooth it is interesting to keep a position in the matrix (pos
) and a position on the window (win_pos
) for every pacman/ghost, and update the position on the window one pixel at a time until pos * sprite_size == win_pos
.
The pacman game I designed has a few extras we weren't asked to implement, but I thought would make the overall game experience better. Here's a list of the most relevant additions:
- Add custom (simple) chasing algorithm using euclidean distances.
- Ability to restart the game by pressing the
R
key - Ability to exit the game by pressing the
Q
key - Ghosts enter panic mode when there's less than a third of the pacdots remaining. During this time the players will be faster and ghosts will flash for the sake of distraction
- The score is displayed in the classic pacman fonts, instead of the (ugly) builtin fonts from
mlx_string_put()
- Arrow keys also move the player
- Add decorating pacman logo centered at the bottom
- Add support for both
linux
andMacOS
- Implement several players in a single map
- Ability to save a direction for when it's available
- Added 30 test maps (most of them playable)
- Restrict ending or resetting the game while pacman is dying
There are a total of 30 maps available. See MAPS.md
Here are a few sample gameplays from my favorite maps:
There are 20+ maps (ported from Machine-Learning-Pacman) to try your skill/luck.
See Other Maps
git clone https://gitlab.com/madebypixel02/so_long.git
cd so_long
make
Linux
If you're not using a MacOS computer from 42, you'll need to install the libraries manually. Please refer to the official github for more details. To install it, do the following (requires root access):
git clone https://github.com/42Paris/minilibx-linux.git
cd minilibx-linux/
make
sudo cp mlx.h /usr/include
sudo cp libmlx.a /usr/lib
MacOS
To install the library, you will need to first install a package manager like homebrew (check here) to then install the X11 package with brew install Xquartz
. After that you must extract the minilibx file called minilibx_opengl.tgz
. Then install it to your system with the following commands (requires sudo as well):
cd minilibx_opengl
make
sudo cp mlx.h /usr/local/include
sudo cp libmlx.a /usr/local/lib
sudo reboot
Note: A reboot is necessary to ensure that the Xquartz
is working properly. You can test if it is by running a test example with the command xeyes
.
If you want quick access to the mlx manuals, it is recommended that you copy the files from the man
folder in minilibx-linux to your system manuals:
Linux
sudo cp man/man3/* /usr/share/man/man3/
Note: Depending on your Linux configuration, to get the manuals working (e.g. man mlx
) you will need to individually gzip all the manual files you just copied, e.g. sudo gzip /usr/share/man/man3/mlx.3
.
MacOS
sudo cp man/man3/* /usr/X11/share/man/man3
make compiles so_long executable
make test MAP={path_to_map} compiles and executes so_long with the specified map
make play compiles and executes a small set of maps sequentially
make play2 compiles and executes a much larger set of maps sequentially
make git adds and commits everything, then pushes to upstream branch
make norminette runs norminette for all files in the project that need to pass it
Note: we were not allowed to use multiple threads, thus it is pretty hard to time the speeds of the game. I found that using valgrind on linux
helps slow the game down so it is more similar to the performance in MacOS. Depending on your computer's performance the speed of the game may vary. I hope to learn ways to improve that for future projects. For linux
, try always using valgrind as follows: valgrind ./so_long <map.ber>
Throughout the project, there have been a few roadblocks that needed to be worked around, here's some of them:
- Timing: getting the timing for animations and other stuff right was quite challenging. I ended up just using the number of loop repetitions from
mlx_loop_hook
and fiddling with different rates, which also varied a lot between MacOS and Linux. - Sprites: The minilibx in Linux doesn't handle transparency in sprites the same way MacOS' minilibx does. On Linux the sprites will just have black pixels in the transparent area. I found ways to work this around but it was too much of a hussle.
- Leaks: Minilibx has some leaks that are only detected by
valgrind
. Using the functionmlx_destroy_display
solves that leak, but apparently that function isn't a part of the minilibx in MacOS. For this reason I decided to split the file where that function was called, so that the file compiled in Linux had that extra line for valgrind to be happy. - Keycodes: yet another difference between MacOS and Linux. The keycodes (
WASD
,ESC
, etc) are completely different. I defined those variables straight from the makefile to correct this issue.
This has been my favorite project so far, coding my own pacman clone was so much fun, regargless how flawed it might be :)
August 14th, 2021