Skip to content Skip to sidebar Skip to footer

Prevent Javascript From Running Again if Back Spacing on Page

Chapter 16Project: A Platform Game

All reality is a game.

Picture of a game character jumping over lava

Much of my initial fascination with computers, similar that of many nerdy kids, had to do with computer games. I was fatigued into the tiny simulated worlds that I could manipulate and in which stories (sort of) unfolded—more, I suppose, considering of the manner I projected my imagination into them than because of the possibilities they actually offered.

I don't wish a career in game programming on anyone. Much like the music manufacture, the discrepancy betwixt the number of eager young people wanting to piece of work in it and the bodily demand for such people creates a rather unhealthy environment. But writing games for fun is amusing.

This affiliate will walk through the implementation of a small platform game. Platform games (or "jump and run" games) are games that expect the histrion to move a effigy through a world, which is normally 2-dimensional and viewed from the side, while jumping over and onto things.

The game

Our game will exist roughly based on Nighttime Blue past Thomas Palef. I chose that game because it is both entertaining and minimalist and considering it tin exist congenital without as well much code. It looks like this:

The game Dark Blue

The dark box represents the histrion, whose task is to collect the yellow boxes (coins) while avoiding the red stuff (lava). A level is completed when all coins have been nerveless.

The player can walk around with the left and right arrow keys and can leap with the upward pointer. Jumping is a specialty of this game character. It can reach several times its own height and can modify management in midair. This may not be entirely realistic, just information technology helps give the player the feeling of beingness in straight control of the on-screen avatar.

The game consists of a static background, laid out similar a grid, with the moving elements overlaid on that groundwork. Each field on the grid is either empty, solid, or lava. The moving elements are the player, coins, and sure pieces of lava. The positions of these elements are non constrained to the grid—their coordinates may be fractional, allowing smooth motion.

The technology

We will apply the browser DOM to display the game, and we'll read user input by handling cardinal events.

The screen- and keyboard-related code is simply a small part of the piece of work we need to practise to build this game. Since everything looks similar colored boxes, drawing is simple: we create DOM elements and use styling to requite them a groundwork colour, size, and position.

We can correspond the background as a tabular array since information technology is an unchanging grid of squares. The gratis-moving elements tin can be overlaid using admittedly positioned elements.

In games and other programs that should animate graphics and respond to user input without noticeable delay, efficiency is important. Although the DOM was non originally designed for high-functioning graphics, information technology is really improve at this than you would expect. You saw some animations in Chapter 14. On a modern machine, a unproblematic game like this performs well, even if nosotros don't worry about optimization very much.

In the next chapter, we volition explore another browser technology, the <canvas> tag, which provides a more traditional style to draw graphics, working in terms of shapes and pixels rather than DOM elements.

Levels

We'll want a human-readable, man-editable way to specify levels. Since it is okay for everything to start out on a filigree, we could use big strings in which each grapheme represents an chemical element—either a part of the background filigree or a moving element.

The program for a small level might wait similar this:

          let          simpleLevelPlan          =          `          ......................          ..#................#..          ..#..............=.#..          ..#.........o.o....#..          ..#.@......#####...#..          ..#####............#..          ......#++++++++++++#..          ......##############..          ......................`;

