This is the fourth day of my participation in the August More text Challenge. For details, see:August is more challenging

preface

To the moon from the beginning,

Even if the failure

Maybe we can get a star.

— Clement Stone

introduce

Haven’t seen a firefly in a long time. Think of childhood, in the woods full of small fireflies, in the dark twilight, how happily they spread their wings! He is neither the sun nor the moon, and like the stars he brings endless joy.

Virtual joysticks are an important component of mobile games, especially RPGS. In this installment, we developed a virtual joystick to control the movement of fireflies. We will complete this scene by building the basic scene, drawing the background and firefly, and implementing the virtual joystick.

The development of

1. Set up basic scenarios

<style>
    * {
        padding: 0;
        margin: 0;
    }
    html.body {
        width: 100%;
        height: 100vh;
        overflow: hidden;
    }
    #canvas{
        width: 100%;
        height: 100%;
    }
</style>
<body>
    <canvas id="canvas"></canvas>
    <script type="module" src="./app.js"></script>
</body>
Copy the code

Here we write the infrastructure first, using the Module pattern to import modules.

Let’s define rocker and introduce it.

/*rocker.js*/
class Rocker {
  constructor(options) {}
  render(ctx) {
    return this; }}export default Rocker;
Copy the code

We first started to write the logic of the main scene. In fact, firefly should be written in a separate class and then instantiated to control. However, since the whole scene is relatively small and the focus of this time is on the virtual joystick part, it should be written together with the main scene.

/*app.js*/
import Rocker from "./js/rocker.js";
class Application {
  constructor() {
    this.canvas = null;            / / the canvas
    this.ctx = null;               / / environment
    this.w = 0;                    / / canvas width
    this.h = 0;                    / / the canvas
    this.r = 64;                   // The size of the firefly
    this.x = 0;                    // The X-axis coordinates of the firefly
    this.y = 0;                    // Firefly y coordinate
    this.speed = 1;                // Firefly moving speed
    this.textures = new Map(a);/ / texture set
    this.spriteData = new Map(a);// Sprite data
    this.rocker = null;            // The joystick object
    this.angle = 0;                // The current Angle
    this.gradient = null;          // Firefly glow gradient
    this.init();
  }
  init() {
    / / initialization
    this.canvas = document.getElementById("canvas");
    this.ctx = canvas.getContext("2d");
    window.addEventListener("resize".this.reset.bind(this));
    this.reset();
    this.textures.set("rocker0".".. /assets/rocker0.png");
    this.textures.set("rocker1".".. /assets/rocker1.png");
    this.load().then(this.render.bind(this));
  }
  reset() {
    // Screen change event
    this.w = this.canvas.width = window.innerWidth;
    this.h = this.canvas.height = window.innerHeight;
    this.x = (this.w - this.r) / 2;
    this.y = (this.h - this.r) / 2;
  }
  load(){
    // Load the texture
    const {textures, spriteData} = this;
    let n = 0;
    return new Promise((resolve, reject) = > {
      for (const key of textures.keys()) {
        let _img = new Image();
        spriteData.set(key, _img);
        _img.onload = () = > {
          if(++n == textures.size) resolve(); } _img.src = textures.get(key); }})}render() {
    / / the main rendering
    const {spriteData, w, h, ctx} = this;
    this.rocker = new Rocker({
      w,
      h,
      spriteData,
    }).render(ctx);
    this.step();
  }
  drawBackground(){
     // Draw the background
  }
  drawBall(){
     // Draw fireflies
  }
  step(delta) {
    / / redraw
    const {w, h, ctx} = this;
    requestAnimationFrame(this.step.bind(this));
    ctx.clearRect(0.0, w, h);
    this.drawBackground();
    this.drawBall();
    this.rocker && this.rocker.draw(); }}window.onload = new Application();
Copy the code

We defined variables and render redraw events in the main scene logic, and first loaded the two images used by the virtual joystick.

The virtual joystick picture is divided into the outer ring and the inner joystick block, as shown in the figure below:

The render method instantiates the virtual joystick and then performs the redraw operation, rendering the image and the firefly and the virtual joystick inside the redraw.

2. Background and firefly drawing

// Draw the background
drawBackground() {
    const {w, h, ctx} = this;
    ctx.fillStyle = "RGB (24,13,50)";
    ctx.fillRect(0.0, w, h);
}
Copy the code
// Draw fireflies
drawBall() {
    const {x, y, ctx, r} = this;
    ctx.save();
    if (!this.gradient) {
        this.setGradient()
    }
    ctx.fillStyle = this.gradient;
    ctx.translate(x, y);
    ctx.fillRect(0.0, r, r);
    ctx.restore();
}
Copy the code

