Stopwatch Part III: Stopwatch in TypeScript

Now, let’s create the Stopwatch in TypeScript. This will move us closer to creating a Stopwatch extension.

If you have not created the Blocks version of the stopwatch, you may want to review the first two posts in this series:

Introduction

In this post, we will create the stopwatch using TypeScript. We also will talk about some basics of object-oriented design. We won’t go into a lot of detail; we will just highlight some of the concepts.

TypeScript

We will be using TypeScript for this project. (You may know it instead as JavaScript, since that is what the language is called in the editor.) We will not cover the basics of TypeScript here. If you want to learn TypeScript, then take a look at the CS Intro 3 course in the MakeCode Arcade documentation.

Finished code

Here is a link to the finished version of the project. Feel free to run the project in the simulator. As before, if you’re planning on walking through this journey with me, then please don’t look at the code yet. You don’t want to spoil the story! :slight_smile:

We will come back to this project later, as the code that we create together will be a little bit different from the code in the project above.

Working with the Text Sprite in TypeScript

Installing the Text Sprite extension

First, let’s work with the Text Sprite extension in TypeScript to get a feel for it. To begin, we’ll create a new project and install the extension.

  1. From the Home page, create a new project. Give your project an appropriate name, like “TypeScript Stopwatch”.
  2. Switch from Blocks to JavaScript.
  3. Install the extension just like you did in Blocks mode.
    1. In the toolbox, open the Advanced section.
    2. From the Advanced section of the toolbox, open the Extensions panel.
    3. Search for arcade-text.
    4. Click on the arcade-text extension to add it to your project.
Using the Text Sprite extension in TypeScript

Now, let’s create the same Text Sprite that we created in Blocks for our stopwatch. Feel free to copy-and-paste the following code. We’ll take a look at it together.

let textSprite : TextSprite = textsprite.create("0")
textSprite.setMaxFontHeight(12)
textSprite.setOutline(1, 2) // 2 == red
textSprite.setIcon(img``)
textSprite.x = 80
textSprite.y = 6

In line 1, we create the Text Sprite and set the initial text to “0” using something called a factory function. We will talk a little more about factory functions later. The remaining five lines (lines 2 through 6) set the options for the Text Sprite that we created, just as we did in Blocks:

  • In line 2, we set the font size to 12 pixels.
  • In line 3, we set the outline of the text to one pixel in size, and we set the color to red.
  • In line 4, we set the image that will appear next to the text. (The image is empty for now.)
  • In line 5, we set the horizontal (or x) coordinate of the Text Sprite.
  • In line 6, we set the vertical (or y) coordinate of the Text Sprite.

Feel free to create an image that will appear next to the sprite. Click on the artist’s pallette next to line 4. The image editor will open, and you will be able to create your image. Set the image’s size to 12 x 12, which matches the font size that we are using.

Run the project in the simulator if it is not running already. You will see that we already have created a Text Sprite, ready for us to use as a stopwatch! All with only six lines of code!

Factory functions

In the previous section, I mentioned that we used a factory function to create our text sprite. A factory function is a standalone function that creates variables for us, often simplifying the process. Factory functions are used a lot in MakeCode extensions to simplify things a bit for the Blocks environment. While designed for the Blocks environment, factory functions are useful in our TypeScript projects, too, to make things a bit simpler.

In our current code, we use a factory function to create our Text Sprite, and it works great! Because of how we are going to design our stopwatch application, though, we are going to have to make the code a little more complicated.

Replacing factory function

Because of how we are going to design our stopwatch application, we need to replace the call to the factory function. While it makes our code a little more difficult to read, we get to replace three lines of code with just one!

Replace the first three lines of code with this one:

let textSprite : TextSprite = new TextSprite("0", 0, 1, 12, 0, 0, 0, 1, 2)

Your code now should look something like this:

let textSprite : TextSprite = new TextSprite("0", 0, 1, 12, 0, 0, 0, 1, 2)
textSprite.setIcon(img``)
textSprite.x = 80
textSprite.y = 6

In this new line of code, we are using the constructor for the Text Sprite class. A constructor creates an object out of a class. It’s OK if you are not familiar with those terms. I’ll talk about them again later. The thing to notice, though, is how the factory function simplified the creation of our textSprite variable. The textSprite.create() function only required one parameter: the initial text to display. The constructor function that we are using now, though, requires nine parameters. Notice, too, that those nine parameters include the options that previously configured with additional lines of code: the 12 for the font height, the 1 for the size of the outline, and the 2 for the outline color.

Object-oriented design

Introduction

When we write programs for computers, we follow a programming paradigm. A programming paradigm describes both the structure of programs and how data moves within them.

When we write programs in Blocks, we involve two programming paradigms:

  • In structured programming, we create functions to help us break down a problem into smaller parts. We also use functions to help us reuse code. We create variables to store data; in Blocks, those variables are available to all parts of our program. (These are known as global variables.)
  • In event-driven programming, our programs respond to events – things that happen in our program. Some events are triggered by the user (like a button press); other events are triggered by the program itself (like when two sprites overlap).

Structured programming is an excellent programming paradigm. It’s the paradigm that I used when I was learning computer programming as a youngster, and most of the major programming languages support structured programming. With structured programming, you can learn all three fundamental constructions in programming:

  • Sequence: Performing a series of steps in a specific order
  • Decision: Running different pieces of code depending on a condition (also known as branching)
  • Loops: Repeating a set of code multiple times (also known as iteration)

For languages that support it, though, there is another programming paradigm: object-oriented programming, or OOP (usually pronounced as you would say the word oops). In object-oriented proramming, instead of breaking programs into functions, we break programs apart into objects. Objects are collections of variables and functions (they’re often called methods in OOP) that share something in common.

While Blocks offers a simplified form of object-oriented programming (through the arcade-sprite-data extension), TypeScript fully supports object-oriented programming. We will take advantage of TypeScript’s support of object-oriented programming for our Stopwatch application.

Class diagrams

When designing a program using object-oriented design, we often create class diagrams. Class diagrams visually display the data and functions that we group together, and they also display how different classes of information are related to each other.

For example, here is a class diagram of the Text Sprite class:

1

Let’s recall the code that we wrote earlier that created and manipulated a text sprite:

// First version
let textSprite : TextSprite = textsprite.create("0")
textSprite.setMaxFontHeight(12)
textSprite.setOutline(1, 2) // 2 == red
textSprite.setIcon(img``)
textSprite.x = 80
textSprite.y = 6

// Second version
let textSprite : TextSprite = new TextSprite("0", 0, 1, 12, 0, 0, 0, 1, 2)
textSprite.setIcon(img``)
textSprite.x = 80
textSprite.y = 6

In both versions of our code, we created an object that we called textSprite (with a lowercase t). That object was created from the class called TextSprite (with a capital T). The class name also is known as the object’s type, in the same way that we create variables of type int, bool, or string.

In our code, we also call functions on our object, like textSprite.setMaxFontHeight(). Notice that these methods appear in our class diagram for TextSprite. These functions “belong” to the TextSprite class, and these functions can be called on any object created from the TextSprite class.

Notice, too, in the class diagram that TextSprite objects have properties, like outlineWidth. Each object that is created out of this class will have its own set of properties. (They’re sometimes known as attributes.) So, one object (i.e. one text sprite) may have an outline width of 1, like in our sample project. Another object in the same program could have an outline width of 0 instead. In our sample program, we used the method setOutline() to change the width of the outline. We also could have written something like this:

textSprite.outlineWidth = 1
Inheritance

The objects created from the TextSprite class have other properties, like x and y for setting the text sprite’s location on the screen. If you look back at the class diagram, though, you will not find properties called x and y. Why is that?

