Site logo, www.grelf.net
 

The Forest - program design

This page documents aspects of the design of the software of The Forest, a simulation of the sport of orienteering.

Further documentation from the user's point of view can be found here: User Guide. It would be useful to have read that explanation and tried the program before reading the current page.

I want to encourage creative programming and I am keen for others to have access to this information so it will not be lost at some future date. If others wish to develop the ideas further or use them in other works, then I approve. A reference back to me would be appreciated, as well as some discussion of your plans.

The program is written entirely in client-side JavaScript downloaded by a single HTML5 page. No data go back to the server and no cookies are used.

It is my contention that this platform, HTML5 + JavaScript, is now suitable for many kinds of video games. It avoids the need for the user to install anything and there is no need to involve an "app" store. A small server-side PHP program could limit access to paying customers if required. There are some compromises in the graphics but The Forest demonstrates that a huge amount of detail can be shown in real time. True realism is not necessarily the most important factor in a game. Of course none of this is valid if access to device-specific hardware facilities is necessary, such as cameras, accelerometers, etc.

This page is still being written. New versions are uploaded from time to time (2020).

24 March 2020: The method of generating the terrain is now explained in more detail. Here...

Limitless terrain from limited code

This is the main point of The Forest. Its terrain is unbounded but it is generated from a small amount of code (180 kilobytes, as of June 2019). This results in a small download and start-up time. This does not mean that the terrain is necessarily permanently static. The program includes some things which illustrate change. For example, lake levels rise when it rains and fall back again afterwards. Also features can be added at specific locations, and moved to different locations, without adversely affecting drawing time. The generator is in the file terrain.js and its description below describes how the effects are achieved.

It has to be admitted that older devices may not be fast enough to draw the map or scene at a reasonable speed. This should only improve with time.

The Forest really is vast. By using command line arguments, eg ?x=1e6&y=1e6, it is possible to see when the map fails. It extends for 1.8 x 1012kilometres (1.8 billion). Point features run out after about 30,000km but without causing any problems.

 This terrain is different!

Most developers of video games involving terrain begin by creating a map as a grid*, adding details to it and storing it as a data structure for the game to retrieve and portray as the player(s) move around.

The Forest uses a quite different approach which I developed around 1980 (because machine memory was so very limited then). Given the position of a player as x metres east and y metres north of some origin point, a mathematical function t (x, y) calculates all the details of that point, including

All such things are calculated from the player's coordinates and those of all the points in the circular area immediately around the player. (The visible radius can be chosen by the player and it can be greater on faster machines.) The details are exactly repeated when revisiting the same points but they are unpredictable, so even the author of the game can explore the terrain without knowing exactly what will be found.

The technique can be extended for use on spherical planets by using 3 coordinates x, y and z - not latitude and longitude because of their singular behaviour at the poles.

It is difficult to create linear features such as roads or rivers because the technique is totally based on points rather than lines but nevertheless The Forest does have some such features. It also demonstrates that the terrain is not fixed: see what happens when it rains or if you manage to take a ride in the rocket.

Players in the role of explorer can fall down the mineshafts into a layer of mines (dungeons if you like). Generating these from a function of position is even simpler, as can be seen here.

* The more usual grid may first be generated procedurally (by an automatic algorithm) but that is not the same as the even more automated technique used in The Forest.

 Some conventions

Although I aim to keep a tidy structure by means of OOP there are some compromises to avoid having too lengthy multi-level dot names. So, for example, ROLES at the start of observer.js is NOT Observer.prototype.ROLES because that would become too cumbersome. (In the browser it is really window.ROLES of course but we need not state that.)

Apart from the standard 2D graphics API which, as The Forest demonstrates, is very efficient, I do not use any other JavaScript libraries because they would adversely affect both download times and execution speeds. I despair sometimes of web sites that cause me to wait for this library and that even when they do not seem to be doing anything fancy on their home pages.

 A note about geometry

Computer graphics inevitably involves some geometry. The diagram on the right covers most of what is needed here. This shows a right-angled triangle with sides x, y and d. An observer at O (not necessarily the origin of the coordinate system) may be looking at a point P distance d away on a bearing of b degrees. If the diagram looks unconventional it is because in map work using a compass we use bearings measured clockwise from due north (the y-axis) whereas in maths the convention is that angles are measured anticlockwise from the x-axis (due east).

In JavaScript the formulae are as follows, where I have included the essential conversions from degrees to radians.


  x = d * Math.sin (b * Math.PI / 180);
  y = d * Math.cos (b * Math.PI / 180);
  d = Math.sqrt (x * x + y * y);

The conversion factor Math.PI / 180 should NOT be calculated every time it is needed but done once at the start of the program and set to a constant for multiple re-use (in The Forest it is called DEG2RAD); this is an important general principle for speed. Equally the conversion of b to radians by that multiplication would just be done once in a statement preceding the ones shown.

You may also need to get b from x and y:


  b = Math.atan2 (x, y) * 180 / Math.PI;

Do always use Math.atan2 () instead of Math.atan () because the latter cannot return an unambiguous value in the full 360° range. And for the smart-eyed, yes that is atan2 (x, y) and not the other way round: check the diagram; it is because we are using bearings again.

 Moving the observer

What I call the observer is known in other games as the camera but it is where the player's eyes are supposed to be in the terrain. Moving forward is really easy:


  x += s * Math.sin (b * Math.PI / 180);
  y += s * Math.cos (b * Math.PI / 180);

where s is the stride length, after taking into account the type of terrain and the gradient (slope) in front of the observer.

On each stride the bearing also drifts randomly by up to 5 degrees because it is hard to stay on a fixed bearing when running through rough terrain:


  b += ((Math.random () - 0.5) * 10) % 360;

Note that b is kept within the range 0..360. Then the scene is redrawn, as seen from the observer's new position.

 

 Repeatable pseudo-random bit patterns

There are trees in The Forest of course. They are loaded into the program as image files (in PNG format to allow transparency around them). To avoid monotony there needed to be several different tree images. Suppose there are four (in fact there are more than that). At each position on the ground one of the four is to be chosen, seemingly at random. At that position, whenever the user looks at it, maybe after moving away and coming back, it must always be the same tree out of the four.

This is achieved by calculating a pair of bits (2 bits allows 4 possible values) from a function of the x and y coordinates of each point. To make the bits appear to be random a pair of bits is taken from a function that includes multiplication by π, mathematical pi which is irrational: it has an unpredictable sequence of digits (or, in base 2, bits).

In JavaScript it looks like this:


  var rand = Math.round (PI10000 * x * y) & 0x3;
                      // 2 bits, bit-wise ANDed

where the constant PI10000 has been calculated once at the start of the program:


  PI10000 = Math.PI * 10000;

That shifts pi up a bit so we are not looking at its first bits.

The 0x is not necessary but it is a reminder that we are not interested in decimal numbers here. 0x means hexadecimal which is a useful way of writing bit patterns. If we needed four bits we would AND with 0xf.

A similar thing is done for the exact positions of trees within the 2m square tiles that form the ground, so the trees do not lie always in dead straight rows.

 Is it 3D?

Yes and no. The ground is drawn in true 3-dimensional perspective. Objects placed on the ground are only 2-dimensional images and look the same from whichever direction they are viewed. This is a compromise but it may also be seen as an advantage: users do not have to steer all round things to see what they are or how to use them or get into them.

 Event driven with double buffering