When we drew the firefly we expected the fill color to be a gradual glow from the inside out. So next, we’re going to write setGradient to change the gradient value to fillStyle

// Set the gradient
setGradient(n = 0) {
    const {ctx, r} = this;
    this.gradient = ctx.createRadialGradient(r / 2, r / 2.0, r / 2, r / 2, r / 2)
    this.gradient.addColorStop(0.'rgba(255,255,128,1)');
    this.gradient.addColorStop(0.2 - n, ` rgba (215215, 0,The ${0.7 - n * 2}) `);
    this.gradient.addColorStop(0.5 - n * 2.` rgba (245245, 0,The ${0.3 - n}) `);
    this.gradient.addColorStop(0.7 - n, 'rgba(255,255,255,0)');
}
Copy the code

We can’t write dead in gradient, because when the firefly flies, the flapping of its wings will affect the light area slightly, so we give it a value to simulate its state change when redrawing.

step(delta) {
    const {w, h, ctx} = this;
    requestAnimationFrame(this.step.bind(this));
    ctx.clearRect(0.0, w, h);
    this.drawBackground();
    this.drawBall();
    this.rocker && this.rocker.draw();
    // + Take mode and flash every 3
    if (~~delta % 3= =0) {
        this.setGradient(0.1)}else {
        this.setGradient()
    }
}
Copy the code

So here we have the picture, the fireflies flashing in the middle of the screen.

3. Realization of virtual joystick

We’re going to implement a class of virtual joysticks and then control the firefly to move through its instantiation.

class Rocker {
  constructor(options) {
    this.w = 0;                             // The width of the current scene
    this.h = 0;                             // The height of the current scene
    this.D = 180;                           // Diameter of outer ring
    this.d = 60;                            // The slider diameter
    this.spriteData = null;                 // Sprite image data
    Object.assign(this, options);
    this.x = 0;                             // The X-axis coordinates of the slider
    this.y = 0;                             // The y axis of the slider
    this.centerX = 0;                       // The x coordinate of the center point
    this.centerY = 0;                       // The Y-axis coordinate of the center point
    this.padding = 20;                      // The distance between the outer ring and the bottom
    this.isActive = false;                  // Whether to trigger
    this.angle = 0;                         // The current Angle
    this.event = new Map(a);// Event dictionary
    return this;
  }
  render(ctx) {
    / / the main rendering
    const {d, h, D, padding} = this;
    this.ctx = ctx;
    this.x = this.centerX = padding + D / 2;      // Calculate the x coordinate of the center point
    this.y = this.centerY = h - padding - D / 2;  // Calculate the y coordinate of the center point
    this.draw();
    let canvas = ctx.canvas;            // Get the current scene canvas
    // Determine the device environment
    // 1. Touch events on the mobile terminal
    if (navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)) {
      canvas.addEventListener("touchstart".this._mousedown.bind(this));
      canvas.addEventListener("touchmove".this._mousemove.bind(this));
      canvas.addEventListener("touchend".this._mouseup.bind(this));
    } 
    // 2. Mouse event on the PC
    else {
      canvas.addEventListener("mousedown".this._mousedown.bind(this));
      canvas.addEventListener("mousemove".this._mousemove.bind(this));
      canvas.addEventListener("mouseup".this._mouseup.bind(this));
      canvas.addEventListener("mouseout".this._mouseup.bind(this));
    }
    return this;
  }
  on(eventName, callback) {
    // Fake subscription message
    this.event.set(eventName, (. args) = >callback && callback(... args)) }_mousedown(e) {
    // Input device press down
    const {d} = this;
    let x = e.offsetX || e.changedTouches[0].clientX;
    let y = e.offsetY || e.changedTouches[0].clientY;
    if ((x > this.x - d / 2 && x < this.x + d / 2) && (y > this.y - d / 2 && y < this.y + d / 2)) {
      this.isActive = true; }}_mousemove(e) {
    // The input device moves
    if (this.isActive) {
      let x = e.offsetX || e.changedTouches[0].clientX;
      let y = e.offsetY || e.changedTouches[0].clientY;
​
      this.limitToCircle({
        x,
        y
      }, this.D / 2 - this.d / 2); }}_mouseup(e) {
    // The input device lifts or leaves
    this.isActive = false;
    this.x = this.centerX;
    this.y = this.centerY;
  }
  limitToCircle(pos, limitRadius) {
    // Determine the sliding boundary
    const {centerX, centerY} = this;
    let dx = pos.x - centerX;
    let dy = pos.y - centerY;
    let _r = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
    this.angle = Math.atan2(dy, dx);
    if (this.event.has("change")) this.event.get("change") (this.angle, Math.min(_r,limitRadius) / limitRadius);
    if (_r < limitRadius) {
      this.x = pos.x;
      this.y = pos.y;
    } else {
      this.x = centerX + Math.cos(this.angle) * limitRadius;
      this.y = centerY + Math.sin(this.angle) * limitRadius; }}draw() {
    / / the main draw
    this.drawOuter();
    this.drawCenter();
  }
  drawOuter() {
    // Draw the outer ring
    const {ctx, spriteData, D, h, padding} = this;
    ctx.save();
    ctx.translate(padding, h - D - padding);
    ctx.drawImage(spriteData.get("rocker0"), 0.0, D, D);
    ctx.restore();
  }
  drawCenter() {
    // Draw the center slider
    const {ctx, spriteData, d, x, y} = this;
    ctx.save();
    ctx.translate(x - d / 2, y - d / 2);
    ctx.drawImage(spriteData.get("rocker1"), 0.0, d, d); ctx.restore(); }}export default Rocker;