Periods are empty space, hash (#) characters are walls, and plus signs are lava. The player'due south starting position is the at sign (@). Every O graphic symbol is a coin, and the equal sign (=) at the top is a cake of lava that moves back and forth horizontally.

We'll back up 2 boosted kinds of moving lava: the pipage character (|) creates vertically moving blobs, and v indicates dripping lava—vertically moving lava that doesn't bounciness back and forth just only moves down, jumping dorsum to its start position when it hits the floor.

A whole game consists of multiple levels that the player must complete. A level is completed when all coins have been collected. If the histrion touches lava, the electric current level is restored to its starting position, and the thespian may effort again.

Reading a level

The following class stores a level object. Its argument should exist the string that defines the level.

          form          Level          {          constructor(plan) {          permit          rows          =          plan.trim().split("\due north").map(l          =>          [...          l]);          this.pinnacle          =          rows.length;          this.width          =          rows[0].length;          this.startActors          =          [];          this.rows          =          rows.map((row,          y)          =>          {          return          row.map((ch,          x)          =>          {          let          blazon          =          levelChars[ch];          if          (typeof          type          ==          "cord")          return          blazon;          this.startActors.push button(          blazon.create(new          Vec(10,          y),          ch));          return          "empty";       });     });   } }

The trim method is used to remove whitespace at the start and end of the programme cord. This allows our example programme to first with a newline so that all the lines are directly below each other. The remaining string is separate on newline characters, and each line is spread into an array, producing arrays of characters.

So rows holds an array of arrays of characters, the rows of the program. We tin derive the level'southward width and height from these. Just nosotros must still divide the moving elements from the background grid. We'll call moving elements actors. They'll be stored in an array of objects. The background will be an array of arrays of strings, holding field types such as "empty", "wall", or "lava".

To create these arrays, we map over the rows and then over their content. Remember that map passes the assortment alphabetize as a second argument to the mapping function, which tells us the x- and y-coordinates of a given character. Positions in the game will exist stored as pairs of coordinates, with the top left being 0,0 and each background square existence 1 unit high and wide.

To translate the characters in the plan, the Level constructor uses the levelChars object, which maps background elements to strings and histrion characters to classes. When type is an histrion class, its static create method is used to create an object, which is added to startActors, and the mapping function returns "empty" for this background square.

The position of the actor is stored as a Vec object. This is a two-dimensional vector, an object with x and y backdrop, as seen in the exercises of Affiliate vi.

Equally the game runs, actors will end up in different places or even disappear entirely (every bit coins exercise when nerveless). We'll utilise a Land class to track the country of a running game.

          grade          State          {          constructor(level,          actors,          status) {          this.level          =          level;          this.actors          =          actors;          this.status          =          condition;   }          static          starting time(level) {          render          new          State(level,          level.startActors,          "playing");   }          get          player() {          return          this.actors.find(a          =>          a.type          ==          "player");   } }

The status holding will switch to "lost" or "won" when the game has ended.

This is again a persistent information structure—updating the game state creates a new state and leaves the sometime one intact.

Actors

Player objects correspond the current position and state of a given moving element in our game. All histrion objects conform to the same interface. Their pos property holds the coordinates of the element'south pinnacle-left corner, and their size holding holds its size.

And then they have an update method, which is used to compute their new state and position afterwards a given fourth dimension stride. Information technology simulates the matter the actor does—moving in response to the arrow keys for the player and bouncing back and forth for the lava—and returns a new, updated actor object.

A blazon belongings contains a string that identifies the blazon of the thespian—"player", "coin", or "lava". This is useful when cartoon the game—the look of the rectangle drawn for an histrion is based on its blazon.

Actor classes take a static create method that is used by the Level constructor to create an actor from a character in the level plan. It is given the coordinates of the character and the character itself, which is needed because the Lava course handles several different characters.

This is the Vec grade that we'll utilize for our two-dimensional values, such as the position and size of actors.

          class          Vec          {          constructor(x,          y) {          this.x          =          x;          this.y          =          y;   }          plus(other) {          render          new          Vec(this.x          +          other.10,          this.y          +          other.y);   }          times(cistron) {          return          new          Vec(this.x          *          factor,          this.y          *          factor);   } }

The times method scales a vector by a given number. It volition be useful when we need to multiply a speed vector by a time interval to get the distance traveled during that fourth dimension.

The unlike types of actors go their ain classes since their behavior is very unlike. Allow's define these classes. We'll get to their update methods later.

The actor class has a belongings speed that stores its current speed to simulate momentum and gravity.

          form          Player          {          constructor(pos,          speed) {          this.pos          =          pos;          this.speed          =          speed;   }          become          type() {          return          "player"; }          static          create(pos) {          return          new          Thespian(pos.plus(new          Vec(0,          -          0.five)),          new          Vec(0,          0));   } }          Player.prototype.size          =          new          Vec(0.8,          ane.v);

Because a thespian is 1-and-a-half squares high, its initial position is set to exist half a square above the position where the @ graphic symbol appeared. This style, its bottom aligns with the bottom of the square it appeared in.

The size property is the same for all instances of Player, and then we shop information technology on the prototype rather than on the instances themselves. Nosotros could have used a getter like blazon, but that would create and return a new Vec object every time the property is read, which would be wasteful. (Strings, beingness immutable, don't have to be re-created every time they are evaluated.)

When constructing a Lava actor, we need to initialize the object differently depending on the grapheme it is based on. Dynamic lava moves along at its current speed until it hits an obstacle. At that signal, if it has a reset property, it volition jump back to its start position (dripping). If it does not, it will capsize its speed and keep in the other direction (bouncing).

The create method looks at the graphic symbol that the Level constructor passes and creates the appropriate lava player.

          class          Lava          {          constructor(pos,          speed,          reset) {          this.pos          =          pos;          this.speed          =          speed;          this.reset          =          reset;   }          go          type() {          return          "lava"; }          static          create(pos,          ch) {          if          (ch          ==          "=") {          return          new          Lava(pos,          new          Vec(2,          0));     }          else          if          (ch          ==          "|") {          return          new          Lava(pos,          new          Vec(0,          2));     }          else          if          (ch          ==          "v") {          return          new          Lava(pos,          new          Vec(0,          three),          pos);     }   } }          Lava.epitome.size          =          new          Vec(i,          one);

Coin actors are relatively simple. They by and large only sit in their place. But to liven upwardly the game a petty, they are given a "wobble", a slight vertical back-and-forth motility. To runway this, a coin object stores a base position besides as a wobble holding that tracks the phase of the bouncing movement. Together, these determine the coin's actual position (stored in the pos property).

          form          Coin          {          constructor(pos,          basePos,          wobble) {          this.pos          =          pos;          this.basePos          =          basePos;          this.wobble          =          wobble;   }          get          type() {          render          "coin"; }          static          create(pos) {          let          basePos          =          pos.plus(new          Vec(0.2,          0.1));          return          new          Coin(basePos,          basePos,          Math.random()          *          Math.PI          *          2);   } }          Coin.epitome.size          =          new          Vec(0.6,          0.6);

In Chapter xiv, we saw that Math.sin gives usa the y-coordinate of a bespeak on a circle. That coordinate goes back and along in a smooth waveform every bit we movement along the circumvolve, which makes the sine function useful for modeling a wavy move.

To avoid a situation where all coins motility up and down synchronously, the starting phase of each coin is randomized. The period of Math.sin'southward wave, the width of a wave it produces, is 2π. We multiply the value returned past Math.random by that number to give the money a random starting position on the moving ridge.

We tin can now define the levelChars object that maps plan characters to either background grid types or actor classes.

          const          levelChars          =          {          ".":          "empty",          "#":          "wall",          "+":          "lava",          "@":          Player,          "o":          Coin,          "=":          Lava,          "|":          Lava,          "v":          Lava          };

That gives us all the parts needed to create a Level case.

          let          simpleLevel          =          new          Level(simpleLevelPlan);          console.log(`${          simpleLevel.width          }          past ${          simpleLevel.meridian          }          `);        

The task ahead is to display such levels on the screen and to model time and motion within them.

Encapsulation every bit a burden

Most of the code in this chapter does non worry about encapsulation very much for two reasons. First, encapsulation takes extra effort. It makes programs bigger and requires additional concepts and interfaces to be introduced. Since there is only so much code y'all can throw at a reader before their eyes glaze over, I've made an endeavor to go along the plan pocket-size.

Second, the various elements in this game are so closely tied together that if the behavior of one of them changed, information technology is unlikely that any of the others would be able to stay the aforementioned. Interfaces betwixt the elements would end up encoding a lot of assumptions most the way the game works. This makes them a lot less effective—whenever yous change one part of the arrangement, you nonetheless have to worry about the way information technology impacts the other parts because their interfaces wouldn't cover the new situation.

Some cutting points in a organisation lend themselves well to separation through rigorous interfaces, but others don't. Trying to encapsulate something that isn't a suitable boundary is a sure way to waste a lot of energy. When you are making this mistake, you lot'll ordinarily notice that your interfaces are getting awkwardly big and detailed and that they demand to be changed oftentimes, as the programme evolves.

There is one thing that nosotros will encapsulate, and that is the drawing subsystem. The reason for this is that nosotros'll brandish the same game in a different way in the next chapter. Past putting the drawing behind an interface, we can load the same game programme in that location and plug in a new display module.

Drawing

The encapsulation of the drawing code is washed by defining a display object, which displays a given level and land. The display type we ascertain in this affiliate is called DOMDisplay because it uses DOM elements to evidence the level.

We'll be using a style sheet to set the bodily colors and other fixed properties of the elements that make upward the game. It would likewise exist possible to directly assign to the elements' way belongings when we create them, simply that would produce more verbose programs.

The following helper role provides a succinct fashion to create an element and give it some attributes and child nodes:

          part          elt(name,          attrs,          ...          children) {          let          dom          =          certificate.createElement(proper name);          for          (let          attr          of          Object.keys(attrs)) {          dom.setAttribute(attr,          attrs[attr]);   }          for          (permit          child          of          children) {          dom.appendChild(kid);   }          return          dom; }

A brandish is created by giving information technology a parent chemical element to which it should suspend itself and a level object.

          class          DOMDisplay          {          constructor(parent,          level) {          this.dom          =          elt("div", {class:          "game"},          drawGrid(level));          this.actorLayer          =          null;          parent.appendChild(this.dom);   }          clear() {          this.dom.remove(); } }

The level's background filigree, which never changes, is drawn once. Actors are redrawn every time the display is updated with a given state. The actorLayer property will be used to rails the element that holds the actors so that they tin can be easily removed and replaced.

Our coordinates and sizes are tracked in grid units, where a size or distance of 1 means one filigree block. When setting pixel sizes, we will have to calibration these coordinates upward—everything in the game would be ridiculously pocket-sized at a unmarried pixel per square. The scale constant gives the number of pixels that a single unit takes up on the screen.

          const          calibration          =          20;          function          drawGrid(level) {          return          elt("table", {          grade:          "background",          way:          `width: ${          level.width          *          scale          }          px`          },          ...          level.rows.map(row          =>          elt("tr", {style:          `height: ${          scale          }          px`},          ...          row.map(blazon          =>          elt("td", {class:          type})))   )); }

As mentioned, the groundwork is fatigued as a <table> element. This nicely corresponds to the structure of the rows property of the level—each row of the grid is turned into a tabular array row (<tr> element). The strings in the grid are used as class names for the tabular array prison cell (<td>) elements. The spread (triple dot) operator is used to laissez passer arrays of child nodes to elt as separate arguments.

The following CSS makes the table look like the background we want:

          .background          {          background:          rgb(52,          166,          251);          tabular array-layout:          stock-still;          border-spacing:          0;              }          .background          td          {          padding:          0;                     }          .lava          {          groundwork:          rgb(255,          100,          100); }          .wall          {          groundwork:          white;              }

Some of these (table-layout, border-spacing, and padding) are used to suppress unwanted default beliefs. We don't want the layout of the table to depend upon the contents of its cells, and we don't want space betwixt the table cells or padding inside them.

The groundwork dominion sets the background color. CSS allows colors to be specified both as words (white) or with a format such as rgb(R, G, B), where the red, green, and bluish components of the colour are separated into 3 numbers from 0 to 255. So, in rgb(52, 166, 251), the blood-red component is 52, green is 166, and blueish is 251. Since the blue component is the largest, the resulting color will be blueish. You can meet that in the .lava dominion, the first number (cerise) is the largest.

Nosotros draw each thespian past creating a DOM element for it and setting that element's position and size based on the actor'due south properties. The values have to be multiplied by scale to go from game units to pixels.

          function          drawActors(actors) {          return          elt("div", {},          ...          actors.map(histrion          =>          {          let          rect          =          elt("div", {class:          `histrion ${          player.type          }          `});          rect.way.width          =          `${          actor.size.x          *          calibration          }          px`;          rect.manner.top          =          `${          actor.size.y          *          scale          }          px`;          rect.mode.left          =          `${          actor.pos.x          *          scale          }          px`;          rect.style.peak          =          `${          actor.pos.y          *          scale          }          px`;          return          rect;   })); }

To requite an element more than than ane form, nosotros split up the class names by spaces. In the CSS code shown adjacent, the actor class gives the actors their accented position. Their type name is used equally an extra class to requite them a color. We don't have to ascertain the lava form again because we're reusing the class for the lava grid squares we defined before.

          .actor          {          position:          absolute;            }          .coin          {          background:          rgb(241,          229,          89); }          .role player          {          background:          rgb(64,          64,          64);   }

The syncState method is used to make the display bear witness a given state. It offset removes the old actor graphics, if any, and and so redraws the actors in their new positions. It may be tempting to try to reuse the DOM elements for actors, only to make that piece of work, we would demand a lot of additional bookkeeping to associate actors with DOM elements and to make sure we remove elements when their actors vanish. Since in that location volition typically be only a handful of actors in the game, redrawing all of them is not expensive.

          DOMDisplay.paradigm.syncState          =          function(state) {          if          (this.actorLayer)          this.actorLayer.remove();          this.actorLayer          =          drawActors(state.actors);          this.dom.appendChild(this.actorLayer);          this.dom.className          =          `game ${          land.status          }          `;          this.scrollPlayerIntoView(state); };

Past adding the level'southward current status as a class proper noun to the wrapper, we tin can style the histrion histrion slightly differently when the game is won or lost by calculation a CSS rule that takes event simply when the player has an antecedent element with a given class.

          .lost          .player          {          groundwork:          rgb(160,          64,          64); }          .won          .player          {          box-shadow:          -4px          -7px          8px          white,          4px          -7px          8px          white; }

After touching lava, the player's color turns dark cherry, suggesting scorching. When the last coin has been nerveless, we add two blurred white shadows—ane to the top left and ane to the acme right—to create a white halo effect.

We can't presume that the level e'er fits in the viewport—the element into which we describe the game. That is why the scrollPlayerIntoView call is needed. Information technology ensures that if the level is protruding outside the viewport, we roll that viewport to make sure the player is near its center. The post-obit CSS gives the game'due south wrapping DOM element a maximum size and ensures that anything that sticks out of the chemical element's box is not visible. Nosotros also requite it a relative position so that the actors inside it are positioned relative to the level's pinnacle-left corner.

          .game          {          overflow:          hidden;          max-width:          600px;          max-peak:          450px;          position:          relative; }

In the scrollPlayerIntoView method, we find the histrion'due south position and update the wrapping element'southward whorl position. We change the scroll position past manipulating that chemical element's scrollLeft and scrollTop properties when the player is likewise close to the edge.

          DOMDisplay.epitome.scrollPlayerIntoView          =          part(state) {          permit          width          =          this.dom.clientWidth;          let          height          =          this.dom.clientHeight;          let          margin          =          width          /          3;              let          left          =          this.dom.scrollLeft,          right          =          left          +          width;          permit          pinnacle          =          this.dom.scrollTop,          lesser          =          top          +          height;          let          player          =          country.role player;          let          heart          =          player.pos.plus(histrion.size.times(0.v))                          .times(scale);          if          (eye.x          <          left          +          margin) {          this.dom.scrollLeft          =          center.x          -          margin;   }          else          if          (center.x          >          right          -          margin) {          this.dom.scrollLeft          =          center.x          +          margin          -          width;   }          if          (center.y          <          meridian          +          margin) {          this.dom.scrollTop          =          center.y          -          margin;   }          else          if          (center.y          >          lesser          -          margin) {          this.dom.scrollTop          =          center.y          +          margin          -          peak;   } };

The way the histrion's center is found shows how the methods on our Vec blazon allow computations with objects to be written in a relatively readable fashion. To observe the actor'due south centre, nosotros add its position (its top-left corner) and half its size. That is the center in level coordinates, but we need it in pixel coordinates, so we then multiply the resulting vector by our brandish scale.

Next, a series of checks verifies that the player position isn't outside of the allowed range. Note that sometimes this volition set nonsense scroll coordinates that are below cipher or beyond the chemical element's scrollable surface area. This is okay—the DOM will constrain them to acceptable values. Setting scrollLeft to -10 will cause it to become 0.

Information technology would take been slightly simpler to ever attempt to curl the player to the center of the viewport. Just this creates a rather jarring upshot. As yous are jumping, the view volition constantly shift upwardly and down. It is more pleasant to take a "neutral" area in the middle of the screen where you lot can move effectually without causing whatever scrolling.

We are now able to brandish our tiny level.

          <          link          rel="stylesheet"          href="css/game.css"          >          <          script          >          permit          simpleLevel          =          new          Level(simpleLevelPlan);          permit          display          =          new          DOMDisplay(document.body,          simpleLevel);          display.syncState(Country.showtime(simpleLevel));          </          script          >        

The <link> tag, when used with rel="stylesheet", is a style to load a CSS file into a page. The file game.css contains the styles necessary for our game.

Move and collision

Now nosotros're at the point where we tin commencement adding motility—the most interesting aspect of the game. The basic approach, taken by most games similar this, is to split up time into small steps and, for each stride, move the actors by a distance corresponding to their speed multiplied by the size of the time stride. Nosotros'll mensurate time in seconds, so speeds are expressed in units per second.

Moving things is easy. The difficult part is dealing with the interactions betwixt the elements. When the thespian hits a wall or floor, they should not simply move through it. The game must notice when a given motility causes an object to hit another object and respond accordingly. For walls, the motion must be stopped. When hitting a coin, it must be nerveless. When touching lava, the game should be lost.

Solving this for the general example is a big task. Yous can find libraries, ordinarily called physics engines, that simulate interaction between physical objects in two or three dimensions. We'll have a more modest approach in this chapter, handling merely collisions betwixt rectangular objects and handling them in a rather simplistic way.

Before moving the player or a cake of lava, we test whether the motion would have information technology inside of a wall. If it does, we simply abolish the motion altogether. The response to such a collision depends on the blazon of actor—the player volition stop, whereas a lava block will bounce back.

This approach requires our time steps to be rather small since it will cause motion to stop before the objects actually touch. If the fourth dimension steps (and thus the motion steps) are too big, the actor would stop up hovering a noticeable distance above the footing. Some other arroyo, arguably ameliorate only more complicated, would be to find the exact collision spot and motion at that place. We volition take the elementary approach and hide its bug by ensuring the blitheness gain in small steps.

This method tells us whether a rectangle (specified by a position and a size) touches a grid element of the given type.

          Level.prototype.touches          =          office(pos,          size,          type) {          allow          xStart          =          Math.floor(pos.x);          permit          xEnd          =          Math.ceil(pos.10          +          size.x);          permit          yStart          =          Math.floor(pos.y);          let          yEnd          =          Math.ceil(pos.y          +          size.y);          for          (let          y          =          yStart;          y          <          yEnd;          y          ++) {          for          (permit          x          =          xStart;          x          <          xEnd;          x          ++) {          let          isOutside          =          ten          <          0          |          |          ten          >=          this.width          |          |          y          <          0          |          |          y          >=          this.elevation;          let          here          =          isOutside          ?          "wall"          :          this.rows[y][x];          if          (here          ==          type)          return          truthful;     }   }          return          false; };

The method computes the ready of grid squares that the trunk overlaps with past using Math.floor and Math.ceil on its coordinates. Call back that grid squares are ane past one units in size. By rounding the sides of a box up and down, we get the range of background squares that the box touches.

Finding collisions on a grid

We loop over the cake of filigree squares found by rounding the coordinates and render true when a matching square is establish. Squares outside of the level are always treated as "wall" to ensure that the player tin can't get out the world and that we won't accidentally try to read outside of the bounds of our rows array.

The state update method uses touches to figure out whether the player is touching lava.

          State.prototype.update          =          part(time,          keys) {          permit          actors          =          this.actors          .map(histrion          =>          actor.update(time,          this,          keys));          let          newState          =          new          State(this.level,          actors,          this.status);          if          (newState.status          !=          "playing")          render          newState;          let          actor          =          newState.actor;          if          (this.level.touches(player.pos,          actor.size,          "lava")) {          return          new          State(this.level,          actors,          "lost");   }          for          (let          actor          of          actors) {          if          (actor          !=          player          &          &          overlap(actor,          thespian)) {          newState          =          actor.collide(newState);     }   }          return          newState; };

The method is passed a time stride and a information structure that tells it which keys are being held down. The first matter it does is call the update method on all actors, producing an array of updated actors. The actors also get the fourth dimension stride, the keys, and the state, and then that they can base their update on those. Merely the thespian volition actually read keys, since that's the simply actor that'south controlled past the keyboard.

If the game is already over, no farther processing has to be done (the game can't be won after being lost, or vice versa). Otherwise, the method tests whether the player is touching background lava. If so, the game is lost, and we're done. Finally, if the game actually is still going on, information technology sees whether any other actors overlap the thespian.

Overlap betwixt actors is detected with the overlap office. Information technology takes 2 role player objects and returns true when they affect—which is the case when they overlap both forth the x-axis and along the y-axis.

          function          overlap(actor1,          actor2) {          render          actor1.pos.x          +          actor1.size.x          >          actor2.pos.x          &          &          actor1.pos.x          <          actor2.pos.x          +          actor2.size.x          &          &          actor1.pos.y          +          actor1.size.y          >          actor2.pos.y          &          &          actor1.pos.y          <          actor2.pos.y          +          actor2.size.y; }

If any actor does overlap, its collide method gets a chance to update the land. Touching a lava histrion sets the game status to "lost". Coins vanish when yous touch them and set up the status to "won" when they are the last money of the level.

          Lava.image.collide          =          part(state) {          return          new          State(state.level,          state.actors,          "lost"); };          Money.paradigm.collide          =          role(state) {          let          filtered          =          state.actors.filter(a          =>          a          !=          this);          permit          status          =          state.status;          if          (!          filtered.some(a          =>          a.type          ==          "coin"))          status          =          "won";          return          new          State(state.level,          filtered,          status); };

Actor updates

Actor objects' update methods take as arguments the fourth dimension step, the land object, and a keys object. The one for the Lava player type ignores the keys object.

          Lava.prototype.update          =          function(time,          state) {          let          newPos          =          this.pos.plus(this.speed.times(time));          if          (!          land.level.touches(newPos,          this.size,          "wall")) {          return          new          Lava(newPos,          this.speed,          this.reset);   }          else          if          (this.reset) {          return          new          Lava(this.reset,          this.speed,          this.reset);   }          else          {          render          new          Lava(this.pos,          this.speed.times(-          1));   } };

This update method computes a new position by calculation the product of the time step and the current speed to its old position. If no obstruction blocks that new position, it moves there. If at that place is an obstacle, the behavior depends on the blazon of the lava block—dripping lava has a reset position, to which information technology jumps back when it hits something. Bouncing lava inverts its speed by multiplying it by -one so that it starts moving in the reverse management.

Coins utilize their update method to wobble. They ignore collisions with the grid since they are simply wobbling around within of their own foursquare.

          const          wobbleSpeed          =          8,          wobbleDist          =          0.07;          Coin.image.update          =          role(time) {          let          wobble          =          this.wobble          +          time          *          wobbleSpeed;          allow          wobblePos          =          Math.sin(wobble)          *          wobbleDist;          render          new          Coin(this.basePos.plus(new          Vec(0,          wobblePos)),          this.basePos,          wobble); };

The wobble property is incremented to rail time and then used as an statement to Math.sin to notice the new position on the moving ridge. The coin'south electric current position is and then computed from its base position and an offset based on this wave.

That leaves the role player itself. Player motion is handled separately per centrality because hit the flooring should not prevent horizontal move, and hitting a wall should non cease falling or jumping motion.

          const          playerXSpeed          =          7;          const          gravity          =          thirty;          const          jumpSpeed          =          17;          Histrion.prototype.update          =          office(fourth dimension,          state,          keys) {          let          xSpeed          =          0;          if          (keys.ArrowLeft)          xSpeed          -=          playerXSpeed;          if          (keys.ArrowRight)          xSpeed          +=          playerXSpeed;          permit          pos          =          this.pos;          let          movedX          =          pos.plus(new          Vec(xSpeed          *          time,          0));          if          (!          state.level.touches(movedX,          this.size,          "wall")) {          pos          =          movedX;   }          let          ySpeed          =          this.speed.y          +          time          *          gravity;          let          movedY          =          pos.plus(new          Vec(0,          ySpeed          *          fourth dimension));          if          (!          state.level.touches(movedY,          this.size,          "wall")) {          pos          =          movedY;   }          else          if          (keys.ArrowUp          &          &          ySpeed          >          0) {          ySpeed          =          -          jumpSpeed;   }          else          {          ySpeed          =          0;   }          render          new          Role player(pos,          new          Vec(xSpeed,          ySpeed)); };

The horizontal motion is computed based on the land of the left and right arrow keys. When there's no wall blocking the new position created past this motion, it is used. Otherwise, the quondam position is kept.

Vertical motion works in a similar mode but has to simulate jumping and gravity. The role player'southward vertical speed (ySpeed) is starting time accelerated to account for gravity.

We check for walls again. If we don't hit whatsoever, the new position is used. If there is a wall, there are ii possible outcomes. When the upward arrow is pressed and we are moving down (meaning the thing we hitting is below us), the speed is gear up to a relatively large, negative value. This causes the player to spring. If that is not the instance, the player merely bumped into something, and the speed is gear up to zero.

The gravity force, jumping speed, and pretty much all other constants in this game accept been set past trial and error. I tested values until I plant a combination I liked.

Tracking keys

For a game like this, we practice non want keys to take effect once per keypress. Rather, we desire their consequence (moving the player figure) to stay active as long as they are held.

We need to ready a primal handler that stores the current state of the left, right, and up pointer keys. We volition also want to call preventDefault for those keys and so that they don't terminate up scrolling the page.

The following function, when given an assortment of cardinal names, will render an object that tracks the current position of those keys. It registers issue handlers for "keydown" and "keyup" events and, when the key code in the event is present in the fix of codes that it is tracking, updates the object.

          function          trackKeys(keys) {          let          down          =          Object.create(null);          office          track(event) {          if          (keys.includes(issue.primal)) {          down[effect.fundamental]          =          event.blazon          ==          "keydown";          event.preventDefault();     }   }          window.addEventListener("keydown",          rails);          window.addEventListener("keyup",          rail);          render          down; }          const          arrowKeys          =          trackKeys(["ArrowLeft",          "ArrowRight",          "ArrowUp"]);

The same handler function is used for both outcome types. It looks at the effect object'south type property to make up one's mind whether the fundamental country should be updated to true ("keydown") or false ("keyup").

Running the game

The requestAnimationFrame function, which nosotros saw in Affiliate 14, provides a proficient way to animate a game. Just its interface is quite primitive—using it requires u.s.a. to track the time at which our function was chosen the last time effectually and phone call requestAnimationFrame again after every frame.

Let'south define a helper role that wraps those tiresome parts in a convenient interface and allows u.s. to simply call runAnimation, giving it a function that expects a time deviation as an argument and draws a single frame. When the frame office returns the value false, the animation stops.

          part          runAnimation(frameFunc) {          let          lastTime          =          cypher;          function          frame(time) {          if          (lastTime          !=          nil) {          let          timeStep          =          Math.min(time          -          lastTime,          100)          /          1000;          if          (frameFunc(timeStep)          ===          false)          render;     }          lastTime          =          time;          requestAnimationFrame(frame);   }          requestAnimationFrame(frame); }

I have prepare a maximum frame pace of 100 milliseconds (one-10th of a second). When the browser tab or window with our folio is hidden, requestAnimationFrame calls will be suspended until the tab or window is shown again. In this example, the departure between lastTime and time will be the entire time in which the page was subconscious. Advancing the game past that much in a single step would look light-headed and might cause weird side effects, such as the thespian falling through the flooring.

The part also converts the fourth dimension steps to seconds, which are an easier quantity to think well-nigh than milliseconds.

The runLevel function takes a Level object and a brandish constructor and returns a hope. Information technology displays the level (in document.torso) and lets the user play through it. When the level is finished (lost or won), runLevel waits ane more than second (to let the user see what happens) and so clears the display, stops the animation, and resolves the promise to the game'southward terminate status.

          part          runLevel(level,          Display) {          let          display          =          new          Display(certificate.body,          level);          allow          state          =          State.outset(level);          permit          ending          =          1;          render          new          Promise(resolve          =>          {          runAnimation(time          =>          {          state          =          state.update(time,          arrowKeys);          display.syncState(state);          if          (state.status          ==          "playing") {          return          true;       }          else          if          (catastrophe          >          0) {          ending          -=          time;          return          true;       }          else          {          display.clear();          resolve(country.status);          return          fake;       }     });   }); }

A game is a sequence of levels. Whenever the player dies, the current level is restarted. When a level is completed, we move on to the next level. This tin be expressed by the following function, which takes an assortment of level plans (strings) and a brandish constructor:

          async          function          runGame(plans,          Display) {          for          (allow          level          =          0;          level          <          plans.length;) {          let          status          =          await          runLevel(new          Level(plans[level]),          Display);          if          (status          ==          "won")          level          ++;   }          panel.log("You've won!"); }

Because nosotros made runLevel render a hope, runGame tin can be written using an async function, as shown in Chapter xi. It returns another promise, which resolves when the histrion finishes the game.

There is a set of level plans bachelor in the GAME_LEVELS binding in this chapter'southward sandbox. This folio feeds them to runGame, starting an actual game.

          <          link          rel="stylesheet"          href="css/game.css"          >          <          body          >          <          script          >          runGame(GAME_LEVELS,          DOMDisplay);          </          script          >          </          body          >        

See if you can beat those. I had quite a lot of fun building them.

Exercises

Game over

Information technology's traditional for platform games to have the player offset with a limited number of lives and subtract 1 life each time they die. When the histrion is out of lives, the game restarts from the kickoff.

Suit runGame to implement lives. Have the player kickoff with 3. Output the current number of lives (using panel.log) every time a level starts.

          <          link          rel="stylesheet"          href="css/game.css"          >          <          torso          >          <          script          >                    async          role          runGame(plans,          Display) {          for          (allow          level          =          0;          level          <          plans.length;) {          let          status          =          wait          runLevel(new          Level(plans[level]),          Display);          if          (status          ==          "won")          level          ++;     }          panel.log("You've won!");   }          runGame(GAME_LEVELS,          DOMDisplay);          </          script          >          </          trunk          >        

Pausing the game

Make information technology possible to break (append) and unpause the game past pressing the Esc central.

This can be washed past changing the runLevel function to utilize another keyboard event handler and interrupting or resuming the animation whenever the Esc key is striking.

The runAnimation interface may not expect like information technology is suitable for this at first glance, just information technology is if you rearrange the way runLevel calls information technology.

When you have that working, there is something else you could try. The way we have been registering keyboard event handlers is somewhat problematic. The arrowKeys object is currently a global binding, and its issue handlers are kept around even when no game is running. You could say they leak out of our organisation. Extend trackKeys to provide a way to unregister its handlers and then alter runLevel to register its handlers when it starts and unregister them again when it is finished.

          <          link          rel="stylesheet"          href="css/game.css"          >          <          body          >          <          script          >                    function          runLevel(level,          Display) {          let          display          =          new          Display(document.trunk,          level);          let          state          =          State.beginning(level);          permit          ending          =          1;          render          new          Hope(resolve          =>          {          runAnimation(time          =>          {          state          =          state.update(time,          arrowKeys);          display.syncState(state);          if          (land.status          ==          "playing") {          return          true;         }          else          if          (ending          >          0) {          ending          -=          time;          return          true;         }          else          {          display.articulate();          resolve(state.status);          return          false;         }       });     });   }          runGame(GAME_LEVELS,          DOMDisplay);          </          script          >          </          body          >        

An blitheness can be interrupted past returning fake from the function given to runAnimation. It tin be continued by calling runAnimation once more.

Then nosotros need to communicate the fact that nosotros are pausing the game to the function given to runAnimation. For that, you can utilize a binding that both the event handler and that role have access to.

When finding a way to unregister the handlers registered past trackKeys, remember that the exact same function value that was passed to addEventListener must be passed to removeEventListener to successfully remove a handler. Thus, the handler function value created in trackKeys must be available to the lawmaking that unregisters the handlers.

Yous tin add a property to the object returned by trackKeys, containing either that function value or a method that handles the unregistering straight.

A monster

It is traditional for platform games to accept enemies that yous can spring on top of to defeat. This do asks yous to add such an histrion type to the game.

We'll phone call information technology a monster. Monsters motility only horizontally. You lot can make them move in the direction of the role player, bounce back and forth similar horizontal lava, or have any motility pattern you want. The class doesn't have to handle falling, but it should make sure the monster doesn't walk through walls.

When a monster touches the player, the effect depends on whether the player is jumping on superlative of them or not. You can approximate this by checking whether the player's bottom is almost the monster'due south top. If this is the case, the monster disappears. If not, the game is lost.

          <          link          rel="stylesheet"          href="css/game.css"          >          <          style          >          .monster          {          background:          purple          }</          manner          >          <          torso          >          <          script          >                    class          Monster          {          constructor(pos, ) {}          get          type() {          return          "monster"; }          static          create(pos) {          return          new          Monster(pos.plus(new          Vec(0,          -          1)));       }          update(time,          land) {}          collide(state) {}     }          Monster.prototype.size          =          new          Vec(one.2,          ii);          levelChars["M"]          =          Monster;          runLevel(new          Level(`          ..................................          .################################.          .#..............................#.          .#..............................#.          .#..............................#.          .#...........................o..#.          .#..@...........................#.          .##########..............########.          ..........#..o..o..o..o..#........          ..........#...........Chiliad..#........          ..........################........          ..................................          `),          DOMDisplay);          </          script          >          </          body          >        

If you desire to implement a type of motility that is stateful, such as bouncing, brand sure you store the necessary state in the player object—include information technology as constructor argument and add it as a belongings.

Remember that update returns a new object, rather than changing the old one.

When handling collision, observe the player in country.actors and compare its position to the monster's position. To go the bottom of the player, y'all have to add its vertical size to its vertical position. The creation of an updated state volition resemble either Money's collide method (removing the actor) or Lava's (changing the status to "lost"), depending on the player position.

malcomprawn1992.blogspot.com

Source: https://eloquentjavascript.net/16_game.html

Post a Comment for "Prevent Javascript From Running Again if Back Spacing on Page"