Swipe the touch screen or use arrow keys to change the snake's direction. Score by eating apples. If you eat yourself you lose!
I haven't made a game in years, so recently I decided to create my own game as a side project. Inspired by the little games on my TI-89 calculator that I used to play in math class, I decided to build my own version of a classic game snake, titled: B SNAKE.
One goal I had for this game was to be playable from both PC and mobile clients. With Blazor WebAssembly, a platform for running .NET in the web browser, I could feasibly write a game in C# that could be played by users on a PC or a smart phone.
I needed a way to programmatically draw rectangles and text to the screen with Blazor. I found the Blazor Canvas Extension, which provides C# library methods to draw 2D graphics in an HTML 5 canvas element.
I had to write my first game loop. I refreshed my memory and remembered that this goes something like this:
while (isRunning)
{
readInput();
updateGame();
renderGraphics();
}
Per my understanding, one limitation of using the Blazor canvas is that I must trigger a canvas re-paint using the standard Javascript requestAnimationFrame() method. That means I can't have a pure C# game, but I have to use a little dash of JS to get the wheels turning. From src/BlazorClient/wwwroot/index.html
// The game loop
function gameLoop(timestamp)
{
window.theInstance.invokeMethodAsync("GameLoopStep", timestamp);
window.requestAnimationFrame(gameLoop);
}
// Game loop initialization code (will be invoked by our C# at startup).
window.initRenderJS = (instance) => {
window.theInstance = instance;
window.requestAnimationFrame(renderJS);
};
Then in my Blazor component src/Pages/Index.razor, I implement the game loop code in C#. The method below sets up the first call to the JS game loop.
// The C# portion of the game loop (game and rendering logic is implemented by these objects)
public async ValueTask GameLoopStep(float ts)
{
// Update game
Game.Tick(ts);
// render graphics to canvas
await Drawer.Draw(Game.GetState());
}
// This is called when the blazor canvas renders.
// It initializes the game if needed and kicks off the JS game loop.
public async ValueTask OnRenderAsync(bool firstRender)
{
if (Game == null)
{
var repo = new HighScoreRepository(logger, Client);
Game = new SnakeGame(logger, repo);
Drawer = new BlazorDrawer();
}
this.ctx = await _canvasReference.CreateCanvas2DAsync();
await JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
await base.OnInitializedAsync();
}
For more details on the game loop, see this great guide by Scott W Harden on creating a game loop with Blazor.
I used the Blazor canvas component's OnKeydown event handler to capture user key presses. A PlayerInput class handles the key presses by keeping them in its state, and the game will check what the player's current input is during each iteration of the game loop.
public void OnKeyDown(KeyboardEventArgs e)
{
Game.playerInput.OnKeyDown(e.Code);
}
Touch screen interactions are captured by a separate Javascript touch listener, which translates those swipes into equivalent keyboard key presses. Those translated key presses get forwarded to the game for handling. In the future, it'd be ideal to include this in the C# implementation instead of the wrapper Javascript.
To give B SNAKE an old-school arcade machine vibe, I built a high score leaderboard displaying the champs' 3 initials. The user can press H or tap on the "High Score" text to see the leaderboard. The scores are stored in a cloud storage database and are retrieved by the game client using an HTTP request to the backend service. In my case, this backend service is hosted as an Azure Function.
After handing the game to my friends, they said it was too easy to die. They reported top annoying causes of death:
I played other versions of Snake without these annoyances and made small adjustments in my version. I gave the snake the ability to wrap-around the edges of the screen, which actually made it way more fun as the player finds new ways to dodge in and around their growing snake. I also prevented dying after a 180-degree turn by adding code to ignore the turn if it would make the snake collide with itself. With these 2 fixes, B SNAKE is surprisingly playable and fun.
B SNAKE is a success in my book. It was a fun challenge that got me familiar with Blazor and with making my own games from scratch. As I build more projects, I'm hoping to keep learning more game dev and software design techniques that make it easier to for me to build more advanced games in the future. Thanks for reading!