The TextSprite class is not a standalone class. Instead, it depends on another class. A TextSprite, after all, is still a Sprite. In object-oriented terms, we say that the TextSprite class inherits from the Sprite class. We can show that in our class diagram:

2

The big arrow that links the TextSprite class with the Sprite class indicates inheritance. Because the TextSprite class inherits from the Sprite class, everything that works with a sprite will also work with a text sprite. You can change a text sprite’s location just as you can change a sprite’s location. You can give a text sprite velocity and acceleration, just as you can with any other sprite.

Creating the Stopwatch class

Adding a file to the project

While you can write the code for our new class in the main file, we typically put classes into separate code files. Let’s create a new file for our Stopwatch class.

  1. Beneath the simulator, click on Explorer to reveal the file explorer for our project.
  2. When the file explorer is expanded, a plus sign appears next to the word Explorer. Click on the plus sign to add a file to our project. A dialog appears on the screen.
  3. Give the new file the name stopwatch.ts.
  4. The new file opens in the editor.
  5. Switch back and forth between the main.ts file and the stopwatch.ts file. Also, feel free to explore the other files that are in your project.
Creating the Stopwatch class

Let’s create the initial version of our Stopwatch class. It will not look like much … but we get a lot of functionality out of this little bit of code!

stopwatch.ts

class Stopwatch extends TextSprite
{
}

The class diagram for our application now looks like the image below. I’ve simplified the diagram for TextSprite just to make the image a bit smaller.

3

Now, switch back to main.ts and change the first line of your code to use our new class.

main.ts

let textSprite : Stopwatch = new Stopwatch("0", 0, 1, 12, 0, 0, 0, 1, 2)

Pause for a moment and take this in. We have written no code at all for our Stopwatch class. None. However, because it inherits from the TextSprite class, we get all of its functionality!

A new constructor

Whenever an object is created from a class, a special function called a constructor is called. That’s what is happening in the first line of main.ts – an object from our new Stopwatch class is created, which calls the constructor function.

Let’s help out our users a bit, though, by writing a new constructor that’s simpler. Right now, we are using the constructor for the TextSprite class, which requires nine arguments. Let’s make some assumptions and shrink that down to zero.

In the Stopwatch class, create a constructor. Because the constructor takes zero arguments, it is known as a default constructor. The code for the entire class is shown below so that you know where to write the code for the constructor. Notice that the code for the constructor is very similar to the code that you already have in main.ts.

stopwatch.ts

class Stopwatch extends TextSprite
{
    constructor()
    {
        super("0", 0, 1, 12, 0, 0, 0, 1, 2)
        this.setIcon(img``)
        this.x = 80
        this.y = 6
    }
}

Our class diagram now looks like this:

4

Let’s go through this code:

  • The first line calls the constructor function in the TextSprite class – the class from which we are inheriting. (It’s also called our superclass or parent class for TextSprite.) This sets the initial text to “0”, sets the outline to 1 red pixel, etc… all of the things that it did when we used it in main.ts. We still need all of those things to happen, so we call that constructor here. To call the constructor of our parent class, we use the keyword super.
  • The second line sets the icon, the third line sets the horizontal coordinate, and the fourth line sets the vertical coordinate, just as it does in main.ts.
  • Notice that those three lines use the keyword this. When we are working with objects within our class, we do not know the objects’ names. So, when we are manipulating an object within its own class, we use the keyword this to represent that object.

Now, back in main.ts, because we are doing all of this work already in the constructor, we can replace all of the code with just this one line:

main.ts

let sw : Stopwatch = new Stopwatch()

Now, if our users want to change the icon, text outline, or location, they certainly can, using code similar to what we used to have in main.ts. We’re simply offering some initial settings that seem reasonable.

Adding functionality to the Stopwatch class

Planning functionality

We have our Stopwatch class functioning as a customized text sprite. Now, we need to add functionality so that objects work like stopwatches.

