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. 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 frequently (2018).

 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 beforehand and set to a constant for multiple re-use: this is an important general principle for speed.

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.

 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. 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

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.

 

 Perspective calculation

The following method is in screen.js and it calculates the screen position of a point on the ground at location coordinates (x, y), plotting it in perspective as seen by the observer. Any object at that location can then be plotted relative to that position (typically standing on it but scaled for distance from the observer). I have annotated the method here with extra // comments.


  Scene.prototype.getScreenXY = function (x, y)
  { var sp = this.around [x][y];                         
    // see scene.js: sp has distance and bearing from observer
    var ht = this.ft.terra (x, y).height;                
    // see terrain.js: ft is forest.terrain
    var zz = sp.d * Observer.prototype.COSDEG [sp.b];
    // Look up cosine from b degrees
    if (zz < 1) zz = 1; // Avoid points behind me       
    // Perhaps a fudge, playing safe
    var yy = (ht - this.ht0) * this.YSCALE - this.ME_HT; 
    // ht0 is the ground height of the observer's location
    // ME_HT is how tall the observer (constant)
    var xx = sp.d * Observer.prototype.SINDEG [sp.b];
    // Perspective:
    var fRatio = this.FF / zz;
    var sx = fRatio * xx + this.wd2; // Relative to screen centre, wd2
    var sy = this.htBase - fRatio * yy;
    // htBase is where ground at same height as observer would appear
    return {x:sx, y:sy};
    // Return object with two properties, screen x and y
  };

The diagram below is labelled according to variables in the method. The bearing sp.b is relative to the direction in which the observer is looking, because the screen is angled for that relative to the ground coordinates. FF is like a camera's focal length, the supposed distance from the observer to the screen. The dashed triangle on the screen at FF is geometrically similar to that at the actual distant plane zz so the sides are all in the same ratio.

 

 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 half the distance to the horizon then the object is assigned a fog factor which rises linearly from 0 at half 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
  {
    return '#' + r.toString (16) + g.toString (16) + b.toString (16); 
  }

(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 the 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 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 the original unfogged version if the fogged one 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 fog appears 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);
        else this.g2.drawImage (im, x, y, wd, ht); // fall back to unfogged version for now
      }
    }
  };

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

 Programming notes by file

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 but all other resources may be cached.

 index.html

This is the single 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 above 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 is used by the scripts, so that they should work on all platforms (so we 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. [4/7/18: It is intended to improve the layout. ]

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";

At the end of this HTML page there is a second canvas element which is never displayed (its style attribute prevents it). See the section below for workarea.js for more information about this.

 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 functions loadImage () and loadScript () work asynchronously but we can check when they have finished. In the case of loading an image we test the corresponding object of type Image to see whether its loaded boolean has become true. For loading a script we supply some code which is to be executed once loading is complete; usually this is a function call which invokes something in that newly loaded script. The main example of this in The Forest occurs when an explorer gets too close to a mineshaft and falls down it: mine.js is then loaded. This never occurs for orienteers.

This script 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.

 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.)

 workarea.js

This used to be in mine.js which is only loaded when an explorer falls into a mine. It is also used by the newer inside.js that similarly loads only when needed. So it was decided to make this small file part of the initially loaded set of scripts.

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 called 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.

 observer.js

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

Among other things, the constructor of the Observer object creates 2 properties which are arrays of sines and cosines at integer degree values from -360 to +360. Looking up these values is quicker than calculating them every time (see verification section below). The negative range saves time worrying about negative values when angles have been subtracted.

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.

 terrain.js

One object of this type is constructed by the init () function in forest.js. This is almost exactly the same as in the original forest dating from the 1980s, simply translated from Z80 assembler to JavaScript.

The starting point is a 1-dimensional profile of length 256 for which the data are stored as a literal array of integer values. Charted in a spreadsheet it looks like this:

The profile is a periodic structure so the array is indexed by the remainder of some number (p, say) when divided by 256 to get the height of the terrain. The number p is a function of the x, y coordinates of a point.

  height = profile [p % 256]

p is obtained essentially by summing various multiples of x and y (though it is slightly more complicated than this):

  p = sum (a [i] * x + b [i] * y)

Effectively this is adding together several copies of the profile at different amplitudes and various angles in the x-y plane. This is analogous to a Fourier series where a number of sine waves are added at different amplitudes and phases to create the shape of any desired function.

If the height is below a certain fixed value there is deemed to be a lake. Update June 2018: the lake height slowly increases when it rains, reducing back to the original fixed value when the sun shines.

A similar thing is done for determining terrain types (thicket, moor, etc), using the same profile but different parameter arrays a and b. If the result lies within a certain range of values, the terrain is of a certain type. Because the parameters are different the patches of vegetation do not follow the contour shapes.

Height is calculated using the full floating point values for x and y, because the observer is not necessarily standing exactly on the whole-metre x-y grid and we want height to be as smooth a function as possible. Vegetation and point features use only the integer parts of x and y however.

The existence of a point feature (boulder, pond, etc) at a given position does also involve the profile. Then a relatively simple function of x and y is tested for certain values within a range of bits.

Notice that the map is generated point by (x, y) point. There is no consideration of how neighbouring points may be related and therefore there are no linear features such as paths or streams. In fact it would be much more difficult to generate a map containing such features. Only the vegetation boundaries can be considered as linear features. One of the aims of The Forest is to help orienteers learn to navigate by using the contour information, so the absence of paths to follow is considered to be a good thing. Making the contours form narrow continuous lines was not so easy and we consider that in the map section.

 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 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).

 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.

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.