Copy the code

Above is the virtual joystick complete code.

Virtual joysticks go through the following processes:

  • Draw: Without going into detail here, just put the loaded image and position it.
  • Input events: Monitors the operation of the input device. The PC uses the mouse event to monitor, and the mobile end uses the touch event to monitor. It activates the slider that is currently clicked on the joystickisActiveMake a switch to judge. If you hold it down and move it, keep changing the coordinates to make it look like it’s dragging with the input device. Finally, it bounces offisActiveDeactivate it and return it to its original center.
  • Sliding boundary with obtaining AngleWhen sliding, we don’t let the extreme value of the firefly be drawn at any time and if there is no Angle we don’t send to the firefly’s moving position. Here we set the maximum rangelimitRadius, the company uses the distance between two points to figure out the distance between the current point and the center of the circle, if the distance is greater thanlimitRadius. So we also want to find the extreme point of the circle in that direction. But I had to use it anywayMath.atan2Figure out the Angle at which the X-axis is dragging the current point. We can use this Angle to match the trigonometric functionslimitRadiusAs the radius, you can find the extreme point in the current range. And finally, if phi is greater thanlimitRadius, then return the extremum to him, and now slide the range.
  • Release subscription: Here we for convenience, we only do a simple layer as the outside injectionchangeAfter the event we collected. When the slider to determine whether the change event exists, if so, the Angle and sliding force back to the outside world.

Finally, in the main logic, we modify the main rendering logic again to add the virtual joystick position change event listening.

render() {
    const {spriteData, w, h, ctx} = this;
    this.rocker = new Rocker({
        w,
        h,
        spriteData,
    }).render(ctx);
    this.rocker.on("change".(angle, speed) = > {
        this.angle = angle
        this.speed = speed * 3;
    })
    this.step();
}
Copy the code

So we have the Angle and the velocity.

Next, we can change his coordinates on the firefly map.

drawBall() {
    const {x, y, ctx, r, angle, speed} = this;
    if (this.rocker && this.rocker.isActive) {
        let vx = speed * Math.cos(angle);
        let vy = speed * Math.sin(angle);
        this.x += vx;
        this.y += vy;
    }
    ctx.save();
    if (!this.gradient) {
        this.setGradient()
    }
    ctx.fillStyle = this.gradient;
    ctx.translate(x, y);
    ctx.fillRect(0.0, r, r);
    ctx.restore();
}
Copy the code

It is simple to determine if the current virtual joystick is active, and use trigonometric functions to calculate the increment of x and y in its direction and velocity to make it displacement.

At this point, we’ve implemented a virtual joystick from zero and controlled the firefly’s movement through it, demonstrating it online

Expansion and extension

With the virtual joystick, you can make all kinds of mobile RPGS. But the math involved goes further and leads to more ideas, such as the detection of objects in range, or the dragging of items, or the movement of characters in war games.


In fact, this tutorial is supposed to write the first yamu tea air through the virtual rocker control air, but afraid of the street. I’ll be a firefly.