Think back to the Blocks version of the stopwatch. What functionality did we introduce? What information did we have to store and manipulate? How did we interact with the stopwatch?

Now, think about our Stopwatch class, perhaps in its visual form with its class diagram. Remember that objects store data in the form of properties (or attributes). Objects also have functions (or methods) that can act upon their properties. What properties would you add to our class? What functions are needed?

Consider those questions and their answers. Then, move on to the next section, where we add to our class diagram.

Adding to the class diagram

In the Blocks version, we created these variables to keep track of information related to the stopwatch:

  • timerIsRunning : boolean
  • startTime : number
  • accumulatedTime : number

We also created the following functions:

  • startTimer()
  • stopTimer()
  • resetStopwatch()
  • updateStopwatch()

Let’s incorporate these into our class diagram. Because we are adding these to the Stopwatch class, we know that all of these things are related to Stopwatch objects. We’ll simplify some of these names.

Adding code and connecting controls

Before we dive into the code for these functions, we’ll just add “function stubs” to our class. They won’t have any functionality just yet, but we can still call them.

Head over to stopwatch.ts and add the code that you see below. You already have the code for the constructor, so I’ll leave it out to shorten this code snippet.

stopwatch.ts

class Stopwatch extends TextSprite
{
    public isRunning : boolean = false
    public startTime : number = 0
    public accumulatedTime : number = 0

    constructor()
    {
	    /* You have code here; keep it! */
    }

    public reset() : void
    {
    }

    public start() : void
    {
    }

    public stop() : void
    {
    }

    public update() : void
    {
	    super.update()
    }
}

You may be asking yourself: “What does that word public mean?” We won’t go into details on access modifiers like public, private, and protected. For now, we will make all of our class members public so that we can access them from outside of the class. (The properties and methods of a class, together, are known as the class members.)

You also may notice that we have more than a stub for the update() function. And there’s that keyword super again! Briefly: The parent class, TextSprite, alredy has a function named update(), and it’s called whenever the sprite changes and needs to be drawn on the screen. If we leave our update() function empty, then the sprite never gets drawn on the screen. So, whenever update() is called for a Stopwatch object, the function in the TextSprite needs to run, too. We call TextSprite’s version of the function with the super keyword, similarly to how we used it in our constructor.

Now that we have these functions available, let’s hook them up in our main code file. Remember that, when the user presses A, the stopwatch starts or stops. When the user presses B, the clock resets to zero. We also should update the sprite in the game update loop if the stopwatch is running.

Let’s add those pieces of code to our main.ts file. Use the toolbox to add code snippets as needed.

main.ts

let sw : Stopwatch = new Stopwatch()

controller.A.onEvent(ControllerButtonEvent.Pressed, function() {
    if (sw.isRunning) {
        sw.stop()
    } else {
        sw.start()
    }
})

controller.B.onEvent(ControllerButtonEvent.Pressed, function() {
    sw.reset()
})

game.onUpdate(function() {
    if (sw.isRunning) {
        sw.update()
    }
})

Feel free to run your project. You can mash on the buttons all you want, but nothing will seem to change. The functions in our class are getting called. However, because the functions are empty, nothing on the screen changes.

Recalling Blocks implementation

Think back again to the Blocks version. When the user pressed A, we called a function called startTimer. What happened in that function? What about stopTimer? And resetTimer?

If you’re comfortable in TypeScript and comfortable with the code from the Blocks implementation, then pause here and see if you can write the code for reset(), update(), start(), and stop() in the Stopwatch class.

If you’re not quite there yet, then that’s OK! Pause here for a moment anyway and reflect on the code that we wrote in Blocks. Once you’re comfortable with that code, return here. We will implement the start() function next.

Implementing start

Normally, I probably would write the reset() and update() functions first. There wouldn’t be any real change in the sprite, though, so let’s instead write the start() function.

