Source: Rikulo Blog

Rikulo Blog Creating a Snake game using Rikulo, HTML 5 and the canvas

IntroductionThis blog post details how to create a Snake style game using Rikulo, Dart, HTML 5 and the Canvas. The final game serves as a sample of how to use Rikulo along with Dart.The ResultTo control the Snake try using the arrow keys or using a mouse on a computer or finger to swipe on a tablet or phone.For a better experience you can view the demo on its own hereModeling the itemsI started by modeling the different elements of the game, we are required to model the Snake, its environment and the food that it eats. If an item was complex I split it over a couple of classes. The file names should give you all a clue as to what elements are modeled within them, but just for completeness sake here is a list of files and what they represent.Snake.dart - The actual snake, will require a body, direction and will be responsible for acting on turnsFood.dart - The food that the snake eatsSnakePoint.dart - A point on the canvasSnakeEvironment.dart - The environment that the snake acts inSnakeCanvas.dart - The main class which sets up the gameI will walk you through the process of modeling the Snake and Food.SnakeAs previously mentioned the Snake will be responsible for its own body and direction. First of all let's setup the snake body. The body will be represented via a list of SnakePoints. The SnakePoint is very easy so we can include the entire code for the class here.class SnakePoint { int x, y; SnakePoint(this.x, this.y); String toString() { return "($x, $y)"; } } As we can see the SnakePoint class is just used to represent a point and does nothing else. So the body representation of the snake in Snake.dart. The snippet below also includes methods for retrieving the Snake's length and head.List<SnakePoint> body; int length() { return body.length; } SnakePoint head() { return body.last(); } The Snake also has a direction which is represented internally via an integer. The directions are also provided using static integer which represent the directions of the Snake. The following code snippet outlines the possible snake directions and the getter and setters.static final int UP = -2, DOWN = 2, LEFT=-1, RIGHT=1; int _direction; int get direction() => _direction; set direction(int value) { if((_direction + value) != 0) { _direction = value; } } In this case the setter for direction does not check whether the direction is one of UP, DOWN, LEFT or RIGHT. That is of course desired should a production quality game being created but for the purposes of our example we will leave it be. The set direction function prevents an opposite direction being set, so the Snake cannot be moving left and be made to move right. It does this by assigning absolute equal values to opposite directions, thus when _direction is added to the proposed value and the outcome is 0 then the direction is not set.Finally we move onto looking to implement the actions of the snake, this is generally to move and to grow depending on whether the snake consumes the food or not. To implement these actions we create a function called act which takes the canvas rendering context and the location of the food, this function is responsible for movement and checking for growth, let's take a look at the function.bool act(CanvasRenderingContext2D context, Food food) { if(initial) { body.forEach((element) => drawSnake(context, element, null)); initial = false; return false; } SnakePoint moveTo = nextMove(); bool grow = (moveTo.x == food.x && moveTo.y == food.y); var removed = move(moveTo, grow); draw(context, removed); return grow; } As shown in the function when the snake is initialized this function will draw the entire snake using the drawSnake method which we will talk after this section. The snake also looks at the next available move before moving to determine whether or not it will actually consume the food. Whether or not the snake will grow is stored in a boolean value and then passed to the move method to move the actual snake, the following shows this method.SnakePoint move(SnakePoint to, bool grow) { var removed = null; if(!grow) { removed = body[0]; body.removeRange(0, 1); } body.add(to); return removed; } In this method if the snake will not grow the last point of the body is removed and the new point where the snake moves to is added. However, if the snake will in fact grow then the last point is not removed, so in effect the snake grows by 1 square. The removed point is then returned from the function.The snake, as all actors are in this simple example, is responsible for its own drawing. Drawing the shape of the Snake is a reasonably simple affrair, let's take a look at the code.void draw(CanvasRenderingContext2D context, SnakePoint removed) { drawSnake(context, body.last(), removed); } void drawSnake(CanvasRenderingContext2D context, SnakePoint point, SnakePoint removed) { context.beginPath(); context.fillStyle = "black"; num adjustment = SnakeEnvironment.adjustment; if(removed != null) { context.clearRect(removed.x, removed.y, adjustment, adjustment); } int width = SnakeEnvironment.adjustment; int height = SnakeEnvironment.adjustment; int radius = SnakeEnvironment.adjustment / 3; context.beginPath(); context.moveTo(point.x + radius, point.y); context.lineTo(point.x + width - radius, point.y); context.quadraticCurveTo(point.x + width, point.y, point.x + width, point.y + radius); context.lineTo(point.x + width, point.y + height - radius); context.quadraticCurveTo(point.x + width, point.y + height, point.x + width - radius, point.y + height); context.lineTo(point.x + radius, point.y + height); context.quadraticCurveTo(point.x, point.y + height, point.x, point.y + height - radius); context.lineTo(point.x, point.y + radius); context.quadraticCurveTo(point.x, point.y, point.x + radius, point.y); context.closePath(); context.fill(); } This code will draw one point of the snake and remove one point of the snake. The new point is the new head of the snake to simulate mvoement and the point removed is the last point of the snake, unless it grows. The quadraticCurveTo and lineTo function calls will paint a rounded rectangle. If a rounded rectangle is not required it is of course possible to just replace this with a context.rect function call which would simplify the code a lot.FoodThe food is the easy part as it just consists of 3 functions, relocate, draw and the constructor. In addition to that there are two properties, one for x and one for y which hold the location of the food.Let's start with the relocate method:void relocate(List<SnakePoint> avoid) { double suggestedX = Math.random()*((snakeEnvironment.width / SnakeEnvironment.adjustment) - 1); double suggestedY = Math.random()*((snakeEnvironment.height / SnakeEnvironment.adjustment) - 1); suggestedX = suggestedX.floor() * SnakeEnvironment.adjustment; suggestedY = suggestedY.floor() * SnakeEnvironment.adjustment; bool has = false; for(final point in avoid) { if(suggestedX == point.x && suggestedY == point.y) { has=true; break; } } if(has) relocate(avoid); else { x = suggestedX.toInt(); y = suggestedY.toInt(); } } The above relocate function takes a list of points to avoid (in this case the Snake's location) and then locates a point on the grid where the food can be painted. The algorithm is a very simple implementation and no doubt can be improved. The last function is the draw function which is self explanatory. It will draw an old style Snake food item which consists of 4 small rectangles within a small area.void draw(CanvasRenderingContext2D context) { double smallSquareWidthAndHeight = SnakeEnvironment.adjustment / 3; context.beginPath(); context.fillStyle = "black"; //first rectangle, top row context.rect(_x + smallSquareWidthAndHeight, _y, smallSquareWidthAndHeight, smallSquareWidthAndHeight); //two rectangles second row context.rect(_x, _y + smallSquareWidthAndHeight, smallSquareWidthAndHeight, smallSquareWidthAndHeight); context.rect(_x + (smallSquareWidthAndHeight * 2), _y + smallSquareWidthAndHeight, smallSquareWidthAndHeight, smallSquareWidthAndHeight); //bottom row rectangle context.rect(_x + smallSquareWidthAndHeight, _y + (smallSquareWidthAndHeight * 2), smallSquareWidthAndHeight, smallSquareWidthAndHeight); context.closePath(); context.fill(); } Having walked through individual classes it is time to discuss how the environment and gaming loop were implemented, the following section introduces these concepts.The environment, game loop, UI and controlsThese are 3 pretty large topics, however, first of all let's take a look at how the environment is setup.The environmentIn our class the canvas is initialized in the SnakeCanvas.dart class which is the main class for the UI and the game loop. First of all let's walk through the SnakeEnvironment class which initializes all of the objects within the game and determines whether the gamer scored, continues or loses.class SnakeEnvironment { static final int SCORED=0, GAMEOVER=1, CONTINUE=2; static final num adjustment = 10; num height,width; Snake snake; Food food; SnakeEnvironment(this.height, this.width) { if((this.height % adjustment != 0) || (this.width % adjustment != 0)) { throw new IllegalArgumentException("Height & Width must be divisible by the adjustment (${adjustment}) without a remainder"); } snake = new Snake(this); food = new Food(this); food.relocate(snake.body); } int draw(CanvasRenderingContext2D context) { food.draw(context);

Read full article »
Est. Annual Revenue
$100K-5.0M
Est. Employees
1-25
CEO Avatar

CEO

Update CEO

CEO Approval Rating

- -/100



Rikulo is a Private company. Rikulo has a revenue of $1M, and 10 employees. Rikulo has 2 followers on Owler.