-
Notifications
You must be signed in to change notification settings - Fork 5
Snake Clone
This tutorial assumes you completed the Simple Platformer tutorial.
This is another tutorial translated from the excellent work done in the TIC-80 tutorials, this time following the Snake Clone Tutorial
Start a new game folder with the following structure inside the Programs directory of Leikr:
Snake/
../Audio/Sound/eat.wav
../Code/Snake.groovy
../program.properties
This game will use some audio for the "eating" sound whenever the Snake eats some food. You can be creative with this and make your own eat sound using whichever audio software you'd like, and export to 16-bit wav. Otherwise if you'd like to just find a sound to use, sites like Open Game Art make it pretty easy to find work people share with the world (watch the licenses)
Firstly we need to set up the Boilerplate code of the game file.
class Snake extends leikr.Engine{
}
In Leikr, the create() method is optional and can be omitted. Since this game is so simple in design, I will opt to just define my variables right away at beginning. This way when the program gets initializes through the default constructor, the variables will already be ready.
When we first start our program we are going to need to initialize the snake's body, and the initial placement of food. We can use a Groovy Map for the player to make thing's very easy later on. We can also use the map for the food's x,y coordinates.
def snake = [[x: 15, y: 8], [x: 16, y: 8], [x: 17, y: 8]]
def food = [x: 0, y: 0]
def score = 0, time = 0
Just like in the TIC-80 version, we are going to use a dirs
variable to hold the directions we can move the snake. Using the Groovy maps again, this can actually be a little more readable too.
def dirs =[
up: [x:0, y:-1],
down: [x:0, y:1],
left: [x:-1, y:0],
right: [x:1, y:0]
]
What I really like about this is that now when we want to queue a direction using controls, we can call the direction based on the name itself in the map, instead of trying to use indexes. For example our next variable dir
which is the direction the snake will go.
def dir = dirs.up
Yes, now dir
equals [x:0, y:-1]
. Pretty slick right?
Now our full initialization should look like this:
def snake = [[x: 15, y: 8], [x: 14, y: 8], [x: 13, y: 8]]
def food = [x: 0, y: 0]
def score=0, time=0
def dirs =[ up: [x: 0, y:-1], down: [x:0, y:1], left: [x:-1, y:0], right: [x:1, y:0] ]
def dir = dirs.up
def lastDir
First we need to increment our time
variable and collect the head
and neck
portions of the snake's body.
void update(float delta){
time++
def head = snake.getAt(-1)
def neck = snake.getAt(-2)
}
So now we have our head
which is the very last index of the snake's body and neck
is a the element right before the head, obviously. We can use the Groovy collections method getAt(-1)
to get the last element in the list (using negative index. Awesome)
Before we finish with the core update logic, we will be defining a few methods.
boolean gotFood(head){
head == food
}
void setFood(){
food.x = randInt(0, 29)
food.y = randInt(0, 19)
snake.each{ i ->
if(food == i) setFood()
}
}
The gotFood(head)
method is our helper to check if the snake's head is on the same coordinate as the food. In Groovy we don't always have to explicitly use the return
keyword. Our method has a return type of boolean
, which is a true or false value, which means that the final line in the method will return a true or false. In this case, we are checking if the argument passed in, head
, is equal to food
. So if they are equal, it is the same as checking
if(head.x == food.x && head.y == food.y) return true
else return false
The next method setFood()
is used to place a food item after the snake has eaten a food item. It starts by randomly assigning the x,y values to the food
variable, then it checks if the food item is on any part occupied by the snake. If it is, it starts over with recursion
Now the rest of our update.
To add a slight delay to the game, we only update the rest of the logic if time%10==0
. So, essentially checking if time
is divisible by 10.
if(time%10 == 0){
snake.add([x: head.x+dir.x, y: head.y+dir.y])
if(!gotFood(head)) snake.remove(0)
else {
score++
playSound("eat.wav")
setFood()
}
}
In this we are adding a new body piece to the end of the snake using our dir variable and the head position. Then we check if we got food. If not (note the !
) then we remove the final piece of the snake. This makes it look like we are moving!
If the snake did pick up food, then we want to increment the score, play our eat sound, and set a new piece of food.
To finish up our update method, we will check for user input. We will simply use the keyboard arrow keys for this tutorial.
lastDir = dir
if(key("Up")) dir = dirs.up
if(key("Down")) dir = dirs.down
if(key("Left")) dir = dirs.left
if(key("Right")) dir = dirs.right
if(head.x+dir.x == neck.x && head.y+dir.y == neck.y) dir = lastDir
This is again why using a Groovy map with named members is so convenient. Depending on which key is pressed, we simply set the direction variable dir
to the direction we want to go from the dirs
map. At the end we are checking if the player is trying to reverse their direction. If this is the case, we just set it back to the previous direction, meaning no going backwards ;)
Render is pretty straight forward since we aren't really doing a whole lot of drawing in this tutorial. We will start by setting the background color to black which clears the screen.
Then, for each element in the snake, we will draw a simple blue filled rectangle. Notice that we are multiplying the x,y coordinates by 8. This is because all of the positional maths have been using simple 1 based arithmetic. If we want this to be positioned correctly using the whole screen, we simply multiply by the scale 8. We do the same for the food item, which we use a filled red rectangle for.
Finally, we draw the score in the top left corner. Groovy has really awesome String interpolation which means we can embed our variables directly into a String. More on Groovy Strings can be found here. This comes in handy with doing the RGB string color methods in Leikr.
void render(){
bgColor(6)
snake.each{i ->
fillRect(10, i.x*8, i.y*8, 8, 8)
}
fillRect(8, food.x*8, food.y*8, 8, 8)
drawString(1, "$score", 0, 0)
}
class Snake extends leikr.Engine {
def snake = [[x: 15, y: 8], [x: 14, y: 8], [x: 13, y: 8]]
def food = [x: 0, y: 0]
def score=0, time=0
def dirs =[ up: [x: 0, y:-1], down: [x:0, y:1], left: [x:-1, y:0], right: [x:1, y:0] ]
def dir = dirs.up
def lastDir
void update(float delta){
time++
def head = snake.getAt(-1)
def neck = snake.getAt(-2)
if(time%10 == 0){
snake.add([x: head.x+dir.x, y: head.y+dir.y])
if(!gotFood(head)) snake.remove(0)
else {
score++
playSound("eat.wav")
setFood()
}
}
lastDir = dir
if(key("Up")) dir = dirs.up
if(key("Down")) dir = dirs.down
if(key("Left")) dir = dirs.left
if(key("Right")) dir = dirs.right
if(head.x+dir.x == neck.x && head.y+dir.y == neck.y) dir = lastDir
}
void render(){
bgColor(6)
snake.each{i ->
fillRect(10, i.x*8, i.y*8, 8, 8)
}
fillRect(8, food.x*8, food.y*8, 8, 8)
drawString(1, "$score", 0, 0)
}
boolean gotFood(head){
head == food
}
void setFood(){
food.x = randInt(0, 29)
food.y = randInt(0, 19)
snake.each{ i ->
if(food == i) setFood()
}
}
}
Happy Hacking!