Learn basic javascript / typescript for game development

- 5 mins
Hello1

Intro

Now that we have setup a simple typescript game environment with cold reloading we are ready to start exploring different game design concepts using typescript code. The rest of the tutorials in this series will leverage typescript to demonstrate different game design concepts.

Whilst not totally necessary if you are not familiar with typescript the following tutorials will be easier to understand with some foundational typescript knowledge.

That's why today we will be looking at some specific common javascript and typescript examples and concepts that I frequently use when doing typescript game development.

Everything discussed in this tutorial can be tested in a typescript repl (an online typescript compiler).

If for whatever reason the repl isn't working. You can use the simple-typescript-canvas-game-project-setup as a base.

Lets jump in!

Primitive types

Typescript as the name suggests is a strongly typed language that builds on javascript. For anyone that has worked with a strongly typed language before typescript aims to bring to javascript all the benefits that types introduce to a language.

Typescript uses a : type syntax to denote the type of an expression. Typescript offers type bindings for many common primitive types that exist in most strongly typed languages such as...

  • string
  • boolean
  • number

Typescript primitive types are all lowercase so we use string instead of String. The String keyword is the javascript String constructor and the lowercase string is the typescript type binding.

Type bindings

When working with dependencies we often need to add the typescript type bindings for those dependencies otherwise the typescript compiler won't know how to infer the different libraries custom types.

An example of this using styled-components (a css in js library) is below.

npm install --save styled-components
npm install --save @types/styled-components

The first line above saves the styled-components dependency to our project. The second line saves the type bindings required for styled-components. Without these bindings we would receive compile errors when explicitly using styled-components types.

Logical operators

&&: Used to signify logical AND. true && false//false, true&&true//true ||: Used to signify logical OR. true || false//true, false || false//false !: Used to signify logical NOT. !true//false, !false//true ===: Used to test strict equality 11==='11'//false ==: Used to signify 11=='11'//true

In general it's best to use strict equality === and convert to the appropriate type before comparing.

Defining variables

In javascript we can use var, let and const. var has pretty much been superseded by const and let.

const defines a constant that cannot be reassigned and let can. A general rule of thumb is to prefer const over let unless you have to re-assign a variables value.

const animal1: string = 'dog'
animal1 = 'cat' // Error

let animal2: string = 'dog'
animal2 = 'cat' // No error

Custom types

Typescript allows us to define custom types based on primitive types and even other custom types. Because of this we can build up a game domain type system like Lego blocks as shown below.

type Weapon = string

type Direction = 'left' | 'up' | 'right' | 'down'

type Position = {
  x: number,
  y: number
}

type PositionWithName = {name: string} & Position

type GameState = {
  lives: number
  score: number
  playerName: string
  playerPosition: Position
  direction: Direction
  spawnPosition: PositionWithName
}

const invalidDirection = 'forwards' // This will result in a compile error

const gameState: GameState = {
  lives: 3,
  score: 0,
  playerName: 'Blake',
  playerPosition: {
    x: 0,
    y: 0
  },
  direction: 'right',
  spawnPosition: {
    x: 100,
    y: 100,
    name: 'base'
  }
}

The Weapon type is a type alias for the primitive string. We can now use the Weapon type in place of the string type. This can make code easier to understand if we have a function that takes a string that represents a weapon we can use Weapon instead for the argument.

Here we have defined a custom union type Direction which can only take on the value of the 4 directions specified. We use the | operator to create a union of the 4 different direction strings. Position defined a x, y coordinate. We can use the object {} key value syntax to describe custom types with nested fields.

The PositionWithName demonstrates an intersection type using the & operator which combines all the fields between the two types hence creating a new custom type with x, y and name.

The invalidDirection constant will result in a compile error as forwards does not equal one of the possible Direction types.

We define another custom type GameState which has a direction field of type Direction, playerPosition of type Position and spawnPosition of type PositionWithName.

Finally the const gameState shows how we can declare a constant that has the GameState type.

Functions

Typescript functions look similar to javascript functions except that the arguments and output have associated types. These types can be inferred or explicitly referenced.

const displayScore(score: number): string => {
  return `Players score: ${score}`
}

displayScore(1) // 'Players score: 1'

The anatomy of the above function is described below.