It is important to realise that The Forest is not the kind of game program which tries to redraw the screen in time with video frames, 25 or 30 times per second (or more typically these days, 60Hz). Instead it only redraws in response to changes initiated by the user. If a button or key is pressed or the screen image is touched or mouse-clicked that may require the scene or the map to be changed.

Event listener functions are invoked to handle the user's actions and decide what needs to be done. An event is therefore the beginning of a sequence of function or method calls. When the sequence is complete processing ceases until another event is detected.

It is part of the HTML5 specification that drawing on a displayed canvas is done using double buffering. First the drawing is done in a hidden back buffer (copy of the canvas) and when the drawing is complete buffers are switched so that the back buffer becomes the front buffer, displayed. In this way if the drawing takes longer than one video frame interval, as it generally does in The Forest, the user will not see the drawing building up, only the final result.

In The Forest that buffer switch usually occurs when the sequence of processing initiated by an event comes to an end.

The Forest contains some slow animations, such as when the explorer operates the machine for draining lakes: the map changes in steps and the user is given time to see each step.
In this case the standard function setTimeout (func, delay_ms) is used. Here func is the name of the (parameterless) function to call after an interval of delay_ms milliseconds. In such cases the processing sequence ends just after the setTimeout call, so the display occurs. Another event-driven processing sequence then starts on a timer event from the system clock, instead of the user interacting.

Occasionally we do some faster animation, such as when an explorer falls down a mine shaft or a lift is used inside a building. Then we explicitly ask for display on successive frames by repeatedly calling requestAnimationFrame () and that is the function that other game programs would call if they were running continually in a loop.

 

 Perspective calculation

It is perfectly possible to draw scenes in true 3D perspective even in the plain 2D graphics context. The following diagrams should explain the maths. An object at distance d from the observer is to be projected on a screen at distance FF. In the plan view sp is an object of type ScenePoint (see scenepoint.js) which contains data about a ground point, such as distance d from the observer and bearing difference b from the observer's straight-ahead direction. These values will have been calculated using the simple geometry described earlier, while scanning the area around the observer.

So in scene.js there is a method like this:


  Scene.prototype.getScreenXY = function (x, y)
  {
    var sp = this.around [x][y]; // sp has distance and bearing from observer
    var ht1 = this.ft.terra (x, y).height; // see terrain.js: ft is forest.terrain
    var brad = sp.b * DEG2RAD; // sp.b is in degrees
    var zz = sp.d * Math.cos (brad);
    if (zz < 1) zz = 1; // Avoid points behind me       
    var yy = (ME_HT - ht1 + this.ht0) * this.YSCALE; // Predetermined vertical scale
    // ht0 is the ground height of the observer's location
    // ME_HT is how tall the observer (constant)
    var xx = sp.d * Math.sin (brad);
    // Perspective:
    var fRatio = this.FF / zz; // Must avoid zz near zero
    var sx = fRatio * xx + this.wd2; // Relative to screen centre, wd2
    var sy = this.htBase + fRatio * yy;
    // htBase is where anything at observer's eye level would appear
    return {x:sx, y:sy}; // Return object with two properties, screen x and y
  };

Note the need to avoid zz being near zero. This can occur either when object and observer are coincident (d = 0) or, more likely, when the object is 90 degrees to either side of the observer. The latter case is simple to ignore for small objects because they will be well outside the field of view (45 degrees to either side in The Forest) but it becomes a problem for extended objects such as walls and this is discussed further in the section about mine.js.

 

 The foggy or hazy horizon - how it is done

This code lies within the file scene.js but it is described as a separate section here because it illustrates a few techniques that could be applied more generally.

It was realised that a foggy horizon would be beneficial because otherwise objects can pop into view too suddenly as they are approached. This is particularly the case when the user's horizon range is short, say 100m or less. Some users will keep the range short because the processors in their devices are not fast enough for a longer range. That means that whatever we write to produce fog must not slow scene drawing down noticeably. Nevertheless the fog has been made optional by means of a check-box at the bottom of the HTML page, initially not checked (as of version 18.12.2).

 How fog is represented

The distance to the horizon is initially 60 metres but can be made longer by the user via the drop-down list labelled "Visible range in scene". If the distance from the observer of any object which is to be drawn in the scene is greater than three quarters of the distance to the horizon then the object is assigned a fog factor which rises linearly from 0 at 0.75 of the horizon distance to 1 at the horizon. In the program file, scene.js, this fog factor is called fogNo and every ScenePoint object in the arrays around and ahead (which describe the ground near the observer) has the property fogNo (0..1).

The fog factor determines the degree to which the colour of any pixel in the drawn object is to approach the colour of the sky. When fogNo is 0 the pixels retain their original colour but when it is 1 they become the colour of the sky. Intermediate values lie proportionally within that colour range. Each of the components of the colour, red, green and blue, is scaled accordingly.

The scaling is done by two different methods, as described in the next sections.

 Buildings and tiles

The method for fogging the walls and doors of buildings and for uniformly coloured tiles such as paving and lake surfaces is relatively simple because they are each drawn by filling a path, created in the graphics context, with a single colour. It is only necessary to calculate the foggy colour, interpolated between the normal colour of each surface and the colour of the sky. This is done in scene.js by the function fogRGB (). This takes as input parameters the 3 colour components and the fog factor. It returns a colour style string in the usual hexadecimal format, #xxxxxx. The code for this is quite simple:


  function fogRGB (r, g, b, f) // f=fogNo, 0..1
  {
    r += (skyR - r) * f;
    g += (skyG - g) * f;
    b += (skyB - b) * f;
    return makeCssColour (Math.floor (r), Math.floor (g), Math.floor (b));  
  }

  function makeCssColour (r, g, b) // each 0..255
  { var rs = r.toString (16), gs = g.toString (16), bs = b.toString (16);
    if (r < 16) rs = '0' + rs;
    if (g < 16) gs = '0' + gs;
    if (b < 16) bs = '0' + bs;
    return '#' + rs + gs + bs; 
  }

(The standard function rgb () might be useable instead of my makeCssColour () but this works efficiently.)

 Other objects, loaded as images

Objects loaded as images presented some much more interesting design problems.

As of version 18.12.2 there are 30 images to be loaded that represent objects that can appear in a ground-level outdoor scene in The Forest. These begin loading when the Scene object is constructed. They comprise about 1.1 megabytes of data.

It is not feasible to apply the fogging pixel by pixel to each image as it is being drawn in the scene: it would take far too long (bear in mind that there really are thousands of image drawing operations, with varying scale factors, to construct a scene). So the images have to be prepared in some way. Also it is not feasible to prepare all possible values of the fog factor. Mathematically there is a continuous range of values from 0 to 1. Computationally the discrete range of possibilities is still large. So the first decision was to select just 8 stages of fogging on the way from 0 to 1. The continuous value of fogNo is changed to integers from 0 to 7:


  var iFogNo = Math.round (fogNo * 7);

This means that we can have an array of 8 versions for each original image. But we do not really want to have to download 8 times as many images. Not only would that be a much larger amount of data but it enlarges and complicates the code to initiate so many downloads.

The function loadImage () in forest.js was changed so that the onload function creates an 8-element array, called foggy, and sets its [0] element to refer to this just-loaded image. The remaining elements will be undefined for now:


  im.onload = function ()
  { im.loaded = true;
    im.foggy = new Array (8);
    im.foggy [0] = im;
  };

