00:00:11.660
Welcome to "From Start to Published" using Ruby! Today, we'll be building a game from scratch.
00:00:18.119
We'll be recreating a basic implementation of the arcade classic, Snake, from start to finish. Then, we will
00:00:24.420
publish it onto a platform called itch.io.
00:00:29.519
If you have the DragonRuby engine from itch.io, that's great! If you don't,
00:00:35.219
I recommend visiting itch.io later to create an account and claim the DragonRuby Game Toolkit so that you can publish later if you wish.
00:00:42.600
Alrighty! My name is Cameron Gose. This is a year of a lot of firsts for me.
00:00:47.820
This is my first official Ruby Central conference. This is also the first time I've given a workshop at a conference. My first conference was actually RailsConf, which was last month, where I met my good friend Tim Chapeirini.
00:01:06.000
He's been a great friend. This speaks to the Ruby community's support; after I told him about what I was doing, he offered to help. I really appreciated that.
00:01:18.000
The DragonRuby Game Toolkit—it's not a typo, it's "DragonRuby" as one word for SEO purposes—is a powerful game development toolkit.
00:01:25.560
The runtime targets Ruby language specs, allowing us to use Ruby, one of the best programming languages, to develop games across multiple platforms. It's a multi-level cross-platform runtime that can target PC, Mac, Linux, Raspberry Pi, WebAssembly, iOS, Android, Nintendo Switch, PlayStation, Xbox, and Oculus VR, with more support being added. Recently, you can also use a Steam Deck as a developing device.
00:01:44.840
Part of the runtime implements features similar to M Ruby, rather than CRuby, so there may be some differences in the available Ruby methods. The game toolkit is built on top of the runtime, providing a self-contained environment. No dependencies are needed, and you can download it and start right away.
00:02:00.000
Currently, it functions primarily as a 2D game engine, but there are 3D capabilities being added. For example, a lot of work is being done on drawing triangles, which is a crucial piece for building polygons in 3D models.
00:02:12.180
As for requirements, you'll need to bring your own code editor. You can use any editor you prefer—we have no restrictions! Additionally, you need the game toolkit, either through download or the files we provide. You will also need a Linux machine, a Mac, Raspberry Pi, or Windows.
00:02:25.320
Now, how do you get it? Earlier I mentioned one way—the itch.io link. I will walk through it for those watching at home.
00:02:38.520
There is the itch.io page, and then dragonruby.org. You might be familiar with Ruby Motion; DragonRuby came out of Ruby Motion by Amir and his team.
00:02:44.940
If you scroll down to the bottom, you'll see the game toolkit, where you can read more about it with a more detailed list of available features at different levels.
00:02:58.680
The standard level is essentially what you have now. If you have itch.io access, that’s mostly the standard tier; however, if you want more features, such as using C extensions or deploying to iOS or Android, you'll need a higher level.
00:03:12.240
Alright, let's move into setup! First, ensure you have the unzipped folder ready.
00:03:29.700
For those who are following along, please copy the unzipped folder directly to your desktop. You will want to have a terminal pointing to it, or you can change directories into it in your terminal.
00:03:42.060
You will also want to point your editor to open this directory.
00:03:48.660
Once you have this set up, we can start running the game! You will do this by executing the command: .\dragonruby for macOS or Linux, and .\dragonruby.exe for Windows machines.
00:04:05.220
I should also mention that for macOS, it works well on Apple silicon too. The publishing step runs flawlessly on my machine.
00:04:18.240
When you run that command, you should see a window like this that says "Hello, World!" Thank you for reminding me of the intro! This window will display some text and also output an image.
00:04:43.680
Let’s quickly review the code so you can get an overview of how it operates. In the main.rb file, there is a 'tick' function. This function receives arguments, where the arguments being passed in represent the game toolkit environment. This environment gives you access to all the APIs available within the game toolkit.
00:05:04.380
The outputs, such as args.outputs.labels, are collections that you can append to, which will display text. We'll go over this in more detail later, but for now, the sprites will be used to display images in the game window.
00:05:17.700
Now, we're going to approach this a bit differently: we'll actually start by publishing! If you access the internet right now, feel free to follow along.
00:05:29.880
First, if you haven’t created an itch.io account yet, please do so. Once you create an account, you need to visit the following URL.
00:05:37.920
Navigate to "/game/new" and this will open up a new window.
00:05:43.600
Here, we will need to create a game page. Enter a title—let's call it "RubyConf Mini Snake." When you type in a title, an auto-generated slug for the project webpage will be created.
00:06:02.640
Keep note of this slug, as we'll need it for the publishing step. For the game description, we can mention: "This is a basic recreation of Snake." The classification will be a game.
00:06:16.560
For now, we want to specify that the project will be downloadable. Here, enable the option for accepting no payments, but just to note, the itch.io platform is a great resource for content creators to monetize their work.
00:06:27.240
Once done, scroll to the bottom and hit ‘Save and view page.’ Right now, it's not visible to the public as it's in draft mode, as shown in the top right of the screen.
00:06:40.140
Next, we need to go back into the code editor and update the metadata.
00:06:49.860
In your code editor, expand the file section and navigate to my game—there you should find a folder called metadata.
00:06:57.480
Inside, there will be a file called game_metadata.txt. To publish, you'll need an itch.io account. Your Dev ID will be your itch.io username.
00:07:09.800
The Dev Title will be the title you provided earlier.
00:07:15.720
As for the Game ID, that will be the slug from the URL, which, in this case, is "ruby-conf-mini-snake".
00:07:27.120
The Dev Title is just your name, not the game title. The Game Title is what you filled out in the form.
00:07:39.120
You can uncomment the version and icon lines in the metadata file. If you want to change the icon for your game, you can always do that in the future.
00:08:01.080
Currently, open another tab in your terminal and go into the same folder.
00:08:07.640
We will then execute the command "dragonruby.publish" and include the flag "--only package my_game".
00:08:15.600
This command will initiate the initial builds for the game and produce it. Starting off with this publish step is important because DragonRuby makes it easy to iterate on your publish once you have it set up.
00:08:36.600
The process automatically publishes to itch.io afterward. Can everyone see that okay? Let me make things a bit bigger.
00:08:56.520
Does anyone need to back up? Tim!
00:09:00.720
If you have entered this command, press enter and it will take some time.
00:09:07.920
The command will run some compiling and building, generating a number of files that will be visible in the builds folder.
00:09:14.280
To see your builds folder, check within the game toolkit folder. You should now observe multiple build options.
00:09:23.280
This includes an HTML5.zip for WebAssembly, a Linux binary, a Raspberry Pi binary, a macOS zip folder, or folder that you can run the game from, and also a Windows build.
00:09:31.200
It's fairly straightforward to produce executables for each of these platforms.
00:09:45.720
Once you have produced these builds, go back into your itch.io game page and select "edit game."
00:09:54.200
From there, you will scroll down to the uploads section and click "upload files." Find the builds directory.
00:10:01.680
Select the respective builds you want to target for your game.
00:10:06.920
You can either select all of them or just specific ones. In this case, select the HTML5.zip or the Linux 64 builds and so on.
00:10:15.840
Hit upload; this may take a while, but since the game is small, it should not take too long.
00:10:25.720
The game size is relatively small, ranging between three to six megabytes, representing the basic transition from "Hello World" to publishing.
00:10:34.200
Now, returning to your game page, go to "view page" to see your game available for download.
00:10:42.000
Let’s move on. Now, we will cover the basics of making a game.
00:10:47.560
The core idea behind building any kind of game consists of three steps: the game loop.
00:10:58.520
The game loop handles input, updates and calculates the current game state, and finally, it renders to the screen.
00:11:05.280
In short, you need to take user input, determine what's going to happen on the screen based on that input, then draw it accordingly.
00:11:14.860
In the main.rb file, we will create three functions. You can go ahead and delete the "Hello World" text currently in your main.rb file.
00:11:27.180
We will fill it with functions to handle the game loop. The first function we'll create is 'handle_input'. This will be used for handling player inputs.
00:11:40.860
Next, we'll create an update function, which will manage changes to the game state.
00:11:51.060
Finally, we will implement the render function to draw things on the screen. The 'tick' function needs to call the handle_input, update, and render functions in that specific order.
00:12:05.760
The tick function is essential in DragonRuby, as it runs everything in a cyclical manner. Each time the tick is called, the game updates and redraws at a smooth 60 frames per second.
00:12:20.760
Now, we'll begin to set up the scene. First, we’ll draw a grid. The snake travels within a grid, similar to the original Snake game.
00:12:36.360
At the top of the main.rb file, let's create a constant called grid_size, and set it to 20. I chose 20 because it evenly divides the aspect ratio that DragonRuby uses for rendering.
00:12:56.720
Above the render function, create another function called 'render_grid.' We will call this function above when we create the render function.
00:13:14.640
Now, let’s go back to the render_grid function. Here, we will first draw the vertical lines along the x-axis.
00:13:29.880
We will create a numeric grid to split the grid on the x-axis. The total width of the window, which you can access via arguments grid.width.
00:13:44.400
Divide that total width by the grid size, and you will have the number of vertical grid lines.
00:13:52.680
We will also iterate through each vertical grid line using a "each_with_index" method.
00:14:01.620
Finally, we will use args.outputs.lines to draw the lines. Using a hash syntax is very straightforward for our needs, allowing us to specify the X position, Y position, and X2 and Y2 values for rendering lines.
00:14:15.720
Now that we've implemented the vertical lines for the x-axis, we will repeat a similar process for the y-axis.
00:14:27.720
The code will be very similar; only this time we will draw lines horizontally across the game window.
00:14:40.300
We'll determine the number of lines needed by dividing the grid height by the grid size. As before, we'll also use 'each_with_index' to iterate through those y-axis coordinates.
00:14:54.900
Once we have the coordinates, we will generate each horizontal line, ensuring to set proper x and y values based on the grid size.
00:15:09.480
Next up, we will start drawing the head of the snake. We will create a green square at the center of the grid.
00:15:22.680
Let's create another function that will be called within the tick. This is going to hold the state of the game.
00:15:32.240
In the 'defaults' function, we will use the state property available to us from the game toolkit. This will help us store the game state across ticks, enabling data persistence.
00:15:52.440
We will initialize the snake’s head, centering it in the grid by setting the x and y positions to half of grid.width and grid.height.
00:16:02.760
Next, we need to set the width and height of the snake’s head equal to the grid size.
00:16:15.480
For the head's RGB representation, we'll set it to bright lime green with the values 23, 245, and 23.
00:16:24.280
Once we finish defining how the head is set up, we will update the tick function to call defaults, ensuring the parameters are passed in.
00:16:36.320
This means that every time tick is called, the defaults will instantiate the initial state.
00:16:50.000
Next, we proceed to rendering the snake. Here, we’ll create a separate 'render_snake' function.
00:17:01.800
In this function, we will pass in the array containing the head state.
00:17:12.680
To draw the head, we will utilize args.outputs.solids, which is a collection used for outputting squares and rectangles.
00:17:24.880
Now that we have the head rendering function, ensure that this function is called inside the render function so that it displays the green head on the grid.
00:17:37.480
Next, we’ll add movement to the snake to make it interactive. This is where we start handling input in earnest.
00:17:53.480
A few changes shall be made. We will create some local variables within the handle_input function. For instance, we'll assign inputs equals args.inputs for ease.
00:18:05.460
Similarly, we will set a variable called head that equals args.state.head.
00:18:16.040
Now that we have those variables, we can write conditions to handle movement based on user input. For example, if inputs.left is pressed, we will set head.direction to left.
00:18:31.720
We’ll do the same for right, up, and down inputs, ensuring the snake can move fluidly.
00:18:48.240
Next, we will introduce another constant at the top of the file under the grid size. This constant will be called speed, and we will set it to 10.
00:19:01.920
Though this may seem like the speed of the snake, it actually represents the wait time every 10 ticks before we increment the snake.
00:19:12.480
Now, we will create a new function called 'move_snake' and place it above the update function.
00:19:23.440
In this function, we will again create a local variable to store the snake head and define a directional vector.
00:19:39.320
The vector will hold x and y position values for movement. Next, we will use a case statement to determine the direction based on the head’s current direction.
00:19:58.240
When moving right, the x vector will equal one, while for left, it will equate to negative one. We'll do a similar computation for the y direction.
00:20:10.820
Next, we update the head’s x and y values based on those directional vectors.
00:20:30.940
In the update function, we will incorporate a condition that checks if the current tick count, mod speed, equals zero.
00:20:43.840
If so, we’ll execute the move_snake function, allowing the snake to move based on user input and the set speed.
00:20:58.300
We should now see the snake randomly jumping across the screen based on input, simulating a retro gaming aesthetic.
00:21:09.260
In line with the philosophy of the DragonRuby community, we emphasize the importance of publishing early and often.
00:21:24.520
Once you have something presentable, you can call the Dragon Ruby publish command again without any flags.
00:21:36.360
Doing so will rebuild the game and allow for updates through itch.io.
00:21:48.480
This will execute some more tasks using Butler, automating the deployment process.
00:22:00.480
When you run the command, you will see an authentication dialogue. Once authorized, you won't have to authenticate again.
00:22:11.640
Once published successfully, it will show up with a timestamp for when it was last updated.
00:22:22.440
Next, we will introduce boundaries, ensuring that the snake has walls it cannot cross—ultimately leading to a game over state.
00:22:38.920
In the defaults function, we will create properties for the walls, defining their positions relative to the game screen.
00:22:49.640
These properties will also include a color specification via RGB values.
00:23:02.680
We will also set up another function named render_walls to draw the walls around the game window.
00:23:12.320
The walls will be added to the solids collection rendered as per the current state.
00:23:23.900
After rendering walls, you’ll notice the snake may not appear in that wall area upon initialization.
00:23:38.160
To reset the game to its initial state, DragonRuby has an in-built feature, the Quake Terminal, making it easy to do that.
00:23:52.480
Once that’s initialized, you should see the snake in the middle with walls surrounding it.
00:24:06.880
Now, we will add collision detection to halt the snake when it hits the walls.
00:24:21.240
Create a new function called handle_boundary_collision to check for intersections between the head of the snake and the walls.
00:24:35.000
This will include logic to check whether the head intersects with any of the walls using additional parameters for min and max boundaries.
00:24:47.040
In the update function, we shall call handle_boundary_collision to ensure the snake remains within the game area.
00:24:59.840
Now, if everything is set up properly, you should see that moving the snake left or right should not allow it to cross beyond the walls.
00:25:16.640
So, onto scoring: as the snake moves around, collectible items will appear in random locations.
00:25:31.440
These will serve two functions: incrementing the player’s score and making the game progressively harder.
00:25:44.080
We will create a new function called spawn_collectible, which will generate collectibles in specific conditions.
00:25:56.640
The function will check if a collectible is nil; if it is, we will generate random x and y coordinates for it.
00:26:09.760
The x coordinate will be derived from the grid width, divided by the grid size, with adjustments to avoid collision.
00:26:24.320
Following this, we will initialize the collectible with equivalent size attributes and assign it a color.
00:26:36.920
Once the spawn_collectible function is created, we will also set up a render_collectible function above the render function.
00:26:51.640
The render function will add the collectibles to the output solids collection for display in the game.
00:27:05.760
Next, we’ll implement collision detection between the snake’s head and the collectible. This function will be called handle_collectible_collision.
00:27:19.480
In this function, if the collectible is nil, we have no need to check further.
00:27:31.680
When the head intersects with the collectible, we set the collectible to nil to respawn in a new location at the next update.
00:27:47.100
Additionally, we will update the score every time a collectible is picked up, storing this in the state score.
00:28:01.560
Upon each successful collection, the player’s score will increase by one.
00:28:13.260
Finally, we need to display the score to the player. In the render function, we will include code that outputs the score in a specified location on the screen.
00:28:28.920
The x position will be derived from the left edge of the grid, shifted slightly to the right, while the y position will be near the top edge.
00:28:41.840
Finally, we will render the current score in the outputs section, displaying it in real time as the game progresses.
00:29:00.560
Next, let's add sound effects! For this workshop, use the provided collect.wav sound file.
00:29:14.880
You will need to add that file to the sounds folder within your game directory.
00:29:27.080
In handle_collectible_collision, you simply use args.outputs.sounds to append the sound whenever a collectible is grabbed.
00:29:39.240
If you choose .wav files for sound effects, they will only play once, while .ogg files will continuously play.
00:29:53.440
Now, let's conclude by increasing the difficulty of the game by expanding the snake body.
00:30:05.560
In the defaults function, add an empty array called args.state.body and set this up so it initializes once.
00:30:18.800
In the update function, we'll call 'grow_body.' If the body has any segments, we will clone the last segment and track the head’s position.
00:30:34.480
We will then set the direction of each segment of the body to follow the direction of the head.
00:30:48.840
Now, we want to ensure the segments are drawn along with the head while also checking for any collisions with the body.
00:31:02.880
To do this, we will create a function handle_body_collision to check for intersections between the head and the body.
00:31:14.720
If any part of the body collides with the head, we will set the state to game_over.
00:31:27.960
Render an additional label to display the game over message, including the final score.
00:31:38.920
In the main update function, include checks for the game state. If the game state is set to game over, it will trigger rendering the game over screen.
00:31:51.480
To reset the game, include checks for keyboard input. If the escape key is pressed, reset the game state.
00:32:03.880
Finally, let's publish the finished game and deploy it on itch.io!
00:32:18.100
Although Snake might not be the most thrilling game, this process demonstrates the basics of game development.
00:32:34.360
If you've stuck around and followed along, thank you all for participating! In the DragonRuby community, we have resources and documentation available to you.
00:32:48.660
I highly recommend joining the DragonRuby Discord. It's an engaging community eager to expand their familiarity with game development.
00:33:03.440
The documentation on dragonruby.org is crucial, and additionally, exciting samples for various game genres are available.
00:33:18.640
Finally, I encourage everyone to explore game jams, including the current DragonRuby community's 20 second game jam.
00:33:36.960
Participating in game jams provides a fantastic opportunity to practice and showcase your development skills.