An Interest In:
Web News this Week
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
- April 19, 2024
How I Made a Simple Snake Game with Ruby 2D
Snake
Everybody loves Snake. Although the original concept of the game dates back to 1976, this classic game won many hearts when it came preloaded on Nokia phones since 1998. I have many fond childhood memories of playing this classic game on my mom's phone, and I'm sure I'm not the only one whose childhood was bettered by this simple yet brilliant piece of entertainment.
Making Snake
When I decided I wanted to make a Snake game in Ruby, I searched the internet for many tutorials to get me started. By far the best tutorial I found was this one by Mario Visic on Youtube. All credit for this blog post goes to him, I simply followed his method and incorporated a few more things.
In order to re-create this classic game in Ruby, we need the help of Ruby 2D, a wonderful gem which allows you to draw visualizations for your Ruby programs with ease. This gem has a lot of functionality to really bring your programs to life, so it's a no-brainer when it comes to making 2D games in Ruby. Additionally, it has a quite comprehensive and very user-friendly documentation. To install this gem, simply run gem install ruby2d
, and then add require 'ruby2d'
at the top of your Ruby file.
The Initial Set-Up
Once Ruby 2d is installed and required, we have to set a background color, fps_cap (this determines how many frames the game will render per second), and grid size (this determines how many pixels wide each square on the grid will be):
require 'ruby2d'set background: 'navy'set fps_cap: 20GRID_SIZE = 20 #grid size is 20 pixels#for default window size of 480px * 640px, width is 32 (640/20) and height is 24 (480/20) at grid size = 20 pixelsGRID_WIDTH = Window.width / GRID_SIZEGRID_HEIGHT = Window.height / GRID_SIZE
These values can be changed if you desire a different grid size, a different background color, or a different game speed.
The Snake Class
All of the logic for the snake itself was encapsulated inside the snake class:
class Snake attr_writer :direction attr_reader :positions def initialize @positions = [[2, 0], [2,1], [2,2], [2,3]] #first coordinate is x and the second is y, starting from top left corner @direction = 'down' @growing = false end def draw @positions.each do |position| Square.new(x: position[0] * GRID_SIZE, y: position[1] * GRID_SIZE, size: GRID_SIZE - 1, color: 'olive') end end def move if !@growing @positions.shift end case @direction when 'down' @positions.push(new_coords(head[0], head[1] + 1)) when 'up' @positions.push(new_coords(head[0], head[1] - 1)) when 'left' @positions.push(new_coords(head[0] - 1, head[1])) when 'right' @positions.push(new_coords(head[0] + 1, head[1])) end @growing = false end #Preventing snake from moving backwards into itself def can_change_direction_to?(new_direction) case @direction when 'up' then new_direction != 'down' when 'down' then new_direction != 'up' when 'left' then new_direction != 'right' when 'right' then new_direction != 'left' end end def x head[0] end def y head[1] end def grow @growing = true end def snake_hit_itself? @positions.uniq.length != @positions.length #this checks if there are any duplicate positions in the snake (self-collision) end private #This method uses the modulus operator to make the #snake appear on the other side of the screen when it goes over the edge def new_coords(x,y) [x % GRID_WIDTH, y % GRID_HEIGHT] end def head @positions.last endend
Here, the snake is initialized as 4 squares on the top left corner of the window, heading downwards, and not growing. This is how the snake will appear every time the game is started.
The draw
method is used to convert the snake's @positions
array to actual squares on the grid, using Ruby 2D's Square
method.
The move
method moves the snake on the screen by using .shift
on the array of positions the snake occupies, which removes the first element of the array (which actually corresponds to the snake's tail, or last square). After the snake's tail is removed, .push
(which appends to the end of the array, corresponding to the snake's head) is called to redraw the snake's head 1 square away in the direction of movement. The position of the snake's head can be accessed by calling on the head
helper method. The redrawing of the snake's head also uses another helper method, .new_coords
, which makes the snake reappear on the other side of the screen if it goes over the edge. This encompasses pretty much all of the snake's basic movement.
The way in which the direction of movement is determined will become apparent later on in the code, but for now a can_change_direction_to?
method is required to prevent the snake from going backwards into itself.
Then, two simple x
and y
methods are require to simply return the coordinates of the snake's head (these will be needed later). A simple grow
method is also required to set the snake's @growing
condition to true. This will be triggered when the snake eats food and then @growing
will be set to false again after the snake moves.
Finally, a snake_hit_itself?
method is required to check if the snake has crashed into itself, which will finish the game. This is done quite cleverly by just checking if the @positions
array has any duplicate coordinates, meaning that the snake has crashed into itself. If this is the case, the length of @positions
and @positions.uniq
will be different (.uniq
removes any duplicates), and the method will return true.
*If you want to test the game so far, skip ahead to the the Game Loop and Key-Mapping sections, so you can run and interact with the game before proceeding to the next section. If you skip ahead, make sure to not include any references to the Game
class or game
instance anywhere as the Game class hasn't been defined yet.
The Game Class
Now, it's time to make the class that will encompass all of the game's mechanics:
class Game def initialize(snake) @snake = snake @score = 0 initial_coords = draw_ball @ball_x = initial_coords[0] @ball_y = initial_coords[1] @finished = false @paused = false end def draw_ball available_coords = [] for x in (0..GRID_WIDTH-1) for y in (0..GRID_HEIGHT-1) available_coords.append([x, y]) end end selected = available_coords.select{|coord| @snake.positions.include?(coord) == false} selected.sample end def draw unless finished? Square.new(x: @ball_x * GRID_SIZE, y: @ball_y * GRID_SIZE, size: GRID_SIZE, color: 'yellow') end Text.new(text_message, color: 'white', x: 10, y: 10, size: 25) end def snake_hit_ball?(x, y) @ball_x == x && @ball_y == y end def record_hit @score += 1 ball_coords = draw_ball @ball_x = ball_coords[0] @ball_y = ball_coords[1] end def finish @finished = true end def finished? @finished end def pause @paused = true end def unpause @paused = false end def paused? @paused end private def text_message if finished? "Game over, score: #{@score}. Press 'R' to restart, 'Q' to quit." elsif paused? "Game paused, score: #{@score}. Press 'P' to resume." else "Score: #{@score}" end endend
Here, the game is initialized with a score of 0, and the @finished
and @paused
conditions set to false. My code differs a bit from the video tutorial I followed in the way in which the ball (food) is drawn, as it calls on a helper method draw_ball
.
I wrote this helper method to check that the ball isn't drawn inside the snake. In order to do this, this helper method requires access to the snake's position, so I initialized the Game class with an instance of the Snake class. With access to the snake's position, draw_ball
finds the available coordinates to draw the ball in by selecting all the coordinates on the grid which are not currently being occupied by the snake. Then, this method selects a random sample from all those available coordinates and returns it. Props to my instructor Sylwia Vargas for helping me debug my old method which wasn't working!
draw
, again, converts the ball's position array to actual squares on the grid and it also draws a text message on the top left to display information, as long as the game isn't finished. This text message itself is delegated to a helper method text_message
. This helper method displays the current score and information about the game's state, changing accordingly if the game is paused or finished.
The snake_hit_ball?
method just checks if the snake has come into contact with the ball. This will be called in the game loop later.
The record_hit
method adds 1 point to the score and redraws the ball every time it's called.
Finally, the finish
, finished?
, pause
, unpase
and paused?
methods set and return the game's state accordingly.
The Game Loop
Now, with our main classes finished, it's time to put them to use inside our game loop, which will run every new frame:
update do clear unless game.finished? or game.paused? snake.move end snake.draw game.draw if game.snake_hit_ball?(snake.x, snake.y) game.record_hit snake.grow end if snake.snake_hit_itself? game.finish endend
Every frame, this loop starts by clearing the screen, moving the snake (unless the game is paused or finished), redrawing the snake, and redrawing the rest of the game.
Then, the loop checks if the snake has hit the ball, and if so it records a hit and makes the snake grow accordingly.
Finally, the loop checks if the snake has hit itself, in which case it finishes the game.
Key-Mapping
Our game is looking great so far, but it can't really be interacted with without key-mappings, so let's add that. The main key-mappings will allow us to control the snake's direction as well as pause, reset, and quit the game.
on :key_down do |event| if ['up', 'down', 'left', 'right'].include?(event.key) if snake.can_change_direction_to?(event.key) snake.direction = event.key end elsif event.key == 'r' or event.key == 'R' #resetting game snake = Snake.new game = Game.new(snake) elsif event.key == 'q' or event.key == 'Q' #quitting game exit() elsif event.key == 'p' or event.key == 'P' #pausing/unpausing game if game.paused? game.unpause else game.pause end endendshow
We start with an event-listener which will detect whenever a key is pressed. Then, we check the key pressed against multiple conditions.
If the key was 'up', 'down', 'left', or, 'right', change the snake's direction accordingly, as long as that's allowed by snake.can_change_direction_to?
.
If the key was 'r', 'q', or 'p', reset, quit, or pause/unpause the game accordingly.
Finally, we call show
to actually render the game's window. This is the very last line in our code as everything else about the game must be executed first before displaying.
Exporting the Game
Congrats! If you've made this far your game should be working perfectly whenever it's run in the terminal.
Although the game is looking great, it's a bit of a hassle to have to open up the terminal and run ruby snake.rb
every time we want to play it. So let's fix that.
On MacOS:
- Run
brew install mruby
,brew tap simple2d/tap
, andbrew install simple2d
in the terminal. - In the same directory as your Ruby file for the game, run
ruby2d build --macos <your_game_file_name.rb>
.
On Linux:
- Run
sudo apt install mruby libmruby-dev
in the terminal. - In the same directory as your Ruby file for the game, run
ruby2d build --native <your_game_file_name.rb>
These steps will generate a build
directory with your game inside.
Congrats! Now you can enjoy this great classic game by simply pressing on the executable file in the new folder.
Original Link: https://dev.to/joaocardoso193/i-made-a-simple-snake-game-with-ruby-2d-4on4
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To