Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 16, 2020 04:53 pm GMT

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:

  1. Run brew install mruby, brew tap simple2d/tap, and brew install simple2d in the terminal.
  2. In the same directory as your Ruby file for the game, run ruby2d build --macos <your_game_file_name.rb>.

On Linux:

  1. Run sudo apt install mruby libmruby-dev in the terminal.
  2. 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

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To