Recall from the Blocks implementation that the startTimer function saved the current game time and then set the Boolean variable to true. Let’s do the same thing in our Stopwatch class.

stopwatch.ts

public start() : void
{
    this.startTime = game.runtime()
    this.isRunning = true
}

Run your game in the simulator and press the A button. Nothing seems to happen. Why not?

Implementing update

Our text sprite stays at zero, even after the stopwatch has started. Why is that? Because we never change the text in the text sprite.

Remember that, in main.ts, we call the update() function in the main game loop if the stopwatch is running. That seems like a good place, then, to set the text of the sprite.

Think back again to our Blocks implementation. How did we figure out what text to display? We had to do a little bit of math, and then we did had to convert the value of that calculation to text (a string in TypeScript).

Can you come up with the code for the update() function? Give it a try if you like. Be sure to leave that call to the super version of the function. (Leave it as the last statement that runs before exiting the function.) When you’re ready, reveal the spoiler below to see the code.

stopwatch.ts

public update() : void
{
	let time : number = this.accumulatedTime
	if (this.isRunning) {
		time += (game.runtime() - this.startTime) / 1000
	}
	this.text = time.toString()
	super.update()
}

Run your program and press A on the controller to start the clock. Now, we just need to figure out how to stop it!

Implementing stop

As before, think about how we implemented the stopTimer function in Blocks. You’re probably getting pretty good at writing TypeScript by now, so give it a try. See if you can write the stop() function for our Stopwatch class. Then, reveal the spoiler to see my code.

stopwatch.ts

public stop() : void
{
	this.isRunning = false
	this.accumulatedTime += (game.runtime() - this.startTime) / 1000
	this.update()
}

Run your program. Pressing the A button on the controller should allow you to start and pause the stopwatch.

Implementing reset

The only thing left is to implement reset(). As before, give it a try on your own. Reveal the spoiler when you are ready!

stopwatch.ts

public reset() : void
{
	this.isRunning = false
	this.accumulatedTime = 0
	this.update()
}

We now have a functional stopwatch in TypeScript!

Extras

Additional functionality

Here are some ideas to improve our Stopwatch class. I implemented some of these ideas in my finished version, which you can find at the beginning of this post.

  • You may have notice some long decimals as the stopwatch ran, especially if you start and stop the clock. Can you display the time rounded to the nearest tenth or hundredth?
  • After 60 seconds have passed, the timer continues to count in seconds. You may not be used to seeing a stopwatch with 85 seconds displayed. Can you display the time to include minutes, as well?
  • Do you have other ideas that would improve our stopwatch?
Full Stopwatch class diagram and PlantUML code

I used PlantUML to create the class diagrams in this post. The code for the diagram is below. Visit the link below to view and edit the code at the PlantUML web site.

Stopwatch class diagram at PlantUML web site

@startuml
class Stopwatch {
  -isRunning: boolean
  -accumulatedMs: number
  -startTime: number
  +constructor()
  +getState(): boolean
  +reset()
  +start()
  +stop()
  +update()
  -formatTime(ms: number): string
}

class TextSprite {
  +text: string
  +bg: number
  +fg: number
  +maxFontHeight: number
  +borderWidth: number
  +borderColor: number
  +padding: number
  +outlineWidth: number
  +outlineColor: number
  +icon: Image = null
  __
  +constructor(text: string, bg: number, fg: number, maxFontHeight: number,
    borderWidth: number, borderColor: number, padding: number,
    outlineWidth: number, outlineColor: number, icon: Image = null)
  __
  +update()
  +setMaxFontHeight(height: number)
  +setIcon(icon: Image)
  +setText(text: string)
  +setBorder(width: number, color: number, padding: number = 0)
  +setOutline(width: number, color: number)
}

class Sprite {
  +x: number
  +y: number
}

Stopwatch --|> TextSprite
TextSprite --|> Sprite
@enduml
4 Likes