[ Repeat of pseudo-random bit patterns, in different words:

When drawing trees and other features there are (or will be) several different images for each type of object, to give some variety in the scene. They are pseudo-randomly selected, by which I mean that although the selection appears to be random it will always be the same at any given (x, y) position. So if you see a particular kind of boulder at a certain spot it will always be the same type when you revisit. Equally, the variety of tree at any given spot in a wood remains the same as you move about. This is achieved with a variable called variant, created like this:

  var variant = 0x3 & Math.round (this.PI10000 * x * y);

this.PI10000 is set as a constant property of the scene object when the object is first constructed. It is Math.PI x 10000 but we do not want to do that multiplication every time we want to use it, so it is set up first as a constant.

The digits of pi have no predictable pattern to them and so they can be considered for our purposes to be random. pi x 10000 simply shifts the digits up by a few places so there are several before the decimal point: 31415.926... Multiplying this by both the x and y coordinate values results in a new pattern of random digits but a pattern that is always the same for any given x and y. The final part of the formula for our variant is a bit-wise AND with the number 3 (written as a hexadecimal number simply to emphasise that a bit-wise operation is being done). In other words we look at only the least significant 2 bits of the integer part of the result of the multiplications. This can have 4 possible values: 00, 01, 10 or 11 and so we can select one of 4 image variants to display. If at some stage there were to be more than 4 images for boulders (or whatever), this could be modified to look at 3 bits (8 possibilities) by ANDing with 7 instead of 3.

A very similar thing is done when positioning a tree on top of a tile: the method getOffsetScreenXY () offsets the position by a pseudo-random amount within the tile before calculating the screen coordinates for the image. This is to ensure that trees are not always in dead straight rows. ]

 scenepoint.js

Many objects of this type are constructed during the drawing of the scene, and then discarded again. 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.

 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.

 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.

 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).

 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 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.

MORE TO COME

 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.

 plot3d.js

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

MORE TO COME

 stream.js

MORE TO COME

 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.

 mine.js

To keep the main code of The Forest smaller, this script is only loaded if an explorer falls down a mineshaft. It is of no interest to orienteers, who are immune to such accidents!

There is an animation as the explorer falls into a mine. There is (at present) just one level of mines under the entire terrain. It comprises a set of small caverns which may be connected in any of 8 directions: N, NE, E, etc. The caverns are 16m apart. The walls of a cavern show whether there is a connecting tunnel in each direction. In moving forward there is a brief animation zooming the scene towards the relevant tunnel.

Most mineshafts are connected to others via these tunnels. An explorer may escape because there will be a ladder shown below any mineshaft other than the one originally fallen down. Some mineshafts have no such connections and in that unlucky case the explorer is doomed.

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 new object type, WorkArea, to help in assembling the scene. This is associated with a hidden (never displayed) second canvas element in the HTML page.

It is intended to add more action after the treasure chest has been found.

 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 described above plus one new one: manage.js

 manage.js

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.

 Test programs

I have a number of these in a 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.

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 ();
    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, so any help would be appreciated (optimistic, I know).

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 a more important 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.

As of June 2018 there are 3 such diversions, as follows, but it is intended to think up some more.

 Development environment

I am developing the program on a Microsoft Surface Book 2 running 64-bit Windows 10. I use Netbeans 8.2 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. 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 the HTML file into the Firefox Quantum browser. I do not use a localhost server (no need). 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

 

 Verifying look-up speed

I wanted to verify my statement that looking up sines and cosines is faster than calculating them. It seems obvious but is it true in the browser JavaScript environment?

I found out that calculating (including conversion from degrees to radians) takes about 6 times as long as looking up. So it really is worthwhile pre-calculating trigonometric functions when possible.

The code I used for verifying this is below. I called it at the end of the constructor for the observer. There are a few complicating factors to take into account:

  1. It is necessary to put the results in arrays rather than assigning to a single variable because the JavaScript Engine (JSE) might be able to optimise the assignments away and just do the last iteration of the loop.
  2. The arrays must be pre-allocated, otherwise timings will include memory management operations as the arrays get bigger.
  3. The calls to Math.random() are also to avoid the JSE noticing that exactly the same operation is being done every time, so it can be optimised away.
  4. Then we should check how long the random calls take.

JSEs are now very clever at optimisation and of course that is why so much detail can be shown in The Forest in real time.


  var N = 1000000;
  var i, j = new Array (N), k, l = new Array (N), s, t0, t1;
  var deg2rad = Math.PI / 180;
  t0 = new Date ().getTime (); // ms

  for (i = 0; i < N; i++) 
  { 
    j [i] = Observer.prototype.SINDEG [Math.round (67 + Math.random())]; 
  }

  t1 = new Date ().getTime();
  var dt1 = t1 - t0;
  s = "Look up: " + dt1 + "ms<br/>";
  t0 = new Date ().getTime ();

  for (k = 0; k < N; k++) 
  {
    l [i] = Math.sin ((67 + Math.random()) * deg2rad);
  }
  
  t1 = new Date ().getTime();
  var dt2 = t1 - t0;
  s += "Calculate: " + dt2 + "ms<br/>Ratio: " + (dt2 / dt1) + "<br/>";
  t0 = new Date ().getTime ();

  for (i = 0; i < N; i++) 
  {
    j [i] = Math.random();
  }

  t1 = new Date ().getTime();
  s += "Random: " + (t1 - t0) + "ms<br/>";
  document.getElementById ("results").innerHTML = s;
Next page