In typescript and javascript functions are treated as first class citizens which means we can assign them to variables and pass them around the same way we would pass around a primitive type (boolean, string etc). This is why we are allowed to use the const keyword to define our function.

displayScore is our function's name, semantically we use camelCase for our function names.

Everything inside the () represents the arguments to our function. In this case our function takes one argument, score of type number.

The type after the :, string represents the return type of our function.

Everything inside the {} is the body of our function. The code that is run when we invoke our function.

We are using JavaScript's string interpolation that leverages back-ticks ``` to compute a dynamic variable in a string.

The return keyword is used to make the function return some output. The return type should match our function's output type otherwise we will receive a compilation error.

We invoke our function using it's name and providing the required arguments, ie displayScore(1).

In the above example I have explicitly referenced both the argument types and output type of the function. If we leave these out typescript will infer the types and the code will still compile. However typescript won't always know the exact type we are intending to use so I prefer the explicit alternative syntax.

Classes

Classes can be used to group certain functionality together.

class Player {

  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  static compareAge = (player1: Player, player2: Player): string => {
    if (player1.age < player2.age) {
      return `${player1.name} is younger than ${player2.name}`
    } else if (player1.age === player2.age) {
      return `${player1.name} is the same age as${player2.name}`
    } else {
      return `${player1.name} is older than ${player2.name}`
    }
  }

  bio (): string {
    return `${this.name}, age: ${this.age}`
  }
}

const p1: Player = new Player('Tessy', 27)
const p2: Player = new Player('Lucy', 25)
console.log(p1.bio()) //Tessy, age: 27
console.log(Player.compareAge(p1, p2))//Tessy is older than Lucy

The Player class above has two fields name and age that can be set via the constructor when initiated as shown above.

In the above code p1 and p2 are separate instances of the Player class and are instantiated via the constructor using the new keyword.

The static keyword in the compareAge function means that compareAge can be called on the Player class and not on an instance of Player

The bio function on the other hand can only be called on an instance of the Player class.

If else and switch

if (health <= 0) {
  triggerGameOver();
} else {
  increaseScore();
}

switch(lightOn) {
  case 'on':
    return 'Light is on'
  case 'off':
    return 'Light is off'
  case 'flashing':
    return 'Light is flashing'
}

An if else statement allows us execute different code based on some condition. In the example above if health <= 0 gameOver is triggered otherwise the score is increased.

A switch statement is another way to write conditional logic. If a case statement's condition is matched with the variable inside the switch statement the code inside the case statement is executed.

Ternary

const alive = (health > 0) ? true : false

The ternary operator ? is a handy shorthand way to set a variable's value conditionally. In the example above alive will be set to false if the player's health is below 0 and true if not. The same effect can be achieved via an if condition and a mutating variable but this is arguably cleaner code and these sort of conditional results are frequently present in games.

Imports exports

To use classes, functions or variables from another file they must be exported. The below code gives an example of how we can export the GameRuntime class and LevelName variable.

//Game.ts
export class GameRuntime {
}

const levelName = 'Webbed woods'

export const levelName

To import these values into another file the import syntax can be used. Then the code can be referenced and called as shown below.

import {levelName, GameRuntime} from './Game'

console.log(levelName) //'Webbed woods'

Destructuring

Destructing provides an easy way to extract fields of an object as shown below.

type GameState = {
  playerPos: Position
  score: number
  gameOver: boolean
}

const loop = (gameState: GameState) => {
  const {playerPos, score, gameOver} = gameState
  console.log(playerPos, score, gameOver)
}

loop({playerPos: {x: 1, y: 2}, score: 10, gameOver: false})//{x: 1, y: 2}, 10, false

Spread operator

The spread operator ... allows us to easily add properties of one object to another object.


type GameState = {
  playerPos: Position
  score: number
  gameOver: boolean
}

const gameState = {
  playerPos: {x: 1, y: 2},
  score: 10,
  gameOver: false
}

const gameOverGameState = {
  ...gameState,
  gameOver: false
}

console.log(gameOverGameState)//{playerPos: {x: 1, y: 2}, score: 10, gameOver: true}

In the example above we are spreading the gameState onto a new gameOverGameState which transfers all of the original properties of the current gameState, we then manually set the gameOver field to be true.

Summary

The above examples should give a good typescript foundation to help understand the rest of the tutorials in the series.