The next decision was to create the fogged versions of each image only when the need for them first arose. To do this a wrapper method was written to replace each call to context.drawImage (im, x, y, wd, ht) (note: the last 2 parameters there are scaled width and height: drawImage does scaling very efficiently). The wrapper starts like this:


  Scene.prototype.drawImage = function (im, x, y, wd, ht, fogNo) // fogNo 0..1
  {
    if (0 === fogNo || !this.doFog) this.g2.drawImage (im, x, y, wd, ht); // unfogged
    else
    {
      var fogged, iFogNo = Math.round (fogNo * 7);

      if (undefined === im.foggy [iFogNo]) // Fogged version not yet created, create it now:
      {

(this.g2 is a copy within the scene object of the graphics context and this.doFog is a boolean controlled by a check-box in the HTML page, for whether the user wants the fog effect.)

If there is no requirement for fogging then drawImage () is called immediately. Otherwise we check whether we have yet got the image with the required amount of fogging, indexed by iFogNo in an array called foggy that was constructed as a property of each image when it was loaded. If the required image does not exist then this is the time to create it.

Creating the fogged image requires processing each (non-transparent) pixel of the image to interpolate its colour towards that of the sky. We then need the result to be of type Image again because we need the scaling capabilities of drawImage () (scaling cannot be done by context.putImageData ()). I have written about this in detail on the image processing page of my JavaScript (HTML5) course.

After processing the pixels to change their colour for fog we put the image data back into an image by using canvas.toDataURL () and that involves reloading just like the initial loading of an image from file. So the result is not necessarily available immediately for display. Another design decision was therefore to display nothing if the fogged version is not yet ready. On a subsequent call for a version of this image with the same degree of fogging it will be possible to draw the right version. This simply means that the fogged objects appear in stages but it will not be long before fully fogged scenes are drawn as the user moves around.

Here is an annotated version of the finished wrapper method for drawImage ():

 
  Scene.prototype.drawImage = function (im, x, y, wd, ht, fogNo) // fogNo 0..1
  {
    if (0 === fogNo || !this.doFog) this.g2.drawImage (im, x, y, wd, ht); // unfogged
    else
    {
      var fogged, iFogNo = Math.round (fogNo * 7); // iFogNo 0..7
    
      if (undefined === im.foggy [iFogNo]) // Fogged version not yet created, create it now:
      {
        // Get the image data:
        var cnv = document.createElement ('canvas');
        cnv.width = im.width;
        cnv.height = im.height;
        var g2 = cnv.getContext ('2d');
        g2.drawImage (im, 0, 0); // Full size
        var imData = g2.getImageData (0, 0, im.width, im.height);
        var px = imData.data; // Array of pixels (rgba - 4 bytes per pixel)
	  
        for (var i = 0; i < px.length; i++)
        {
          var i3 = i + 3;
        
          if (0 === px [i3]) i = i3; // skip transparent pixel (a == 0)
          else
          {
            px [i] += (skyR - px [i]) * fogNo; i++;
            px [i] += (skyG - px [i]) * fogNo; i++;
            px [i] += (skyB - px [i]) * fogNo; i++;
          } 
        } // NB: loop inc skips alpha channel, a

        g2.putImageData (imData, 0, 0); // Back into the canvas

        // LOAD the image data back into an Image object:
        fogged = new Image ();
        fogged.onload = function () { fogged.loaded = true; }
        fogged.src = cnv.toDataURL ('image/png'); // Transfer in PNG file format
        im.foggy [iFogNo] = fogged;
        // Do not draw - may take time to load. It will be missing from scene for now
      }
      else
      {
        fogged = im.foggy [iFogNo];

        if (fogged.loaded) this.g2.drawImage (fogged, x, y, wd, ht);
      }
    }
  };

Once each fogged version of an image has been created it is likely to be used hundreds of times in drawing each scene.

 Why I like HTML5 2D graphics

The biggest advantage of the standard HTML5 API is that it is already present in everyone's browser. Alternatives such as WebGL either are not available for all devices or would require the user to download and install something.

Working with the HTML5 standard in The Forest has shown me that it is very efficient and effective for my purposes. In particular the following capabilities are extremely useful.

Overall it is a very effective system.

Mozilla reference page about clipping

 Programming notes: HTML files

 index.html

This is the main web page for the program, in HTML5. All of the user interaction occurs here but it is kept as simple as possible. All CSS is kept in a style element in the head because there is not very much of it. All JavaScript is loaded from files by script elements at the end of the body (so a page appears before script loading starts). See the note in the red box below about how script file names change as new versions are made.

The main action occurs in a <canvas> element, so it assumed that the user's browser is sufficiently up-to-date to recognise that. The canvas is initially set to 800 x 600 pixels and that gives a reasonable drawing speed for the various views. (A possible enhancement would be to allow the user to make the canvas larger if their device is fast enough.)

The ordinary 2D graphics context of HTML5 is used by the scripts, so that they should work on all platforms (therefore I do not use WebGL, for example).

The layout of input elements on the page may seem rather untidy but this is an attempt to allow finger room between them if the program is running on a smart phone.

The information and controls available on the page change as the program is used. This is done by having unique id attributes on several of the HTML elements and then using

  document.getElementById ("an_id").innerHTML = "new content";

 courses.html

This is the page that appears if a user in course planner role clicks the button for managing courses. You can easily see that it includes a few of the script files from the main page plus one new one: manage.js.

 Programming notes: JavaScript files

NB: The names of the JavaScript files are changed every time they are modified, by a 1- or 2-character suffix. The script elements in the HTML must therefore be altered every time a new script version is to be uploaded. The server is set to tell browsers not to cache the HTML or CSS but all other resources (scripts or images) may be cached and must therefore change name to download new versions. The benefit is that the end user does not have to download everything every time the program is run, only things that have changed.

The scripts are now (2019 Dec) listed in alphabetical order, rather than in decreasing order of significance as they attempted to be previously. So a few pointers to the most important ones should be given here.

 

 around.js

This file was created for version 19.4.10 to enable scene drawing to be changed so that it does not reallocate two huge arrays and fill them with freshly created objects every time. This rewriting reduces the scene drawing time by about 30%. There is also less variability in the time taken from one scene to another which tends to confirm that garbage collection and reallocation were taking a lot of the time. Reallocation would be variable because it depends so much on what contiguous chunks of memory are available.

The terrain is generated by a function of (x, y) position (see terrain.js). It is a complicated function so it does not want to be done repeatedly for each position; instead its results must be held in arrays for subsequent reference during scene building. Two* big arrays hold the results of that around the (moving) observer's current position to enable the scene to be drawn. For each position the arrays refer to an object of type ScenePoint which contains the distance and bearing of the point from the observer and other things (such as terrain details and amount of fogging for distant points). Each ScenePoint object has 10 properties so requires at least 80 bytes, probably more like 100 bytes allowing for the Object structure itself (I think that varies from browser to browser).

One of the big arrays, around [x][y], goes out to the current view range which is user-selectable and can be up to 400 meters, so the array may have to be 801 x 801 to surround the observer. This is now allocated at the start of the program. Each array value will be a reference to a ScenePoint object so the array itself takes 8 x 801 x 801 = 5.1 megabytes. Then the 801 x 801 ScenePoint objects, also now allocated once at the start instead of freshly in each call to Scene.draw (), occupy about 100 x 801 x 801 = 64 megabytes.

[* Another array, ahead [i], contains references to the same ScenePoint objects but is sorted during scene drawing so that the most distant points come first. This array does not contain references to objects behind the observer and so it is allocated as new Array (400 x 801). It requires a mere 8 x 400 x 801 = 2.6 megabytes.]

So The Forest v19.4.10 allocates just over 70 Mbytes when it starts and then no longer has to reallocate this (in many pieces) every time a scene is drawn.

It is surely remarkable that the scene drawing is done in a couple of seconds even for the 400 metre horizon range. [* During that time the 'ahead' array is sorted too!]

[* When starting to draw a scene 'ahead' has to have all its elements set to undefined because as the observer moves and turns a varying number of points can lie ahead. I rely on the specification for Array.sort() which says that undefined elements get sorted to the end of an array.]

 - Major change for v21.12.21

* All of the points above marked with an asterisk or enclosed in square brackets are no longer true because of the change, which is as follows.

The construction of the Around object now does the sorting by distance instead of that having to be done each time a scene is drawn. A new property of forest.around is an array called xyd. For each of the 801 x 801 (x, y) positions in the surroundings of the observer an object is stored in xyd which comprises properties x, y, d and b, where d is distance and b is bearing from the central position. The xyd array is sorted on d only once, during the program's initialisation. It is then effectively a ground-covering list of points working outwards from the observer.

Notice that having d and b precalculated also eliminates many calculations of Math.sqrt() and Math.atan2() when drawing a scene. We now only do those calculations for points close to the observer (whose true position coordinates are not integers).

The array called ahead now becomes a property of the Scene object, forest.scene, but again it is allocated once by the Scene constructor so it does not keep being garbage-collected and reallocated. See scene.js for more details.

This change considerably speeds up scene drawing, particularly for the longer ranges.

Speaking of range: notice also that although the array xyd has been created and sorted with the full number, 801 x 801, of elements only the first Math.floor (Math.PI * range * range) elements of the array (sorted by increasing distance) need to be processed when drawing a scene.

 

 building.js

This file is new in version 18.10.24 when 3D buildings of definite size appeared instead of 2D images of buildings. The main point is that the new buildings are both plotted on the map and viewed in scenes, so it makes sense to have a single place where they are defined rather than risking differences between definitions in the map and scene files.

Buildings only appear centred on x and y coordinates which are multiples of 16 and for which the terrain type is TOWN.

Objects of type Building are only constructed when drawing a scene. For the map we only need to know whether a given position is at the centre of a building in order to be able to draw the square black symbol, so there is a simple function in this file for that test.

There is a separate script for when an observer is inside a building.

 

 control.js

A control is part of an orienteering course. It has a marker plus an (x, y) offset for showing its number beside it in a suitable position when the course is overlaid on the map. This offset has 2 small shortcomings:

 

 course.js

An important consideration here is the fact that storing the data for user-created orienteering courses in the browser's local storage is done as a single string in JSON format. That is what HTML5 offers. That loses object type information so the string must be parsed when it is reloaded and objects of our specific types (Course, Control, Marker) must be constructed again.

An orienteering course has a definite structure which can easily be described by JSON:

	Course title
	Start position
	For each control point marker:
		Position
		Id code
		Description
		Offset for placing number on map
	Finish position

So the following is a real example of such a course (as in the map below) encoded as JSON.

{"title":"Easy short, 1.6km","controls":[{"marker":{"x":608,"y":1932,"code":"KI"},"description":"water hole","offset":{"x":20,"y":0}},{"marker":{"x":552,"y":2019,"code":"GR"},"description":"boulder","offset":{"x":20,"y":10}},{"marker":{"x":719,"y":2142,"code":"RK"},"description":"mineshaft","offset":{"x":-5,"y":35}},{"marker":{"x":853,"y":2087,"code":"VH"},"description":"rootstock","offset":{"x":-5,"y":35}},{"marker":{"x":977,"y":2123,"code":"PR"},"description":"boulder","offset":{"x":-5,"y":35}},{"marker":{"x":1129,"y":1990,"code":"LO"},"description":"boulder","offset":{"x":-5,"y":35}},{"marker":{"x":1043,"y":1761,"code":"DT"},"description":"knoll","offset":{"x":20,"y":0}},{"marker":{"x":784,"y":1749,"code":"EH"},"description":"boulder","offset":{"x":-30,"y":15}}],"start":{"x":720,"y":1850},"finish":{"x":640,"y":1840}}

Map of a simple orienteering course  

 forest.js

This script acts as an interface between the HTML and the other scripts which are all object-oriented (this one is not). It handles events such as button clicks, drop-down selections and keypresses. For touch-screens (tablets or smart phones) the only gestures recognised by the program are taps which appear as mouse click events.

Generally the event handlers call methods of relevant objects in the other scripts to carry out the required actions.

This file contains an initialisation function, init (), which is invoked by the onload event of the HTML page. This function creates the principal objects for the program: one each of screen, terrain, map, scene, observer and plot3d. It adds relevant event listeners to the canvas. It also calls function loadCourses () in course.js which will load any courses which had been created by the user in a previous run and put in local storage.

init () then calls the map object to draw itself. It is important that the map is drawn first, to give time for the images to download which are required for drawing a scene. They start downloading in the Scene constructor.

The function loadImage () works asynchronously but we can check when it has finished. We test the corresponding object of type Image to see whether its loaded boolean has become true.

This script file also contains the function message () which is a general-purpose way of putting a multi-line message on the screen until the user does anything which causes the screen to be repainted. This is intended to be more user-friendly than JavaScript's native alert () which would always require the user to press OK to acknowledge it and would also darken the screen as a supposed security measure. However, it does have a possible disadvantage in that the user may not notice the message if pressing keys rapidly in succession, so sometimes alert () is still used.

 

 inside.js

This is new for version 18.11.7 when it first became possible to enter buildings. It is mainly responsible for drawing the interior of any building. This script is only loaded when an explorer manages to enter a building by giving the correct key code for the door.

An object of this type is attached to the observer as observer.inside and the fact that that property is not null ensures that the building interior is shown as the scene instead of outside in the forest.

The observer can move around inside the building and look up or down, just as in the forest scene. There are things drawn which might need to be examined more closely by moving up to them.

 

 manage.js

This is the extra script file for the subsidiary page, courses.html, for managing orienteering courses.

The function manage () is the initialisation function for this module. Then there are functions for the buttons on the course management page.

Return to the main page of The Forest is done simply by a call to the standard JavaScript function back (). It does not cause the original page to reload its course data after any changes made on the management page. This may cause users some confusion, so a note about manually refreshing is given on the course management page.

 

 map.js

One object of this type is constructed by the init () function in forest.js.

The map is drawn by a relatively simple scan of the canvas area. For each (x, y) position the terrain type, height and any point features are found by calling the terra () method of the terrain object. The complications in the process are as follows.

Contours are drawn smoothly and every fifth contour is thicker, as is standard for O-maps, to help interpretation. O-maps often have downward pointing tags on some contours as a further aid in removing ambiguity but I can see no way of doing that reliably with realistic speed. (Any ideas?)

I have used the latest IOF mapping standards (ISOM2017) as far as possible. That document specifies colours in the Pantone Matching System (PMS) and I have found the RGB equivalents from Adobe Photoshop.

Colours according to Photoshop CS4 (Colour library Pantone colour bridge CMYK EC)
Brown PMS 471 = #b84e1b
Yellow PMS 136 = #fdb73b
Blue PMS299 = #00aae7
Green PMS 361 = #0bb14d
Grey PMS 428 = #c5ccd1
Process purple = #9c3f98

Moorland (open ground where the running is slow) is shown on (lithographic) printed maps by using dot screens but I have guessed at an equivalent solid colour.

I believe my main departure from the standard is in using a 1-pixel white rim around every point symbol to help it stand out from its background. (This was a major consideration before I worked out how to have thin smooth contour lines.) So point symbols are drawn after everything else except the north lines (even after any course overlay).

 

 marker.js

This represents a potential marker flag for orienteering. One of these objects is constructed for every point feature found when drawing the scene ahead. It may or may not be displayed, depending on the role of the observer.

Each control on an orienteering course has one of these marker objects.

A marker has an (x, y) position and a two-letter control code. It also knows how to draw itself as a standard orange and white orienteering flag with the control code on it. If the observer's role is course planner, markers also display their positions on the flag, to help programmers make courses.

The code is determined by the terrain object whenever a point feature is found. The two letters are from an alphabet as letter numbers x % 26 and y % 26. (% is the modulus, or remainder, operation in JavaScript).

 

 mine.js

There is an animation as the explorer falls into a mine. There is currently just one level of mines under the entire terrain. The mines are deep enough to pass under lakes but that is all we need to know about depth.

The following code snippet shows how it is determined that the mine is open at a given (x, y) position, where x and y are multiples of 16. The second method shows how the mines are pseudorandomly generated from a very simple (and therefore fast) formula.


// There is 1 level of mines so open above only if the ground has a mineshaft
Mine.prototype.isOpenAbove = function (x, y)
{ for (var ix = x - 8; ix < x + 8; ix++)
  { for (var iy = y - 8; iy < y + 8; iy++)
    { if (forest.terrain.terra (ix, iy).feature === FEATURES.MINE) return true; }       
  }
  return false;
};

// Is a mine open at this position? Must be if there's a mineshaft on the ground
Mine.prototype.isOpen = function (x, y)
{ if (this.isOpenAbove (x, y)) return true;
  var u = Math.PI * 10000 * x * y;
  return (u - Math.floor (u)) > 0.4; 
  // Note threshold value 0.4 - could vary to give different openness/connectivity 
};

Here is a tiny fragment of map of the mine level. Cells are 16m square. The cells shown in red are below mineshafts. Although the user can move in any direction, just like above ground, cells are not connected diagonally (in earlier versions they were but movement was more restricted). This fragment shows a complete connected mine with 2 access shafts. At top left there is also a small mine with only one shaft. I don't know how far the other mines around the edges extend or whether they are all accessible.

An explorer may escape because there will be a ladder shown below any mineshaft. It is only necessary to get close to a ladder in order to be whisked up to ground level and positioned a few metres from the corresponding mineshaft.

The drawing of the scene in a mine has a major difference from scenes above ground because there is code to draw the wall images skewed, for a simple perspective effect. This involves a work area, to help in assembling the scene. This has its own hidden (never displayed) canvas element.

The display of the mines was completely redeveloped for version 19.6.11 but the map did not change. The new display involved some challenging considerations which will now be described in some detail.

A scene somewhere in the redeveloped mines looks like this:

This scene has several kinds of components drawn in perspective:

Walls are drawn using the skewHoriz () function in workarea.js. You may just see that more distant walls are darker. The darkening is done by a very similar technique to the hazy horizons above ground but using skewHoriz () instead of context.drawimage () and pixels are faded to black instead of towards a sky colour.

The list of components above indicates some problems that had to be tackled. The floor had to be drawn first and that uses the standard closed path stroking and filling methods of the 2D context of the canvas. Then the walls are drawn, from the furthest to the nearest, but these use skewHoriz () which does pixel reading and writing within image data, bypassing the graphics context. The ladder uses line drawing which could be done either by stroking in the graphics context or by pixel setting in the image data. Other objects use context.drawimage () for its ability to scale for distance and make use of transparency in the object images (PNG format). So there is some to-ing and fro-ing between context and image data. A further complication is that objects drawn in one part of the mine within visible range might after all be blotted out by walls closer to the observer. The following pseudocode indicates how all this was dealt with.



fill the screen canvas with black;

create an array called drawLater, to hold objects that may be visible to the observer;

scan in x and y (steps of 16) to find cells surrounding the observer within a given range;

make an array of cell objects, each having position of centre, distance from observer, whether open, 
  and the screen positions and distances of its 4 corners;

sort the array in descending order of distance from observer;

for each cell in the array (from furthest to nearest)
{
  if cell is open
  {
    draw floor tile using screen positions of the cell's 4 corners;

    draw ceiling light image above the centre of the cell;

    if there is an object in this cell
    { 
      get its screen position and scaled width and height;
      
      append to the array drawLater a reference to these data and the image of the object; 
      // we don't know yet whether it will really be seen or blocked by a wall
    }
  }
}

get the image data for the whole screen canvas;

in the workarea's hidden canvas get the image data for the wall image;

for each cell in the array (from furthest to nearest)
{
  if cell is solid (ie, not open)
  {
    sort the screen positions of the 4 corners in descending order of distance from observer;

    draw the wall connecting 3rd nearest to nearest corner;
	
    draw the wall connecting 2nd nearest to nearest corner;
    // wall drawing reads pixels from the hidden canvas image data 
    //   and writes them into the screen's image data
    // wall drawing is discussed further below
    // wall drawing also tests whether the wall hides any object 
    //   in drawLater and removes it if so
  }
  else if cell is open above
  {
    draw a ladder
    // because the ladder comprises straight lines it can be 
    //   drawn by setting pixels in the hidden canvas image
    //   data just using setPixel () in for-loops
  }
}

put the image data back into the screen canvas' context;

if there are any entries in drawLater use context.drawImage () to draw them;
// this will include the suggestion of a ceiling hole above any ladder

draw compass etc;


If a 16 x 16m cell is not open but solid rock then the observer will see up to 2 walls:

In this plan view the dashed lines indicate the observer's field of view, 45° to either side of the facing direction. The distances of the 4 corners of the cell are calculated and then the objects holding the data for each corner are sorted so that we know which are the nearest 3 corners. In the example shown we would then draw first the wall 3-1 (from left to right) and then the nearest wall, 1-2.

Then there was the question of how to draw walls which only partly appear in the scene and for which the other end may lie well behind the observer. When the observer is very close to a wall it becomes quite tricky. This is where some geometrical calculations were needed, for various possible cases.

The following diagrams for the possible cases each show a plan view with the observer, O, at the centre facing upwards (bearing known of course but arbitrary as far as these diagrams are concerned: upwards is generally not due north). The dashed lines show the field of view, 45° to either side. The solid line is a wall to be drawn, from left to right.

The cases are distinguished by considering the bearing differences between each end of the wall and the observer. A bearing difference is negative if the wall end is left of the straight ahead direction but positive if to the right. Bearing differencs are adjusted to lie in the range from -180 to +180 degrees. In the following (and in the code) db means a bearing difference.

Case 1: Both ends of the wall are within the field of view: Math.abs (db) < 45. No problem. The whole width of the wall image will be used in skewHoriz () (see workarea.js).

Case 2: One end of the wall is outside the field of view but still in front of the observer (the difference in bearings is less than 90°). Still no problem but calling skewHoriz () with x-scan limits saves time.

Case 3: Both ends of the wall are outside the field of view but still in front of the observer (the differences in bearings are greater than -90° for the left end and less than 90° for the right end). Still no problem but calling skewHoriz () with x-scan limits saves time.

Case 4: Both ends of the wall are outside the field of view and both are behind the observer: Math.abs (db) > 90. This is the easiest case because the wall is not drawn at all!

Case 5: Now it starts to get trickier because one end of the wall is in front of the observer but the other is behind. The wall passes in front of the observer and so must be drawn. When points lie behind the observer the result of the method for getting screen coordinates becomes nonsense so a geometrical calculation is needed to avoid this problem. My way of doing it will be given in detail after the rest of these case diagrams.

Case 5a: This is similar to case 5 but this time it is the left one of the two ends which is behind the observer and this makes the calculation different, as I will show.

b

Case 6: This is trickier still because although the wall passes across behind the observer, one end of it is still seen. This case is trickier to test for too because the bearing difference for the left end, that drawing will start from, is now greater than that of the right end.

b

Case 6a: Similar to case 6 but the other way round.

Geometrical solution for case 5

In this diagram angles and distances shown in black are known or easily calculated or chosen. The red values are initially unknown but x3 and y3 need to be calculated.

A wall to be drawn runs, in plan view, from point P1 to point P2. P1 is in front of the observer but P2 is behind and so its screen coordinates cannot be found from our usual method, Mine.getScreenXYD (). We need to find a point (x3, y3) part way along the wall which does not lie behind the observer, in order to get sensible screen coordinates for drawing the wall. Even if this third point is just outside the field of view it will suffice. Angle C can therefore be chosen to be anywhere between 45 or 90 degrees from the straight ahead line from the observer (the line pointing straight upwards in this plan).

At first sight there are too many unknowns for determining x3 and y3 but in fact that is not the case.

Two triangles are formed by the line at angle C, going from the observer to the unknown point. In each triangle we can use the law of sines: the ratio of the length of any side to the sine of the angle opposite to that side is a constant for the triangle. Therefore



	sin (C - db1) / d13 = sin (A) / d1  and also  sin (db2 - C) / (d12 - d13) = sin (B) / d2


What makes this soluble is that sin (180° - a) = sin (a), for any angle a. So in our diagram sin (A) = sin (B). Therefore



    sin A = d1.sin (C - db1) / d13 = d2.sin (db2 - C) / (d12 - d13)
	
	=>  d13 = d12.d1.sin (C - db1) / (d1.sin (C - db1) + d2.sin (db2 - C))


Once we have d13 we can find x3 and y3 as part way along the line from P1 to P2:



    x3 = x1 + (x2 - x1).d13 / d12  and   y3 = y1 + (y2 - y1).d13 / d12


where of course we would have already got (x2 - x1) and (y2 - y1) along the way to calculating d12. And there are other reorganisations that can easily be done to avoid recalculating the same terms, such as sin (C - db1), more than once.

Geometrical solution for case 5a

A very similar calculation may be done as for case 5 but the variables used are inevitably different. That is particularly because the wall is always drawn from left to right, so the starting point is the one behind the observer in this case. I leave the details as an exercise.

Geometrical solutions for cases 6 and 6a

These are essentially the same as for 5 and 5a. The crucial point this time is that identifying these two cases really needs to be done first, before testing for the other cases. This is partly because it may be necessary to swap ends: the one on the left may have the higher bearing difference.

The wall image

I photographed this in 2014 in County Durham, in a quarry by the road that goes south from Stanhope to Barnard Castle. Every scene in The Forest is a composite of my own photographs.

 

 mover.js

Objects of type Mover can move around (surprise!). To enable that, this file was added in version 19.3.29.

A new array forest.movers (in forest.js) contains references to all moveable objects, created by function createMovers () in mover.js.

Every time the user does something function moveStuff () is called, also in mover.js. As long as the role of the user is explorer, it calls the move () method of each of the movers. Each mover is restricted to a particular kind of terrain, so the first ones can only move along the roads near the starting position of the map.

One property of a mover is the bearing of the direction in which it is facing, so when it has to move it tries to go in that direction. If there is no terrain of the required kind in that direction it then randomly searches left or right in increasing amounts until it finds suitable terrain. This may result in it turning right round, which happens at the end of a road, for example.

Each mover has properties x and y for its position. Then when generating the terrain within method Terrain.terra (x, y) (see terrain.js) there is a test to see whether this.placed [x + ',' + y] is undefined. If not, that object property (which looks like an array element but is not) contains a reference to an entity at that position.

This relies on the optional way of representing object properties in JavaScript, where the name of the property looks like an array index of type String. Notice how the index string is built in the most compact but unambiguous way possible (x + y would be ambiguous: many possible value of x and y could give the same sum). The lookup is really done as a hash table, so it is very efficient.

When one of the movers moves from (x1, y1) to (x2, y2) the previous property of the object this.placed is deleted and a new one is created and given a reference to the mover:

	delete this.placed [x1 + ',' + y1];
	this.placed [x2 + ',' + y2] = mover;

In practice this is done by two methods of Terrain: place (x, y, mover) and remove (x, y).

This technique also enables helicopters to move from where they are first found to the place where the user lands them.

 

 observer.js

One object of this type is constructed by the init () function in forest.js. It represents the orienteer or explorer standing on the ground, with x and y position coordinates in metres and facing a bearing (b) in degrees clockwise from north. Given those 3 values it is possible to calculate everything that can be seen ahead: see scene.js.

It is important, following OOP principles, that the observer's properties x, y and b are only modified by methods of the one object of type Observer, forest.observer. Whenever b changes, another method, sincos(), calculates the sine and cosine of b and holds them as further properties of the observer: sinb and cosb. They may be needed several times and we do not want to keep recalculating them because sine and cosine are relatively time-consuming to calculate.

An observer also has a role, initially the general one of explorer. If the user changes this to be an orienteer there will be a course object selected for the observer too. Actions available and whether control markers are seen depend on the role.

 

 plot3d.js

One object of this type is constructed by the init () function in forest.js.

It draws an isometric view of the contours in the 300m square around the observer's current position. It does this by drawing vertical poles at every metre within the square, starting with the furthest side. The poles are striped at the contour interval.

This may not be the fastest way of doing it but it does automatically hide anything which should be hidden.

 

 point.js

This is sometimes a convenient object to construct to help geometrical calculations, mainly because it contains a useful method to get the distance between this and another point. However, unless such a method is required it is generally better not to construct objects of this type, on performance grounds. This is a general point: constructing objects takes time and at some stage the garbage collector will have to get rid of them again. So think carefully in all cases whether it is worth making objects, especially if large numbers of small ones will be needed. Sometimes the purity of object-based structure has to be compromised for performance and you will find several examples of this in my program.

 

 rgbmeter.js

This displays a meter in the top right corner of the scene display. The meter can be found on top of traffic cones underground in the mines, so there are many of them. Once the observer has it, no more copies will be seen. The meter has writing on it in the same cipher that was used in posters on the ground giving hints. Anyone who cracks the (fairly simple) cipher will se immediately what the meter displays. It is useful for unlocking doors on buildings and on the rocket.

 

 road.js

This file was new in version 19.2.19. Its function is described in the terrain section.

In orienteering terms each road is a track, shown as a dashed line on the map. But the term "track" has other connotations in programming so I used "road" to avoid misinterpretation.

The function which marks ground positions in the terrain is paveLine (). It is less efficient than Bresenham's famous line algorithm but it is simpler. It is less efficient because it marks some "pixels" (ground positions) more than once. That does not matter because it is only used during the initialisation of the program. When drawing maps or scenes a look-up of the marked position is all that is needed.

 

 rocket.js

A rocket can be found beside the road heading north from the initial location of the program. It is possible to enter the door of the rocket in the same way as entering buildings, if the key code is known. The observer is then taken for a ride, ending in surprising terrain.

 

 scene.js

One object of this type is constructed by the init () function in forest.js.

The constructor of the scene object initiates loading of the image resources. The images are in the PNG-8 format using transparency. Images are rectangular but the area surrounding a tree is transparent, for example.

Drawing the scene (the draw () method of this object) involves first scanning a square area around the (x, y) position of the observer. Objects out to a predefined range (initially 60m but alterable by the user in the HTML) are to be drawn, so a square of side 2 x range + 1 metres is scanned. The results are held in an array called around. As the points are scanned a check is done against the bearing of the observer, to find out whether each point lies within the visible angle, 45° either side of straight ahead. But because objects close to the observer but out to an angle 70° either side can affect the view, we really mark all points within the +/- 70 degree angle as being potentially ahead and these are all held in an array called ahead; this array includes the view angle and distance of each point.

This diagram represents the 2D array called around in the simplified case when the visible range is only 10 metres (the minimum we allow in the HTML is really 60m). The observer is at the centre, so the array has dimensions 21 x 21 (because 21 = 2 x range + 1). Clearly the size of the array goes up as the square of the range and computation time increases correspondingly. The blue dot in the centre of the diagram represents the observer and the thick blue line is the facing direction, in this case on a bearing of 120° (clockwise from north). The dashed lines either side represent the angle covered by the scene view, 45° either side of the facing direction. The solid thin lines are 70° either side. The centres of the red cells are outside the circular range and therefore need not be considered further.

Note that although the blue dot is shown in the centre of the central cell, the observer does not have integer coordinates. It cannot because it can move by fractions of a metre in x or y (taking into account sines and cosines). Rounded versions of the observer's coordinates are used as the basis for the square array.

The white and green cells in the diagram all get a reference to a ScenePoint object stored in them, containing the following information.

Each green cell potentially affects the scene and a reference to the same ScenePoint object information is appended to the 1-dimensional ahead array as each green cell is encountered.

[* (See the major change below.) Importantly, the ahead array is then sorted in descending order of distance (so that the most distant points come first). The ScenePoint prototype includes a method for defining the sort order. The scene can then be drawn from the back towards the observer so that nearer objects can automatically obscure farther ones.]

The around array is kept because it maintains the spatial relationship between neighbouring points: given x and y we can easily look up around [x + 1][y] for example. There is no such relationship between adjacent entries in the ahead array. This spatial relationship is needed when we come to tile the ground (next paragraph) and also to draw point features that are more than 1 metre across: knolls, mineshafts, ponds and man-made objects; for these we need to mark the positions around their centres so that trees do not grow out of them.

*Having sorted the ahead array we can start using it to draw, from the most distant points forward. First the whole scene is filled with sky colour (which depends on whether it is raining and whether the explorer has gone through a green door!). Then at a given point, several things are drawn in succession:

This example from one of my test programs (which switches off scene.showGround before drawing scenes) illustrates the tiling without the elliptical patches of ground cover:

Notice also how the trees are offset within the tiles.

One of the neat things about the standard 2D graphics context is that the drawImage () method not only takes parameters for where to place the top left corner of the image but also the required width and height, scaling the image very efficiently to fit. In our case there is a scale factor (fscale) formed from the distance of the point from the observer which is applied to every item that is drawn. So distant tiles, trees, etc are scaled down very effectively without my program having to do very much.

fscale = 5 / sp.d; ensures that images of objects 5 metres from the observer are drawn at their original size. When closer they are scaled up but further away they are scaled down.

A tile is drawn about every point for which both the x and y coordinates are odd. That is why the ScenePoint objects created during the scan around the observer contain a boolean indicating this fact. We use the around array to find the 4 neighbouring points (for which x and y are both even). We then get the distance and heights of those 4 corners of the tile. A method called getScreenXY () then does the perspective calculation to get the screen coordinates of each corner. A closed path is then created and filled to draw the tile.

NB: Since version 19.4.10 the arrays around [* and ahead] and the ScenePoint objects they point to are all constructed once at the start of the program (in forest.js) instead of every time they are needed for drawing a scene. See around.js for more information about this.

 - Major change for v21.12.21

Points marked with an asterisk or square brackets above are no longer quite true, as will be explained here. This corresponds to the major change in around.js.

The array forest.around.xyd is now created and sorted when the program starts. It has the full number, 801 x 801, of elements that would be needed for the greatest range setting of 400 metres. Only the first Math.floor (Math.PI * range * range) elements of the array (sorted by increasing distance) need to be processed when drawing a scene. By looping from that furthest element down to the 0 element we process from the furthest objects towards the observer. So there is no longer a need to do any sorting to display a scene and that saves a significant amount of time.

In that first loop, towards the observer, we check which ground positions will be visible and only put those into another array called ahead. This array is now a property of the scene object. It is allocated in the Scene constructor with the maximum number of elements it could possibly need. There is another scene property called nAhead which is set to 0 at the start of drawing and then incremented as each of the ground points is put into this array. It is done like this instead of pushing onto an initially empty array so that the array remains in place instead of being garbage-collected and having to be reallocated on each draw.

 

 scenepoint.js

Many objects of this type are constructed for drawing scenes. These objects are simply records containing values found in the square area around the observer, as described for the scene object.

These objects also have a method which defines the sorting order in descending order of distance.

Several extra properties can be attached to objects of this type during the drawing of a scene. This helps performance. For example, if the quite lengthy terrain calculation is done for this position then the result is set as a property for re-use rather than perhaps having to do the calculation again at some stage. That is the reason for the method getTerra () in this file, which first checks whether the property already exists.

 

 screen.js

This small script has a constructor for type Screen which gets the size of the canvas and a reference to its graphics context, for all other scripts to use.

It contains methods for direct access to pixels in the image displayed on the canvas.

(In hindsight it may have been better to call this file canvas.js and the type Canvas because it only refers to the active canvas element rather than the whole screen.)

 

 stargate.js

This is effectively level 3 of the treasure hunt. Its name may give a clue as to what happens but that is enough for now.

 

 stream.js

If the user types the w key when viewing the map this file has code for finding the path that water would take downhill from the user's current position and for drawing it on the map. The Stream constructor is invoked from forest.js in keydown (). The map causes the Stream object to be removed again after drawing it.

 

 terrain.js

One object of type Terrain is constructed by the init() function in forest.js. This is very similar to the original forest code dating from the 1980s, simply translated from Z80 assembler to JavaScript.

A detailed description of how it works is now on a separate page: forest_terrain.html

As of version 19.2.19 some tracks have been placed on the ground but these are not generated automatically. Instead a test version of the program has been used to set a series of (x, y) ground points manually. These are then used as the vertices of a path in the usual graphics sense. A new file road.js defines a Road as a list of points and the map can then use a draw () method of a road object. More interesting is the way the road is embedded in the terrain. The terrain object has a property placed which is initialised simply as a new Object (). Recall that in JavaScript there can appear to be two quite different ways of accessing the properties of an object: either as obj.propertyId or as obj ['propertyId'], like an array indexed by a string. Behind the scenes objects are implemented as hash tables. The property names as strings are hash keys, directly forming an address within the table (aka content-addressable memory). The point is that given a key we can very rapidly determine whether there is an entry in the table, with no searching involved. That is exploited here, using terrain.placed. The key (or property name string) is formed from the x and y ground coordinates. They are separated by a comma, so we test whether terrain.placed [x + ',' + y] contains a particular value. This is very fast and it does not involve preallocating an array for all possible x and y (which would be impossible anyway because the coordinates are unbounded). What really happens in the buildroad () function in road.js is (a) that a Road object is constructed containing the list of ground positions, for the map, and (b) the lines along that list are traversed to set terrain.placed [x + ',' + y] for every integer position within a certain distance of the line. Then scene.draw () can discover which ground tiles need to be shown as road (or track).

Note that the comma in the key string has a dual purpose. Firstly it causes conversion of integers x and y to strings for concatenation (and slightly faster than 'x' + x + 'y' + y which I first wrote). Secondly it ensures that, for example, x = 123 and y = 45 does not produce the same key as x = 12 and y = 345.

The same technique could be used to position any object at a required location or to indicate that an auto-generated feature has been moved, perhaps by the explorer. It does not want to be overdone because it goes against the initial principle of The Forest in having auto-generated terrain. The technique has been used to make helicopters stay where they land and not to be seen any longer where they started from. It has also been used (19.2.23) to plant a self-moving mystery object next to one of the tracks, an object which may see more action in future versions.

 

 timer.js

This uses the standard JavaScript setInterval () function to rewrite the digital time every 500ms in a span element on the HTML page.

A Timer object is constructed as a property of the Observer object so that split times for the orienteering course can be tabulated at the finish and so that the timer can be stopped.

The timer is only used if the observer's role is orienteer.

 

 workarea.js

This used to be in mine.js which is only used when an explorer falls into a mine. It is also now used by the newer inside.js that is only needed if the user enters a building.

The purpose of the workarea is to enable pixel data of various images to be read and written. The relevant image is first drawn into the workarea with context.drawImage () and then the pixel data array is obtained using context.getImageData ().

This file also contains a function skewHoriz () which uses variable vertical scaling to draw an image with horizontal perspective. This is done for the walls of the mines and for pictures displayed inside buildings.

The function works pixel by pixel on the image data, scanning with a variable scale factor from left to right. The scale factor at each side depends on the distance of that side from the observer and it is varied linearly from left to right. The constant horizontal scale factor is determined by the difference in the bearings of the two ends as seen from the observer. It is important that the scanning is done so that every pixel in the target space is visited, to leave no gaps, and for each one of those pixels we calculate the corresponding pixel position to read from the original rectangular image. Interpolation between pixels is not done, for speed.

The function tests whether each target pixel is within the target canvas, thereby cropping as necessary. A considerable speed up is obtained by first determining the relevant section of the source image from which to sample.

Complications arise when one or both of the sides of the target lie outside the visible scene, and perhaps even behind the observer. These are described in the section about mine.js.

 Test programs

I have a number of these in an unreleased copy of index.html that has been renamed tests.html and then been modified.

The onload attribute of the body element has become onload="testXXX ()" and within the body there is a script element containing several test functions that can be called in that way.

Each of the test functions must call init () in forest.js before doing anything else. Since version 19.2.19, when map orienting became possible, init () has been modified so that if it has any parameter it will not draw the map. This modification is only for testing purposes and it is necessary because the oriented map has to load as a dataURL taking unknown time and this breaks the processing sequence before we get to further code in the test function.

A typical test I would need would be when new behaviour occurs at a particular type of object. I first find an example of that type of object by scanning the map, jumping to the ground and moving until I can see it in the scene. Then note the coordinates and observer bearing in the status line below the display. Then my test function might be as follows.


  function testTileProblems () // Diagnose missing tiles
  {
    init (1); // Any parameter will do, see above
    var me = forest.observer;
    me.x =  -102.44;
    me.y = -82.46; 
    me.b = 123;
    var fs = forest.scene;
    fs.showGround = false;
    fs.showVeg = true;
    fs.showFeatures = true;
    fs.draw ();
  }

If you run this particular example you will see that on the very steepest hills there is occasionally a missing ground tile at the bottom of the scene (very close to the observer), showing the sky through a hole. This is a problem I am still trying to solve.

You may wonder why I keep doing things like creating the variables me and fs in this example. Partly this is to make file sizes smaller but there is another reason. I read some years ago that JavaScript interpreters can find local variables faster than properties of objects. So if an object property is needed several times in a function it is best to make a copy as a local variable first. I don't know whether this is still true. It may not be so important if JIT (Just In Time) compilation is being done. I have not tried to verify whether there is really a speed benefit. [When I get time perhaps...]

 Cones and scarecrows

These two objects were chosen for their colour. At a distance and through trees orienteers might mistake them for controls, as could happen in real life.

The scarecrow is on a stake in the ground and so I considered it to be fixed. It is therefore one of the four man-made objects, mapped with a black x. People can easily move cones and so they are not plotted on the map.

The presence of either object at a given location is calculated in the usual pseudo-random way in terrain.js and the cones do not move.

 Diversions for explorers

Although the primary purpose of The Forest is to help orienteers and others with contour interpretation on maps, it does include some games and side attractions ("diversions") for non-orienteers. These diversions become invisible and have no effects if the user's role is not explorer.

There are several such diversions, including the following, but more are added from time to time.

 Development environment

I am developing the program on a Microsoft Surface Book 2 running 64-bit Windows 10. I use Netbeans IDE (free Integrated Development Environment) with its HTML5/JavaScript plug-in kit, mainly because I already had it for Java programming. An IDE is not essential for this work but it does offer suggestions as I type and it does point out syntax errors immediately. Netbeans also very usefully has its own built-in localhost server that works automatically whenever an HTML file is "run".

When I am unsure about any JavaScript detail I use the Mozilla reference pages which I find more thorough and up-to-date than the old w3schools site. I test the program by loading my test version of the HTML file into the Firefox browser. Firefox has a very useful Web Console (under the Web developer menu, or key Ctrl+Shift+K) which gives me the script file name and the line number at which any run-time error is detected.

I upload the tested files to my web site using Filezilla FTP (free). To avoid possible problems with script caching in client browsers I increment a suffix letter on the name of each script file that changes for a new version. That way the user would only need to refresh the HTML page to ensure that all the correct script versions are loaded. (More recently I have edited the .htaccess file for my site to ask browsers not to cache HTML files.)

I have only tested The Forest in Firefox, Edge and Internet Explorer 11 on PC and Chrome on an Android smartphone. Timings for drawing are fine in all those browsers. I have been told that it works fine on a Kindle tablet running under FireOS (version unspecified but not very recent).

I would very much welcome feedback (gr<at>grelf<dot>net) on performance in other browsers: how long does each take to draw the initial scene, map and 3D plot, as shown in a status line below the graphics? Average of 3 readings please (times vary due to the browser doing other things).

 My JavaScript course

Link to course

 Finally

There are two principles in particular that I try to keep in mind when writing programs:

Next page