Assigment


Write a small real-time event loop that runs at a constant framerate, and shows whether the user has "left" or "right" pressed. Left will be the keyboard letter 'A' and right will be the letter 'D'.

JS Scope


Before we delve into making anything in the browser, we have to understand how it all works somewhat. Most of time we will be interacting with objects and functions that the browser provides (for example "document" and "window"). We have to talk about how these objects are available.

A few things:

  1. Everything in JS is a statement. This means class definitions, function declerations, function executions, etc.
  2. Every JS statement executes inside of a function. This is similar to how all C programs start from the "main" function.
  3. Every function defines a new variable scope and inherites the scope of the function it's in (let's call that parent scope)
  4. There is a top-level global scope but it's ill defined in JS and discouraged to use.
So for example, look at this code:

    function topLevel() {
        let a = 1;

        function secondLevel() {
            let b = 2;
            let c = a + b; //secondLevel can "see" topLevel's variables up to this point.
        }

        let d = b + c; //this is an error!
                       //'b' and 'c' are not visible here.
    }

                

Top Down...


The browser starts processing the page as it starts to download. JS statements can exist inside of <script> tags. The tag can also link to a JS file. These tags can exist in both the "head" and "body" elements of the page. The browser will execute the statements as the it gets them in order. If the "script" tag references a file, the browser will immediately load the file and start executing it.

Take this html:

    ...
    <div id='someElement'> ...
    <script>
        let div1 = document.getElementById('someElement');
        let div2 = document.getElementById('someOtherElement');

        //div2 is undefined because it doesn't exist yet! The browser hasn't gotten there yet.
    </script>
    <div id='someOtherElement'> ...
    ...
                

Many times people will put the "script" tags at the end of the page so that the browser can download all the markup and display something to the user and then get busy executing code. The scoping rules across different "script" blocks are a little funny. Script blocks get to "see" everything defined before it but not after. See an example [here]. This means the webpage itself is like a big giant function! If you could see the whole page as one function it might look like this:

    //browser does this in some kind of special initialization 
    let window = ... some special system call ...
    let document = ... 

    function loadPage() {
        ... execute all script blocks as their own little functions ...

    }
                

So why does this all this matters? In general, your functions will be easier to debug and develop if their scope is as limited as possible. You have to know the scoping rules before you can limit it! When every thing ins defined up top of the page then everything lives in the parent scope and everything can modify it. Over time it grows into a mess.

Real Time Event Loop


A basic real-time event loop for a game looks like this:

    function runGame() {
        while(true) {
            sampleInputs();
            updateGameState();
            renderFrame();
            sleepForABit();
        }
    }
                
However in the browser we can't do this. We have to hand control to the browser so that it can actually render the updates to canvas and what not. We also can't directly sample the keyboard and mouse and have to hook into the mouse and keyboard events. We'll use the "window.setInterval(...)" to handle the looping and sleeping.

So we will reorganize that loop a bit and do something like this:


    let gameState = {
        isPressingLeft:false
    };

    let gameContext = {
        outputDiv = null;
    };

    function runGame() {

        addEventListeners();
        buildContext();
        startLooping();
    }

    function addEventListeners() {
        
        let VK_A = 65; //VK is virtual-key code. See MDN for details of key press events.
        
        window.body.addEventListener("keydown", function(e) {

            // having only two '==' causing weird type conversions.
            // the preferred comparing operator '===' because it doesn't
            // try to convert 'null' to 0 and stuff.

            if( e.keyCode === VK_A ) {
                gameState.isPressingLeft = true;
            }
        });

        window.body.addEventListener("keyup", function(e) {

            if( e.keyCode === VK_A ) {
                gameState.isPressingLeft = false;
            }
        });
    }

    //typically your context are resources that only have to be
    //acquired once or are expensive to acquire.

    function buildContext() {
        gameContext.outputDiv = document.getElementById("someDivId");
    }

    function startLooping() {
        //second arg is frequences in ms so 1000/30 = 30 frames per sec
        window.setInterval(runOneGameLoop, 1000/30);
    }

    function runOneGameLoop() {

        updateGameState();
        renderFrame();
    }

    function updateGameState() {
        //nothign to update!
    }

    function renderFrame() {
        gameContext.outputDiv.innerHtml = "left: " + gameState.isPressingLeft;
    }

                

So now you have like 85% of what you need to complete the assignment! Pretty much get the last snipped of code to work and play around with it a bit. Notice that the output is a div somehwere in the document and not a canvas or anything like that.