All chapters

  • Deep analysis, using Threejs to copy wechat jump (1)
  • Deep analysis, using Threejs to copy wechat jump (2)

preface

Engaged in the front end for three years, stayed in a few small companies, do a few small projects. Three years mentality slightly impetuous, former friends have already practiced the body of the big man, and I stayed in place for a long time. As a result of leaving office in the past period of time, I am preparing for a test to make up for it recently, so that the day and night are reversed, and I can not distinguish the day and night. Force-feeding is always boring, and my damn memory sucks, so find something fun (copy a little game) to stimulate yourself!

Expression ability is limited, writing and poor, if there are a lot of sick sentences also please massive……

Because this article is just an attempt to micro letter jump a jump for an in-depth copy, and the original game must still have a big gap, and the first use of Threejs, so this analysis only as a simple guide, I hope you can have some effect, if there is something wrong with the place readers can play freely.


Ten thousand words multi picture long warning!!

The source code for this chapter has been posted on Github. This is an example. It is a semi-finished product and has not been written yet

Front knowledge

Wechat jump jump, when the game just came out, I wrote a very simple version in my spare time, thinking that the next is very simple, but there is no doubt that it is just no ups and downs of the waves, this rewrite let me step on several pits plus. Seemingly calm water, if you do not go into the water, you do not know how much underwater current surging.

Before we think about implementation, we need to know a little bit about Threejs that are relevant to this game

Threejs three components

  1. There is a Scene.

    const scene = new THREE.Scene()
    // Coordinate auxiliary line is very useful in debugging phase
    scene.add(new THREE.AxesHelper(10e3))
    Copy the code
  2. Camera, the focus here is on the orthogonal Camera, which the game implementation will use.

    • Orthogonal camera

      const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far)
      // Place the orthogonal camera in the scene
      scene.add(camera)
      Copy the code

      The size of an object seen by an orthogonal camera has nothing to do with the distance between it and the object. For example, if your fixed field of vision is 200×320, the object within 1000 meters can be seen at the farthest, and the object 1 meter away can be seen recently, then:

      const camera = new THREE.OrthographicCamera(- 200. / 2.200 / 2.320 / 2.- 320. / 2.1.1000)
      Copy the code
    • We don’t need perspective cameras

  3. Renderer

    const renderer = new THREE.WebGLRenderer({
        antialias: true / / anti-aliasing
    })
    
    // Concrete render
    renderer.render(scene, camera)
    Copy the code

2. Create objects

The first is what shape do you need? Geometry, what does the object look like? Material, and then create its Mesh. Do you need to see objects? Light

Third, object shadow

  1. Objects that receive shadows, such as creating a ground to receive shadows

    const geometry = new THREE.PlaneBufferGeometry(10e2.10e2.1.1)
    const meterial = new THREE.MeshLambertMaterial()
    
    const plane = new THREE.Mesh(geometry, meterial)
    // Accept shadows
    plane.receiveShadow = true
    Copy the code
  2. Object opening projection

    // Create a cube
    const geometry = new THREE.BoxBufferGeometry()
    const meterial = new THREE.MeshLambertMaterial()
    
    const box = new THREE.Mesh(geometry, meterial)
    // Cast my shadow
    box.castShadow = true
    // Other people's shadows can fall on me
    box.receiveShadow = true
    Copy the code
  3. Light source on shadow

    / / parallel light
    const lightght = new THREE.DirectionalLight(0xffffff.8.)
    // Cast shadows
    light.castShadow = true
    // Define the cast shadow of the visible domain
    light.shadow.camera.left = - 400.
    light.shadow.camera.right = 400
    light.shadow.camera.top = 400
    light.shadow.camera.bottom = - 400.
    light.shadow.camera.near = 0
    light.shadow.camera.far = 1000
    Copy the code
  4. The scene also needs shadows on

    const const renderer = new THREE.WebGLRenderer({ ... })
    renderer.shadowMap.enabled = true
    Copy the code

The transformation origin of threejs

The origin of rotation and scale is the center point of the Mesh. Draw a picture to describe it:

In other words, the scaling origin can be controlled by the Geometry of displacement. In addition, there are groups in Threejs, so if the scaling operation is carried out on an object in the Group, the corresponding is to control the scaling origin of the object by controlling the position of the object in the Group

Optimization of Threejs

  • Replace Geometry with BufferGeometry, which caches the grid model for more efficient performance.
  • Use the clone() method
    // create a cube with a default size of 1,1,1
    const baseBoxBufferGeometry = new THREE.BoxBufferGeometry()
    // Clone geometry
    const geometry = baseBoxBufferGeometry.clone()
    // Set the size of the geometry by scaling
    geometry.scale(20.20.20)
    Copy the code
  • Objects that are no longer needed should be destroyeddispose

Start analyzing step one

Since we want to analyze how to start, then we need to take out the phone first, and we need to get familiar with the terrain first

In the case of this kind of don’t know where to start, the first thing we have to find a breakthrough point (for example, must first do), then according to this point were unfolding, until the game’s veil, it is a bit similar to the programming community often emitted a word process oriented type, not very tall, but very practical.

  • First we have to create a scene, and then create a box in the scene, and I have to look at the box with a view like a jump on wechat
  • To be found…

Create a scene

The creation of the scenario is simple, which is threejs’s three main components. One thing to note is, how big is the scene? Actually, I don’t know…

Open wechat jump a few…… And don’t forget to watch carefully!!

Determine the scene size

You can’t visually determine how big the scene is, but you can determine what camera should be used in the scene. Yes, orthogonal camera, the interface from wechat jump should be very clear feeling, the size of the object has no relation to the distance, here two pictures intuitively show the difference between orthogonal camera and perspective camera.

Then the solution is obvious. We just need to define a scene size by ourselves, and then take a proper range of the object size in it relative to the scene size. The width and height of canvas is a bit like the visual viewport, and the scene size is a bit like the layout viewport, and then scale the layout viewport to the visual viewport size. If I set the width of the scene to 1000, I can draw the object with a width of 500, or I can define other dimensions. Considering wechat hop is full screen and fits different phones, we use innerWidth and innerHeight to set the scene size.

Create a camera

Since you want to use an orthogonal camera, and also determine the scene size, that is, determine the width and height of the orthogonal camera’s viewing cone, and then the near end face and far end face are reasonable, which depends on the camera Angle. We create the camera and set the camera position to -100,100,-100 so that the X and Z axes are in front of us. The reason is that we don’t have to use negative coordinates later on (the direction of the auxiliary line is positive).

  const { innerWidth, innerHeight } = window
  /**
   * 场景
   */
  const scene = new THREE.Scene()
  // The scene background is used for debugging
  scene.background = new THREE.Color( 0xf5f5f5 )
  // Coordinate auxiliary line is very useful in debugging phase
  scene.add(new THREE.AxesHelper(10e3))

  /** ** camera */
  const camera = new THREE.OrthographicCamera(-innerWidth / 2, innerWidth / 2, innerHeight / 2, -innerHeight / 2.0.1.1000)
  camera.position.set(- 100..100.- 100.)
  // Look to the center of the scene
  camera.lookAt(scene.position)
  scene.add(camera)

  /** * box */
  const boxGeometry = new THREE.BoxBufferGeometry(100.50.100)
  const boxMaterial = new THREE.MeshLambertMaterial({ color: 0x67C23A })
  const box = new THREE.Mesh(boxGeometry, boxMaterial)
  scene.add(box)

  /** * renderer */
  const canvas = document.querySelector('#canvas')
  const renderer = new THREE.WebGLRenderer({
      canvas,
      alpha: true.// Transparent scene
      antialias:true / / anti-aliasing
  })
  renderer.setSize(innerWidth, innerHeight)

  / / rendering
  renderer.render(scene, camera)
Copy the code

All of this is pretty simple, it’s not that hard, but you find the box is pure black, and you can only see a little bit of outline because there’s no light on it, and now you throw light on it…

/** ** parallel light */
const light = new THREE.DirectionalLight(0xffffff.8.)
light.position.set(- 200..600.300)
/ / the ambient light
scene.add(new THREE.AmbientLight(0xffffff.4.))
scene.add(light)
Copy the code

Now that we’ve seen the color of the box, and we’ve got the corresponding outline, our first step is done. Hey, hey, hey. But what’s missing?

Open wechat jump jump a pondering……

Where is the shadow of the box?

According to the shadow

According to the shadow requirement, we first need to create a floor that receives shadows from boxes and other objects, and this floor acts as a shadow receiver for all objects throughout the game.

const planeGeometry = new THREE.PlaneBufferGeometry(10e2.10e2.1.1)
const planeMeterial = new THREE.MeshLambertMaterial({ color: 0xffffff })

const plane = new THREE.Mesh(planeGeometry, planeMeterial)
plane.rotation.x = -. 5 * Math.PI
plane.position.y = 1 -.
// Accept shadows
plane.receiveShadow = true
scene.add(plane)
Copy the code

At the same time

// Make the object cast a shadow
box.castShadow = true

// Let parallel light cast shadows
light.castShadow = true
// Define the cast shadow of the visible domain
light.shadow.camera.left = - 400.
light.shadow.camera.right = 400
light.shadow.camera.top = 400
light.shadow.camera.bottom = - 400.
light.shadow.camera.near = 0
light.shadow.camera.far = 1000
// Define the resolution of the shadow
light.shadow.mapSize.width = 1600
light.shadow.mapSize.height = 1600

// The scene is shaded
renderer.shadowMap.enabled = true
Copy the code

Ok, shadows appear. However, it can be found that the white ground does not fully fill the camera’s view area, exposing the scene beyond the ground, which is definitely not acceptable. The desired effect should be that the floor covers the entire viewable area. Why does this happen? (Even if the ground is set very large)


Later, when you come across a ShadowMaterial in the threejs document, you can change the ground material to this one and set a background color for the scene.


Determine camera position

The vertical section on the right side of a camera shows that when we fix a vertical Angle Angle a at the center of the scene, the distance Y between the camera and the ground is limited. When Y is smaller than minY, there will be a blank area below the visible area, and when it is larger than maxY, there will be a blank area above. We can solve this problem by adjusting the camera distance.

At the same time, it’s easy to see that minY can be calculated from Angle A and the lower side height of the orthogonal camera

const computeCameraMinY = (radian, bottom) = > Math.cos(radian) * bottom
Copy the code

For maxY, you can calculate the vertical distance from the center of the scene to the far section of the visual cone, and then you can calculate the distance from the near section to the center of the scene, and you can calculate the maximum maxY

const computeCameraMaxY = (radian, top, near, far) = > {
    const farDistance = top / Math.tan(radian)
    const nearDistance = far - near - farDistance
    return Math.sin(radian) * nearDistance
}
Copy the code

When the vertical Angle is fixed, the range of the camera’s Y value is determined. Is there any limit on the range of the horizontal direction? As can be seen from the figure above, the horizontal coordinates x and z should be determined by the Angle between y and the horizontal direction as long as y is normal.

So we also need to figure out a horizontal Angle, let’s do that in terms of the X-axis, and let’s fix that horizontal Angle as Angle B

For ease of understanding, the figure above is drawn at 225 degrees. Now:

  • known< aTo calculate they(You can take a value within an interval)
  • known< bTo calculate thex,z
@param {Number} horizontalDeg The horizontal Angle of the camera and the X-axis * @param {Number} top camera side * @param {Number} bottom camera side * @param {Number} near camera cone end * @param {Number} far camera cone end */
export function computeCameraInitalPosition (verticalDeg, horizontalDeg, top, bottom, near, far) {
  const verticalRadian = verticalDeg * (Math.PI / 180)
  const horizontalRadian = horizontalDeg * (Math.PI / 180)
  const minY = Math.cos(verticalRadian) * bottom
  const maxY = Math.sin(verticalRadian) * (far - near - top / Math.tan(verticalRadian))
  
  if (minY > maxY) {
    console.warn('Warning: Vertical Angle too small! ')}// Select an intermediate value
  const y = minY + (maxY - minY) / 2
  const longEdge = y / Math.tan(verticalRadian)
  const x = Math.sin(horizontalRadian) * longEdge
  const z = Math.cos(horizontalRadian) * longEdge

  return { x, y, z }
}
Copy the code

If you are interested, you can try it yourself. If you set y in the function to a value outside the interval minY and maxY, the problem discussed above will occur.

We don’t have to worry about the size of the ground. We know how big the camera’s view cone is, so make the ground as large as possible

The ground should now be fully visible in the viewable area, and then grab a wechat jump to roughly determine the game’s camera position

const { x, y, z } = computeCameraInitalPosition(35.225, offsetHeight / 2, offsetHeight / 2.0.1.1000)
camera.position.set(x, y, z)
Copy the code

The current Settings will warn you that the vertical Angle is too small. In this case, you can increase the far section of the camera according to the analysis just described

const camera = new THREE.OrthographicCamera(-innerWidth / 2, innerWidth / 2, offsetHeight / 2, -offsetHeight / 2.0.1.2000)
const { x, y, z } = computeCameraInitalPosition(35.225, offsetHeight / 2, offsetHeight / 2.0.1.2000)
camera.position.set(x, y, z)
Copy the code

Place the box on the ground

It can be found that only half of the box is exposed to the ground at this time, so we need to put it on the ground plane, because when the new box is generated in wechat jump, it falls from the top, and there is an animation process of object marbles falling, so we do as follows:

box.translateY(15)
Copy the code

Any questions? This needs to be observed carefully when playing the game, I advise you to open the micro channel to jump a first……

After careful study, we find that in addition to the animation of the box, there is also an animation process when the little man accumulates force, which is a zooming operation. Then, according to the fourth point in the preceding knowledge, we need to place the scale origin of the box at the bottom center, so we have:

box.geometry.translate(0.15.0)
Copy the code

Now that the box is on the ground, it will be much easier to animate and scale the box later.

Determine the box style

The scene size is agreed as innerWidth and innerHeight, so the box size can be determined according to the scene size in order to make the box size ratio seen by different mobile phones be consistent. After all, this is a copy project and there is no design specification, so the width, depth and height of the box can be handled at our discretion.

At the same time, by experiencing and watching the wechat jump, the boxes inside should be partly customized and partly random, with different sizes and shapes. So we can prioritize the random parts and then try to support custom boxes in a similarly configurable way. After all, the appearance is different and the game logic is the same.

Now that need to be randomly generated box, considering there are too many possible differences between box, we can find out from several boxes as part of the similarities of the box of abstracting, and then use a special function to generate it, such as implementing a boxCreator function, this function to generate random sizes, color cube box. Thought of here, it seems we can maintain a collection, the collection of special store a variety of different styles of box generator (function), to achieve the demand can be customized, such as the late products need to add a posted XXX XXX in the shape of a box advertising, we can add a new item to the collection generator, And the style of the box is determined from the outside.

Since the style of the box can be determined from the outside, there needs to be a unified specification, such as the width, depth, height range of the box, and for performance optimization, it is better to provide a copy geometry object and material.

// Maintain a collection of item generators
const boxCreators = []
// Share the cube
const baseBoxBufferGeometry = new THREE.BoxBufferGeometry()
// Share materials
const baseMeshLambertMaterial = new THREE.MeshLambertMaterial()
// Random color
const colors = [0x67C23A.0xE6A23C.0xF56C6C]
// Box size limits range
const boxSizeRange = [30.60]

// Implement a default generator that generates cube boxes of varying sizes and colors
const defaultBoxCreator = (a)= > {
    const [minSize, maxSize] = boxSizeRange
    const randomSize = ~~(random() * (maxSize - minSize + 1)) + minSize
    const geometry = baseBoxBufferGeometry.clone()
    geometry.scale(randomSize, 30, randomSize)
    
    const randomColor = colors[~~(Math.random() * colors.length)]
    const material = baseMeshLambertMaterial.clone()
    material.setValues({ randomColor })
    
    return new THREE.Mesh(geometry, material)
}

// Put the box creation into the admin collection
boxCreators.push(defaultBoxCreator)
Copy the code

By now, we should have a rough idea of how to implement the game, and I think we should do a few things before we start making big moves. Looking back at the previous code, it is completely procedural, without abstraction or modular concept, which may be very helpful for us in the early stage, but this kind of thinking may have a lot of negative impact on us in the later stage. Without a clear architecture, the implementation process may be like robbing Peter to pay Paul. So next, think about what optimizations we need to make for this framework.

The beginning of object orientation

Life is a game, there are you, me and him in the game, the game has the rules of the game, and what is more reference than the real world?

We build a game world with a hop and skip, and it should keep the whole game going:

// index.js
class JumpGameWorld {
    constructor () {
        // ...}}Copy the code

Just like we human beings live on the earth, the earth as a big stage for us to release ourselves, how can jump without a carrier similar to the Earth? Create a jumping play stage:

// State.js
class Stage {
    constructor() {}}Copy the code

There was a little man on the stage:

// LittleMan.js
class LittleMan {
    constructor() {}}Copy the code

There are props (boxes) on the stage

// Prop.js
class Prop {
    constructor() {}}Copy the code

Items are unique and don’t happen in a vacuum, so implement an item generator (like a factory) :

// PropCreator.js
class PropCreator(a){
    constructor() {}}Copy the code

In addition, there are common geometry and materials, tool methods management

// utils.js

/ / material
export const baseMeshLambertMaterial = new THREE.MeshLambertMaterial()
/ / cube
export const baseBoxBufferGeometry = new THREE.BoxBufferGeometry()

// ...
Copy the code

Perfect the Stage

Once the structure of the game is established, it’s time to refine it with the skeleton, and then to refine the stage logic:

class Stage {
  constructor ({
    width,
    height,
    canvas,
    axesHelper = false./ / auxiliary line
    cameraNear, // Camera near section
    cameraFar, // Camera far section
    cameraInitalPosition, // Initial position of the camera
    lightInitalPosition // Initial position of light source{})this.width = width
    this.height = height
    this.canvas = canvas
    this.axesHelper = axesHelper
    // Orthogonal camera configuration
    this.cameraNear = cameraNear
    this.cameraFar = cameraFar
    this.cameraInitalPosition = cameraInitalPosition
    this.lightInitalPosition = lightInitalPosition
    
    this.scene = null
    this.plane = null
    this.light = null
    this.camera = null
    this.renderer = null

    this.init()
  }

  init () {
    this.createScene()
    this.createPlane()
    this.createLight()
    this.createCamera()
    this.createRenterer()
    this.render()
    this.bindResizeEvent()
  }

  bindResizeEvent () {
    const { container, renderer } = this
    window.addEventListener('resize', () = > {const { offsetWidth, offsetHeight } = container

      this.width = offsetWidth
      this.height = offsetHeight

      renderer.setSize(offsetWidth, offsetHeight)
      renderer.setPixelRatio(window.devicePixelRatio)
      this.render()
    }, false)}/ / the scene
  createScene () {
    const scene = this.scene = new THREE.Scene()

    if (this.axesHelper) {
      scene.add(new THREE.AxesHelper(10e3))}}/ / the ground
  createPlane () {
    const { scene } = this
    const geometry = new THREE.PlaneBufferGeometry(10e2.10e2.1.1)
    const meterial = new THREE.ShadowMaterial()
    meterial.opacity = 0.5

    const plane = this.plane = new THREE.Mesh(geometry, meterial)

    plane.rotation.x = -. 5 * Math.PI
    plane.position.y = 1 -.
    // Accept shadows
    plane.receiveShadow = true
    scene.add(plane)
  }

  / / light
  createLight () {
    const { scene, lightInitalPosition: { x, y, z }, height } = this
    const light = this.light = new THREE.DirectionalLight(0xffffff.8.)

    light.position.set(x, y, z)
    // Enable shadow casting
    light.castShadow = true
    // // defines cast shadows for the visible domain
    light.shadow.camera.left = -height
    light.shadow.camera.right = height
    light.shadow.camera.top = height
    light.shadow.camera.bottom = -height
    light.shadow.camera.near = 0
    light.shadow.camera.far = 2000
    // Define the resolution of the shadow
    light.shadow.mapSize.width = 1600
    light.shadow.mapSize.height = 1600

    / / the ambient light
    scene.add(new THREE.AmbientLight(0xffffff.4.))
    scene.add(light)
  }

  / / camera
  createCamera () {
    const {
      scene,
      width, height,
      cameraInitalPosition: { x, y, z },
      cameraNear, cameraFar
    } = this
    const camera = this.camera = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, cameraNear, cameraFar)

    camera.position.set(x, y, z)
    camera.lookAt(scene.position)
    scene.add(camera)
  }

  / / the renderer
  createRenterer () {
    const { canvas, width, height } = this
    const renderer = this.renderer = new THREE.WebGLRenderer({
      canvas,
      alpha: true.// Transparent scene
      antialias:true / / anti-aliasing
    })

    renderer.setSize(width, height)
    // Turn on shadows
    renderer.shadowMap.enabled = true
    // Set the device pixel ratio
    renderer.setPixelRatio(window.devicePixelRatio)
  }

  // Perform rendering
  render () {
    const { scene, camera } = this
    this.renderer.render(scene, camera) } add (... args) {return this.scene.add(... args) } remove (... args) {return this.scene.remove(... args) } }Copy the code

Improved generator PropCreator

We’ve outlined the need to maintain a collection of item generators, with default item generators and the ability to add custom generators later. Based on this logic, PropCreator should provide an API such as createPropCreator to add generators, and this API should also provide corresponding auxiliary properties such as item size range, common materials, and so on.

So what does this external API have to take into account?

  • You need to tell the outside world the limits of item size, and if a custom item is too big or too small, the game won’t be over
  • Consider performance and provide some common textures and geometry to the exterior
  • .
  @param {Boolean} isStatic is created dynamically */
  createPropCreator (creator, isStatic) {
    if (Array.isArray(creator)) {
      creator.forEach(crt= > this.createPropCreator(crt, isStatic))
    }

    const { propCreators, propSizeRange, propHeight } = this

    if (propCreators.indexOf(creator) > - 1) {
      return
    }

    const wrappedCreator = function () {
      if (isStatic && wrappedCreator.box) {
        // Static box, next time directly clone
        return wrappedCreator.box.clone()
      } else {
        const box = creator(THREE, {
          propSizeRange,
          propHeight,
          baseMeshLambertMaterial,
          baseBoxBufferGeometry
        })

        if (isStatic) {
          // We are told it is a static box, cache it
          wrappedCreator.box = box
        }
        return box
      }
    }

    propCreators.push(wrappedCreator)
  }
Copy the code

If a generator has only one style, there is no need to regenerate every time. An isStatic is passed to tell the generator whether it can be cached, so that subsequent iterations do not have to be recreated.

Next, implement the built-in generator and, for easy extension, create a new file to maintain defaultprop.js

const colors = [0x67C23A.0xE6A23C.0xF56C6C.0x909399.0x409EFF.0xffffff]

/ / static
export const statics = [
  // ...
]

/ / not static
export const actives = [
  // Default solid color cube creator
  function defaultCreator (THREE, helpers) {
    const {
      propSizeRange: [min, max],
      propHeight,
      baseMeshLambertMaterial,
      baseBoxBufferGeometry
    } = helpers

    // Random color
    const color = randomArrayElm(colors)
    // Random size
    const size = rangeNumberInclusive(min, max)

    const geometry = baseBoxBufferGeometry.clone()
    geometry.scale(size, propHeight, size)

    const material = baseMeshLambertMaterial.clone()
    material.setValues({ color })

    return new THREE.Mesh(geometry, material)
  },
]
Copy the code

The default item generator is implemented. Maybe I don’t need the default, but I can implement the following configuration:

  constructor ({
    propHeight,
    propSizeRange,
    needDefaultCreator
  }) {
    this.propHeight = propHeight
    this.propSizeRange = propSizeRange

    // Maintain the generator
    this.propCreators = []

    if (needDefaultCreator) {
      this.createPropCreator(actives, false)
      this.createPropCreator(statics, true)}}Copy the code

Then for the inside of the game, we need to provide an API to randomly execute the generator to generate items. Here we notice that the first 2 boxes of each opening of wechat jump are all of the same style (cube), so we can make a control to support passing in an index to generate the specified box.

  createProp (index) {
    const { propCreators } = this
    return index > - 1
      ? propCreators[index] && propCreators[index]() || randomArrayElm(propCreators)()
      : randomArrayElm(propCreators)()
  }
Copy the code

That’s it for item generators, but don’t be afraid to make a serious product. Such as:

  • Control the frequency and number of times items appear
  • Different props are not the same as the entrance animation
  • .

Improved Prop class

The item category needs to be expanded in the later stage. In addition to a few basic attributes, there are other things to be expanded in the future, such as the access and calculation of some attributes of the item, and the animation of the item. At this point, I also decide what I need to write in the later stage.

class Prop {
  constructor ({
    world, // The world
    stage, // The stage
    body, / / the main body
    height
  }) {
    this.world = world
    this.stage = stage
    this.body = body
    this.height = height
  }
  
  getPosition () {
    return this.body.position
  }

  setPosition (x, y, z) {
    return this.body.position.set(x, y, z)
  }
}
Copy the code

Initialize the stage and item generator

Next we initialize the stage and the item generator in the game world. Note that the item generator is only responsible for generating items, it doesn’t know where the generated items should be, so in JumpGameWorld we need to implement an internal createProp method that tells the item generator to generate a box for me, And then IT’s up to me to put it there.

  constructor ({
    container,
    canvas,
    needDefaultCreator = true,
    axesHelper = false{})const { offsetWidth, offsetHeight } = container
    this.container = container
    this.canvas = canvas
    this.width = offsetWidth
    this.height = offsetHeight
    this.needDefaultCreator = needDefaultCreator
    this.axesHelper = axesHelper
    
    // After several attempts
    const [min, max] = [~~(offsetWidth / 6), ~~(offsetWidth / 3.5)]
    this.propSizeRange = [min, max]
    this.propHeight = ~~(max / 2)

    this.stage = null
    this.propCreator = null

    this.init()
  }
  
  // Initialize the stage
  initStage () {
    const { container, canvas } = this
    const { offsetHeight } = container
    const axesHelper = true
    const cameraNear = 0.1
    const cameraFar = 2000
    // Calculate where the camera should be placed
    const cameraInitalPosition = this.cameraInitalPosition = computeCameraInitalPosition(35.225, offsetHeight / 2, offsetHeight / 2, cameraNear, cameraFar)
    const lightInitalPosition = this.lightInitalPosition = { x: - 300..y: 600.z: 200 }
    
    this.stage = new Stage({
      container,
      canvas,
      axesHelper,
      cameraNear,
      cameraFar,
      cameraInitalPosition,
      lightInitalPosition
    })
  }

  // Initialize the item generator
  initPropCreator () {
    const { needDefaultCreator, propSizeRange, propHeight } = this

    this.propCreator = new PropCreator({
      propHeight,
      propSizeRange,
      needDefaultCreator
    })
  }
  
  // Add an interface to the external generatorcreatePropCreator (... args) {this.propCreator.createPropCreator(... args) }Copy the code

So where do I need to put the box next? Open wechat jump a jump, masturbate……

The new box may be generated in two directions, the X-axis and the X-axis, and the distance between the two boxes should be random, but the distance must be limited, the box cannot appear next to the box or the box outside the visual area. PropDistanceRange = [~~(min / 2), Max * 2].

So with that in mind, it seems that we need to implement a method computeMyPosition that calculates the entry position of the box. To calculate the distance of the next box will have to get on a box, at the same time, the game will be generated in the process of a pile of boxes, or thrown away can’t ignore, in order to consider the performance, we also need time to clean up and destroy operation box, so you also need to have a set props to the box of management has been created in this way, Every time you create a box, you just grab the most recent box. Here are a few things to note:

  1. By observing the second box of wechat for many times, WE found that the distance between the second box and the first box is always the same. Therefore, we will deal with the distance of the second box separately.
  2. Except for the fact that the first two boxes are the same distance apart, the first two boxes are the same size, so I have to go backPropCreatorThe default item generator handles it and determines if the first two boxes are fixed size
  3. The box entry animation starts with the third box, and the first two boxes appear directly from the beginning of the game, so the height of the first two boxes must be0I don’t know what the height of the box will be. It’s up to you
  4. When calculating the distance of the box, we need to calculate the size of the box itself, so we need to get the size of the box
// utils.js
export const getPropSize = box= > {
  const box3 = getPropSize.box3 || (getPropSize.box3 = new THREE.Box3())
  box3.setFromObject(box)
  return box3.getSize(new THREE.Vector3())
}

// Prop.js
  getSize () {
    return getPropSize(this.body)
  }
Copy the code

Then Prop class

class Prop {
  constructor ({
    // ...
    enterHeight,
    distanceRange,
    prev
  }) {
    // ...
    this.enterHeight = enterHeight
    this.distanceRange = distanceRange
    this.prev = prev
  }

  // Calculate the position
  computeMyPosition () {
    const {
      world,
      prev,
      distanceRange,
      enterHeight
    } = this
    const position = {
      x: 0.// The first two boxes have y values of 0
      y: enterHeight,
      z: 0
    }

    if(! prev) {// The first box
      return position
    }

    if (enterHeight === 0) {
      // Box 2, fixed a distance
      position.z = world.width / 2
      return position
    }

    const { x, z } = prev.getPosition()
    // random 2 directions x or z
    const direction = Math.round(Math.random()) === 0
    const { x: prevWidth, z: prevDepth } = prev.getSize()
    const { x: currentWidth, z: currentDepth } = this.getSize()
    // Random a distance according to the interval
    constrandomDistance = rangeNumberInclusive(... distanceRange)if (direction) {
      position.x = x + prevWidth / 2 + randomDistance + currentWidth / 2
      position.z = z
    } else {
      position.x = x
      position.z = z + prevDepth / 2 + randomDistance + currentDepth / 2
    }

    return position
  }

  // Put the props into the stage
  enterStage () {
    const { stage, body, height } = this
    const { x, y, z } = this.computeMyPosition()

    body.castShadow = true
    body.receiveShadow = true
    body.position.set(x, y, z)
    // The box needs to be placed on the ground
    body.geometry.translate(0, height / 2.0)
    
    stage.add(body)
    stage.render()
  }

  // Get the item size
  getSize () {
    return getPropSize(this.body)
  }
  
  // ...
}
Copy the code

Now you can implement the box-generation logic

  // JumpGameWorld.js
  // Create the box
  createProp (enterHeight = 100) {
    const {
      height,
      propCreator,
      propHeight,
      propSizeRange: [min, max],
      propDistanceRange,
      stage, props,
      props: { length }
    } = this
    const currentProp = props[length - 1]
    const prop = new Prop({
      world: this,
      stage,
      // The first two boxes are generated with the first creator
      body: propCreator.createProp(length < 3 ? 0 : - 1),
      height: propHeight,
      prev: currentProp,
      enterHeight,
      distanceRange: propDistanceRange
    })
    const size = prop.getSize()

    if(size.y ! == propHeight) {console.warn(` height:${size.y}, the box height must be${propHeight}`)}if (size.x < min || size.x > max) {
      console.warn(` width:${size.x}, the box width must be${min} - ${max}`)}if (size.z < min || size.z > max) {
      console.warn(` depth:${size.z}, the box depth must be${min} - ${max}`)
    }

    prop.enterStage()
    props.push(prop)
  }
Copy the code

And then we initialize it

  init () {
    this.initStage()
    this.initPropCreator()
    // The first item
    this.createProp()
    // The second item
    this.createProp()
  }
Copy the code

At this point, we have implemented the function of randomly generating items, but now the scene is still and there is no way to verify the logic of generating more items, so next, we implement the scene movement first.

Moving scene

Pick up the phone to open wechat jump jump continue pondering……

Regardless of the presence of the person, we can see that every time the box is generated, the scene starts to move. So how do you move it? You can move the camera to make the scene move, there is nothing to worry about, this is the law, just like shooting a movie, the person moves, can your camera not move?

So the question is, where do we move the camera?

  • While setting the camera position, how do you ensure that the latest 2 boxes are properly positioned in the viewable area
  • The size of the box varies, so will half of the boxes appear outside the scene

It doesn’t matter, first pick up the phone to open wechat jump a jump a masturbation……

You’ll notice that after each move, the center point of the scene is roughly the middle of the latest two boxes, but it feels slightly shifted downward, so let’s break it down

This is easy to do. We calculate the point between the two latest boxes, offset this point by one value downward, and add the result to the initial position of the camera to get the position of the camera. Here the offset is agreed to be 1/10 of the height of the apparent cone, then in JumpGameWorld:

  // Calculate the center points of the latest 2 boxes
  getLastTwoCenterPosition () {
    const { props, props: { length } } = this
    const { x: x1, z: z1 } = props[length - 2].getPosition()
    const { x: x2, z: z2 } = props[length - 1].getPosition()

    return {
      x: x1 + (x2 - x1) / 2.z: z1 + (z2 - z1) / 2}}// Move the camera, always look towards the middle of the last 2 balls
  moveCamera () {
    const {
      stage,
      height
      cameraInitalPosition: { x: initX, y: initY, z: initZ }
    } = this
    // Shift the viewable area up a bit to make it look more reasonable
    const cameraOffsetY = height / 10

    const { x, y, z } = this.getLastTwoCenterPosition()
    const to = {
      x: x + initX + cameraOffsetY,
      y: initY, // The height is constant
      z: z + initZ + cameraOffsetY
    }

    // Move the stage camera
    stage.moveCamera(to)
  }
Copy the code

After obtaining the position of the camera, we need to provide the corresponding method in the Stage class, Stage

  // Move the camera
  moveCamera ({ x, z }) {
    const { camera } = this
    camera.position.x = x
    camera.position.z = z
    this.render()
  }
Copy the code

Now that the camera is ready to move, let’s set a timer to test it by setting the box’s Y value to 0

  init () {
    this.initStage()
    this.initPropCreator()
    // The first item
    this.createProp()
    // The second item
    this.createProp()
    // Adjust the camera the first time
    this.moveCamera()

    / / test
    const autoMove = (a)= > {
      setTimeout((a)= > {
        autoMove()
        // The camera needs to be moved every time there is a new item
        this.createProp()
        this.moveCamera()
      }, 2000)
    }
    autoMove()    
  }
Copy the code

Ok, that’s very nice, but there was a problem with testing

  1. When the camera moved a certain distance, the shadow of the prop was no longer visible
  2. The position of the shadow changes every time, which is not the desired effect
  3. The camera needs smooth transition animation
  4. When the camera moves, some of the boxes must be out of view. Are they still useful? How to destroy useless words?

We’ll solve these problems one by one.

The shadow problem, this is because the ground is not big enough, can you make the ground big enough? According to our previous analysis of the camera, yes, because we did not change any Angle of the camera, but only carried out translation, but this is too low, and the maximum value is limited, so we can move the ground every time we move the camera, resulting in the illusion that the ground did not move. So the position of the ground comes out of the air, the position of that central point.

The shadow problem, which is similar to the ground, we can also move the light source with the camera, but the lighting needs to be careful

The direction of a parallel light is from its position to the target position. The default target position is the origin (0,0,0). Note: For the location of the target, it must be added to the scene to change it to any location other than the default.

This means that if the target position of the light changes, a target object must be created and added to the scene. In addition to updating the position of the light source, the target position of the light must be updated

var targetObject = new THREE.Object3D();
scene.add(targetObject);

light.target = targetObject;
Copy the code

Tween.js plugin is used directly. Since there are many other places to use the transition effect, we can simply encapsulate it first

export const animate = (configs, onUpdate, onComplete) = > {
  const {
    from, to, duration,
    easing = k= > k,
    autoStart = true // To use tween's chain
  } = configs

  const tween = new TWEEN.Tween(from)
    .to(to, duration)
    .easing(easing)
    .onUpdate(onUpdate)
    .onComplete((a)= > {
      onComplete && onComplete()
    })

  if (autoStart) {
    tween.start()
  }

  animateFrame()
  return tween
}

const animateFrame = function () {
  if (animateFrame.openin) {
    return
  }
  animateFrame.openin = true

  const animate = (a)= > {
    const id = requestAnimationFrame(animate)
    if(! TWEEN.update()) { animateFrame.openin =false
      cancelAnimationFrame(id)
    }
  }
  animate()
}
Copy the code

The destruction of boxes, which are not visible, is necessary because it can cause significant performance problems when the number is very large. We can do this at an appropriate time, such as cleaning the box every time the camera moves. So how do you tell if the box is in view? First put aside, to solve the first few problems in consideration.

Then modify the moveCamera according to the problems summarized above, do not forget to add a lightTarget object lightTarget, and then need to provide a camera movement completion callback (later used to perform box destruction).

  // Stage.js
  // Center is the center of the two boxes
  moveCamera ({ cameraTo, center, lightTo }, onComplete, duration) {
    const {
      camera, plane,
      light, lightTarget,
      lightInitalPosition
    } = this

    // Move the camera
    animate(
      {
        from: { ...camera.position },
        to: cameraTo,
        duration
      },
      ({ x, y, z }) => {
        camera.position.x = x
        camera.position.z = z
        this.render()
      },
      onComplete
    )

    // Lights and targets also need to move to keep shadows in place
    const { x: lightInitalX, z: lightInitalZ } = lightInitalPosition
    animate(
      {
        from: { ...light.position },
        to: lightTo,
        duration
      },
      ({ x, y, z }) => {
        lightTarget.position.x = x - lightInitalX
        lightTarget.position.z = z - lightInitalZ
        light.position.set(x, y, z)
      }
    )

    // Ensure that the ground does not run out of the limited size
    plane.position.x = center.x
    plane.position.z = center.z
  }
Copy the code

JumpGameWorld, for example

  // Move the camera, always look towards the middle of the last 2 balls
  moveCamera (duration = 500) {
    const {
      stage,
      cameraInitalPosition: { x: cameraX, y: cameraY, z: cameraZ },
      lightInitalPosition: { x: lightX, y: lightY, z: lightZ }
    } = this
    // Set the offset downward to 1/10 of the stage height
    const cameraOffsetY = stage.frustumHeight / 10

    const { x, y, z } = this.getLastTwoCenterPosition()
    const cameraTo = {
      x: x + cameraX + cameraOffsetY,
      y: cameraY, // The height is constant
      z: z + cameraZ + cameraOffsetY
    }
    const lightTo = {
      x: x + lightX,
      y: lightY,
      z: z + lightZ
    }

    // Move the stage camera
    const options = {
      cameraTo,
      lightTo,
      center: { x, y, z }
    }
    stage.moveCamera(
      options,
      () => {
        // Execute box destruction
      },
      duration
    )
  }
Copy the code

Destruction of boxes

We have an idea of when to do it, but what is the basis for it? Obviously the box can be destroyed as long as it is no longer visible, because the scene is moving forward, and the center of the visual area is constantly moving up the X or Z axis. So the first thing that comes to mind is to implement a method to detect whether the box is in the visual area. Threejs also provides the corresponding API to operate, interested friends can go to understand the relevant algorithm, I can’t look at it, the math is too weak. Additionally, the algorithm in Threejs seems to be related to vertices and rays, and the more complex the object (the more vertices), the more computations. Let’s try to look at it another way: is it necessary to calculate whether the box is in the viewable area?

It’s not easy to draw. Assuming that the size of our scene is 200*320, the box size range is [30,60], and the space between boxes is limited [20,100], then we use the minimum safe value to roughly estimate, put 2 boxes 30+20+30, already 80 wide, that is to say, 200 wide across no more than 4. In addition, the center point of our viewable area is in the center of the nearest 2 boxes (regardless of the camera’s lower offset), so when vertical, the range of 160 height can hold up to 3 boxes, plus the one above the center point, which is also 4 boxes. In other words, there could be up to eight boxes in the viewable area at the same time. (You can actually test it if you want to be stingy, but it’s only an estimate, and the error depends on the camera Angle.)

Now, logic already very clear, according to the assumption that when we manage props box set length is more than 8, can perform box destroy operation, and there is no need to clean up after each camera movement, can fix every time cleaning up a few, such as we agreed time clean up four, so every time 12 box destroyed four, And so on……

  // JumpGameWorld.js
  // Destroy items
  clearProps () {
    const {
      width,
      height,
      safeClearLength,
      props, stage,
      props: { length }
    } = this
    const point = 4

    if (length > safeClearLength) {
      props.slice(0, point).forEach(prop= > prop.dispose())
      this.props = props.slice(point)
    }
  }
  
  // Estimate the destruction safety value
  computeSafeClearLength () {
    const { width, height, propSizeRange } = this
    const minS = propSizeRange[0]
    const hypotenuse = Math.sqrt(minS * minS + minS * minS)
    this.safeClearLength = Math.ceil(width / minS) + Math.ceil(height / hypotenuse / 2) + 1
  }
  
  // Prop.js
  / / destroy
  dispose () {
    const { body, stage } = this

    body.geometry.dispose()
    body.material.dispose()
    stage.remove(body)
    // Unreference the previous one
    this.prev = null
  }
Copy the code

Recall that if the algorithm is used to deal with the destruction of boxes, there may be a safe value, why?

If this happens and you don’t set a safe value, the algorithm will tell you that the fourth box from the bottom of the graph is out of view. Should we clean it? Depending on the possible direction of the next box, if the scene moves to the right, the box should appear in the viewable area, not be destroyed.

One problem led to another, and next, we implemented the entry of the box and the drop of the marble

Box bullet ball falls

Adding an animation is actually very simple, you can create the box when it enters the stage, now implement an entranceTransition method

  // Put it on stage
  enterStage () {
    // ...

    this.entranceTransition()
  }
  // Box entry animation
  entranceTransition (duration = 400) {
    const { body, enterHeight, stage } = this

    if (enterHeight === 0) {
      return
    }

    animate(
      {
        to: { y: 0 },
        from: { y: enterHeight },
        duration,
        easing: TWEEN.Easing.Bounce.Out
      },
      ({ y }) => {
        body.position.setY(y)
        stage.render()
      }
    )
  }
Copy the code

At this point, we have realized the main logic of the scene and props, and have begun to take shape.

Little people implement LittleMan

Now to realize the logic of the villain, open wechat jump a jump more than a few……

And then analyze what are the points related to small people?

  1. He has two parts, head and body
  2. There is a momentum process before the jump
  3. The box has an extrusion process when the force is stored
  4. There are special effects surrounding the power storage.
  5. When holding force, the body scales and the head moves down, that is to say, the body part needs to place the scaling origin at the foot of the little person
  6. The box has a rebound animation when it takes off
  7. There’s a flip in the air
  8. There were lingering shadows in the air
  9. The body has a brief cushion when it hits the ground
  10. The ground has special effects when it lands

Below, unpack them one by one……

Draw a mean person

First of all, the head is very simple. It’s a circle. The body part is an irregular cylinder. Since I just touched threejs, I don’t know any shortcut to draw the body part, so HERE I use three geometries to combine the body. Before drawing, we need to look back at the points analyzed to see if we need to pay attention to anything when drawing. The first thing that affects is definitely the zoom function (note that the head does not zoom), which requires drawing with the origin of the scale of the body under his feet, and then the aerial flip, which is not clear at the moment where the origin of the flip is (too fast), which may or may not be the center point of the whole body and head. However, this does not affect our ability to determine that the body and head are a whole (threejs group). As for the origin of the flip, we will deal with it after we make it and debug the effect. So, to be on the safe side, how to draw a picture to describe it

Each dotted box represents a layer of packaging (grid or group). For the small person, if you want to change the rotation origin, you only need to adjust the upper and lower offset positions of the head and body group to do so.

I thought about the opening screen of “jump” in wechat (that is, before the game is started), in which the little man jumps onto the box from a blank place and falls onto the box from the air after the game starts, so there should be an enterStage for the little man to enter, createBody for body creation and jump. so:

class LittleMan {
  constructor ({
    world,
    color
  }) {
    this.world = world
    this.color = color

    this.stage = null
  }

  // Create body
  createBody () {}

  // Enter the stage
  enterStage () {}

  / / jump
  jump () {}
}
Copy the code

We will draw the body first. Since the width of the scene is set according to the width of the viewport, the dynamic size of the small person needs to be calculated.

  // Create body
  createBody () {
    const { color, world: { width } } = this
    const material = baseMeshLambertMaterial.clone()
    material.setValues({ color })

    / / to the head
    const headSize = this.headSize = width * .03
    const headTranslateY = this.headTranslateY = headSize * 4.5
    const headGeometry = new THREE.SphereGeometry(headSize, 40.40)
    const headSegment = this.headSegment = new THREE.Mesh(headGeometry, material)
    headSegment.castShadow = true
    headSegment.translateY(headTranslateY)

    / / body
    this.width = headSize * 1.2 * 2
    this.bodySize = headSize * 4
    const bodyBottomGeometry = new THREE.CylinderBufferGeometry(headSize * 9..this.width / 2, headSize * 2.5.40)
    bodyBottomGeometry.translate(0, headSize * 1.25.0)
    const bodyCenterGeometry = new THREE.CylinderBufferGeometry(headSize, headSize * 9., headSize, 40)
    bodyCenterGeometry.translate(0, headSize * 3.0)
    const bodyTopGeometry = new THREE.SphereGeometry(headSize, 40.40)
    bodyTopGeometry.translate(0, headSize * 3.5.0)

    const bodyGeometry = new THREE.Geometry()
    bodyGeometry.merge(bodyTopGeometry)
    bodyGeometry.merge(new THREE.Geometry().fromBufferGeometry(bodyCenterGeometry))
    bodyGeometry.merge(new THREE.Geometry().fromBufferGeometry(bodyBottomGeometry))

    // Zoom control
    const translateY = this.bodyTranslateY = headSize * 1.5
    const bodyScaleSegment = this.bodyScaleSegment = new THREE.Mesh(bodyGeometry, material)
    bodyScaleSegment.castShadow = true
    bodyScaleSegment.translateY(-translateY)

    // Rotation control
    const bodyRotateSegment = this.bodyRotateSegment = new THREE.Group()
    bodyRotateSegment.add(headSegment)
    bodyRotateSegment.add(bodyScaleSegment)
    bodyRotateSegment.translateY(translateY)

    // Total height = head displacement + head height / 2 = headSize * 5
    const body = this.body = new THREE.Group()
    body.add(bodyRotateSegment)
  }
Copy the code

Then we need to get the little man to walk to the designated position on the stage

  // Enter the stage
  enterStage (stage, { x, y, z }) {
    const { body } = this
    
    body.position.set(x, y, z)

    this.stage = stage
    stage.add(body)
    stage.render()
  }
Copy the code

Initialize in the game, and let the SIMS into the scene

  // JumpGameWorld.js
  // Initialize the SIMS
  initLittleMan () {
    const { stage, propHeight } = this
    const littleMan = this.littleMan = new LittleMan({
      world: this.color: 0x386899
    })
    littleMan.enterStage(stage, { x: 0.y: propHeight, z: 0})}Copy the code

The first step has been completed. Next, we need to get the little man moving and realize his bouncing function.

Realize the little man bounce

Open wechat jump a jump, this needs to carefully mull over……

We can break down the whole bouncing process into two parts: charge -> take off -> parabolic motion -> land -> buffer. Here, charge occurs when the mouse is touchstart or mousedown, and take off occurs when the mouse is released (touchend or mouseup). It is important to note that if a continuous press and release, in front of the dog did not fall to the ground can’t do any action, there is a kind of situation is: if a mouse click when the dog in the air, the ground after a period of time the mouse to loosen, also can’t do any action at this moment, so we can release after pressing the binding event, then loosen after remove it immediately.

  bindEvent () {
    const { container } = this.world
    const isMobile = 'ontouchstart' in document
    const mousedownName = isMobile ? 'touchstart' : 'mousedown'
    const mouseupName = isMobile ? 'touchend' : 'mouseup'
    
    // It's time to jump
    const mouseup = (a)= > {
      if (this.jumping) {
        return
      }
      this.jumping = true
      // The loading action should be stopped
      this.poweringUp = false

      this.jump()
      container.removeEventListener(mouseupName, mouseup)
    }

    // It's time to start
    const mousedown = event= > {
      event.preventDefault()
      // Cannot operate until the jump is complete
      if (this.poweringUp || this.jumping) {
        return
      }
      this.poweringUp = true
      
      this.powerStorage()
      container.addEventListener(mouseupName, mouseup, false)
    }

    container.addEventListener(mousedownName, mousedown, false)}// Enter the stage
  enterStage (stage, { x, y, z }) {
    // ...
    this.bindEvent()
  }
Copy the code

The purpose of storage force is to jump farther, that is to say, the strength determines the distance, we can simulate a range according to the strength * coefficient, at this point, a word comes to my mind, it seems that I have not been in touch with it for n years, and then silently open baidu: oblique throw movement

Oblique throw motion: the motion made by the object with a certain initial velocity is called oblique throw motion when the air resistance can be ignored. A body moves in a uniform curve and its trajectory is a parabola.

Wechat jump is the oblique throw movement? Open it to ponder……

Up and down the watch for a long time, with the dimensional feeling of I can almost tell it “should” is not a uniform variable curve of the diagonal movement, after all, the sport of stant cast formula is under the condition of the air resistance can be ignored is valid, but WeChat jump jump track completely don’t like a symmetric parabola, it looks like this:

This should be more like a drag slant motion, but I can’t find a formula on the Internet for slant motion that takes drag into account, so we might have to change the way we use the slant motion a little bit. Without modification, the y value needs to be calculated by the value of x, so we can not control the curve of y more directly. Now, let’s make a turn. Let’s separate the y motion and keep the constant velocity of the X axis. Let’s create one transition on the X axis and two transitions on the Y axis at the same time. Then, according to the relevant formula of oblique throw motion, we can calculate the horizontal range and shooting height, and the movement time I feel is a fixed value in weixinyi jump, which will not be counted here.

Since we need to use the oblique throw formula, we need to create two variables, velocity V0 and Theta, to simulate the trajectory by increasing v0 and decreasing theta while storing force. Get the formula ready and add a new gravity parameter to JumpGameWorld: gravity G, 9.8 by default

// Oblique throw calculation
export const computeObligueThrowValue = function (v0, theta, G) {
  constSin2 theta = sin ().2 * theta)
  constSine theta = sin (theta)const rangeR = pow(v0, 2Sin2 theta over GconstRangeH = pow(v0 * sinθ,2)/(2 * G)

  return {
    rangeR,
    rangeH
  }
}
Copy the code

Xu li

Then, we now implement the basic logic of storage. All we need to do is increment the slant parameter and scale the little man. We don’t care about the slant parameter value, but we can adjust it after we get the little man moving. Another point to note is that after the storage force is finished, the little man needs to be restored, but not directly. The value at the end of the storage force needs to be saved, and then the little man is restored in the parabolic movement stage, so that the effect is smooth.

  resetPowerStorageParameter () {
    this.v0 = 20
    this.theta = 90

    // Due to the deformation caused by storage, need to record, in the air to restore the small man
    this.toValues = {
      headTranslateY: 0.bodyScaleXZ: 0.bodyScaleY: 0
    }
    this.fromValues = this.fromValues || {
      headTranslateY: this.headTranslateY,
      bodyScaleXZ: 1.bodyScaleY: 1}}/ / xu li
  powerStorage () {
    const { stage, bodyScaleSegment, headSegment, fromValues, bodySize } = this

    this.resetPowerStorageParameter()

    const tween = animate(
      {
        from: { ...fromValues },
        to: {
          headTranslateY: bodySize - bodySize * 6..bodyScaleXZ: 1.3.bodyScaleY: 6.
        },
        duration: 1500
      },
      ({ headTranslateY, bodyScaleXZ, bodyScaleY  }) => {
        if (!this.poweringUp) {
          // Stop charging while lifting
          tween.stop()
        } else {
          this.v0 *= 1.008
          this.theta *= 99.

          headSegment.position.setY(headTranslateY)
          bodyScaleSegment.scale.set(bodyScaleXZ, bodyScaleY, bodyScaleXZ)
          
          // Save this position for restoration
          this.toValues = {
            headTranslateY,
            bodyScaleXZ,
            bodyScaleY
          }

          stage.render()
        }
      }
    )
  }
Copy the code

Now press the mouse, you should be able to see the effect of the little man storage, next we need to achieve the effect of the little man on the box extrusion.

Box extrusion effect

There were a couple of questions that I didn’t even think about before, where do I stand when I enter the stage? After I receive my next instructions, where do I go next?

  1. I may be standing on a box, or I may be on the ground (i.e. before clicking to start the game, the little man jumps onto the box from the ground)
  2. My goal should be the next box

Based on the above analysis, the villain should know which box currentProp he is currently in, which might be null, and of course which box nextProp is the next to be targeted.

First, determine when to set currentProp for the current box. We implemented the enterStage method to enter the stage before, so it should be clear that this method only enters the stage and has nothing to do with the box. So now we need to jump to the first box after the small person enters the stage and jump according to the view of wechat:

  1. Without clicking to start the game, go to the edge of the stage (except on items), and thenStant cast movementJump to the first box
  2. After clicking to start the game, the little man appears directly above the first box, and thenThe marbles fallJump to the first box, and for that we need to implement it separately

If the above analysis is not very clear, suggest you take your mobile phone to open wechat jump a jump more than a few……

So what to do is very clear, under the dog to enter the stage set the goal of a jump box, then execute the jump, jump after it is set into the box, at the same time set the box next to the next leap target, here can generate place it back in the box associated with the next, convenient processing

// JumpGameWorld.js
  // Create the box
  createProp (enterHeight = 100) {
    // ...
    
    // The next link is used for the villain to find the target
    if (currentProp) {
      currentProp.setNext(prop)
    }

    prop.enterStage()
    props.push(prop)
  }
  
// Prop.js
  setNext (next) {
    this.next = next
  }

  getNext (next) {
    return this.next
  }
  / / destroy
  dispose () {
    const { body, stage, prev, next } = this
    // Disassociate the reference
    this.prev = null
    this.next = null
    if (prev) {
      prev.next = null
    }
    if (next) {
      next.prev = null
    }

    body.geometry.dispose()
    body.material.dispose()
    stage.remove(body)
  }
Copy the code
  // LittleMan.js
  enterStage (stage, { x, y, z }, nextProp) {
    const { body } = this

    body.position.set(x, y, z)
    this.stage = stage
    // Tell the target when entering the stage
    this.nextProp = nextProp

    stage.add(body)
    stage.render()
    this.bindEvent()
  }
  
  / / jump
  jump () {
    const {
        stage, body,
        currentProp, nextProp,
        world: { propHeight }
    } = this
    const { x, z } = body.position
    const { x: nextX, z: nextZ } = nextProp.position

    // At the beginning of the game, the little man enters the game directly above the first box to do the pinball drop
    if(! currentProp && x === nextX && z === nextZ) { body.position.setY(propHeight)this.currentProp = nextProp
      this.nextProp = nextProp.getNext()
    } else {
      // ...
    }
    
    stage.render()
  }
Copy the code

The specific jump animation will be solved later. Now that you know which box to stand on, you can happily squeeze it. So how should we achieve the specific extrusion effect? The power storage of the little man has been realized in the front. According to the effect of a jump on wechat, the squeezing effect also transitions during the power storage period. When the box is squeezed, the overall Y-axis position of the little man also needs to be updated

  // Initializes slant throw parameters
  resetPowerStorageParameter () {
    // ...
    
    this.toValues = {
      // ...
      propScaleY: 0
    }
    this.fromValues = this.fromValues || {
      // ...
      propScaleY: 1}}/ / xu li
  powerStorage () {
    const {
      stage,
      body, bodyScaleSegment, headSegment,
      fromValues,
      currentProp,
      world: { propHeight }
    } = this

    // ...

    const tween = animate(
      {
        from: { ...fromValues },
        to: {
          // ...
          propScaleY: 8.
        },
        duration: 1500
      },
      ({ headTranslateY, bodyScaleY, bodyScaleXZ, propScaleY }) => {
        if (!this.poweringUp) {
          // Stop charging while lifting
          tween.stop()
        } else {
          // ...
          
          currentProp.scale.setY(propScaleY)
          body.position.setY(propHeight * propScaleY)

          // ...

          stage.render()
        }
      }
    )
  }
Copy the code

Now, the extrusion effect has been realized. Next, to analyze the jump process, please open wechat jump……

takeoff

It’s super fast, I can’t see it, so I’ll analyze it myself. First of all, according to the common sense of life, the initial speed of the little man jumping should be greater than the rebound speed of the box, and there should be no collision before the box bounces to the top. Then we can open two animations at the same time, one is the rebound of the box, and the other is the oblique throwing movement of the little man.

The first animation implements a springbackTransition for the box:

  // Rebound animation
  springbackTransition (duration) {
    const { body, stage } = this
    const y = body.scale.y
    
    animate(
      {
        from: { y },
        to: { y: 1 },
        duration,
        easing: TWEEN.Easing.Bounce.Out
      },
      ({ y }) => {
        body.scale.setY(y)
        stage.render()
      }
    )
  }
Copy the code

The second animation is the parabolic motion of the little man, which has been analyzed. The X-axis moves at a uniform speed, and the Y-axis is divided into two segments. The ascending segment is deceleration, and the descending segment is acceleration. The jump process, in addition to parabolic movement, also includes a large buffer, the buffer is 2 period change in theory, but due to the change very fast here, I think it is hard to identify to the naked eye, so only look at the effect of the setting during the second half of the first, at the same time, the end of the buffer time point should be the end point of the whole process of jumping.

In addition, the movement direction of the little man may be X-axis or Z-axis, so it is necessary to determine the direction of the little man first. We can determine the direction by comparing the X-value and z-value of the two boxes. If X is equal, the direction is z-axis, otherwise it is X-axis

Now, with direction, curve of motion, range determined, can we begin? Too young too simple, can we use this range directly for the small man’s offset on the X or Z axis?

Pictured above, assume that dog won’t jump out of the box, first clear, each jump are need to aim at the center of the next box, as for can fall on the center point accurately, it is uncertain, is determined by the range, but will not jump out from the takeoff point to the next box center is connected to the line, now, will have a further decomposition under:

As can be seen from the rules in the figure, given the straight-line distance and coordinate difference between C1 and P2, the offsets in the X and Z axes can be calculated according to similar triangular row characteristics. So now we have a set of formulas, we get the real x and z, and we implement a computePositionByRangeR method.

@param {Object} c1 start point * @param {Object} p2 Target box center point */
export const computePositionByRange = function (range, c1, p2) {
  const { x: c1x, z: c1z } = c1
  const { x: p2x, z: p2z } = p2

  const p2cx = p2x - c1x
  const p2cz = p2z - c1z
  const p2c = sqrt(pow(p2cz, 2) + pow(p2cx, 2))

  const jumpDownX = p2cx * range / p2c
  const jumpDownZ = p2cz * range / p2c

  return {
    jumpDownX: c1x + jumpDownX,
    jumpDownZ: c1z + jumpDownZ
  }
}
Copy the code

Before then we will summarize the takeoff logic implementation, including figures for the first time the pinball whereabouts, because I found after implementation experience if the storage time is short, calculated at high value is a little low (and WeChat experience difference a bit big), so I write directly to shoot high dead a minimum 😄, looks and WeChat jump jump experience closer to some.

  / / jump
  jump () {
    const {
      stage, body,
      currentProp, nextProp,
      world: { propHeight }
    } = this
    const duration = 400
    const start = body.position
    const target = nextProp.getPosition()
    const { x: startX, y: startY, z: startZ } = start

    // At the beginning of the game, the little man enters the game directly above the first box to do the pinball drop
    if(! currentProp && startX === target.x && startZ === target.z) { animate( {from: { y: startY },
          to: { y: propHeight },
          duration,
          easing: TWEEN.Easing.Bounce.Out
        },
        ({ y }) => {
          body.position.setY(y)
          stage.render()
        },
        () => {
          this.currentProp = nextProp
          this.nextProp = nextProp.getNext()
          this.jumping = false})}else {
      if(! currentProp) {return
      }

      const { bodyScaleSegment, headSegment, G } = this
      const { v0, theta } = this.computePowerStorageValue()
      const { rangeR, rangeH } = computeObligueThrowValue(v0, theta * (Math.PI / 180), G)

      // Horizontal uniform speed
      const { jumpDownX, jumpDownZ } = computePositionByRangeR(rangeR, start, target)
      animate(
        {
          from: {
            x: startX,
            z: startZ, ... this.toValues },to: {
            x: jumpDownX,
            z: jumpDownZ, ... this.fromValues }, duration }, ({ x, z, headTranslateY, bodyScaleXZ, bodyScaleY }) => { body.position.setX(x) body.position.setZ(z) headSegment.position.setY(headTranslateY) bodyScaleSegment.scale.set(bodyScaleXZ, bodyScaleY, bodyScaleXZ) } )// the y axis is up and down
      const rangeHeight = Math.max(60, rangeH) + propHeight
      const yUp = animate(
        {
          from: { y: startY },
          to: { y: rangeHeight },
          duration: duration * 65..easing: TWEEN.Easing.Cubic.Out,
          autoStart: false
        },
        ({ y }) => {
          body.position.setY(y)
        }
      )
      const yDown = animate(
        {
          from: { y: rangeHeight },
          to: { y: propHeight },
          duration: duration * 35..easing: TWEEN.Easing.Cubic.In,
          autoStart: false
        },
        ({ y }) => {
          body.position.setY(y)
        }
      )

      // After landing, generate the next block -> Move the camera -> update the box of concern -> End
      const ended = (a)= > {
        const { world } = this
        world.createProp()
        world.moveCamera()

        this.currentProp = nextProp
        this.nextProp = nextProp.getNext()
        // The jump is over
        this.jumping = false
      }
      // Drop buffer
      const bufferUp = animate(
        {
          from: { s: 8. },
          to: { s: 1 },
          duration: 100.autoStart: false
        },
        ({ s }) => {
          bodyScaleSegment.scale.setY(s)
        },
        () => {
          // The jump ends at the end of the landing buffer
          ended()
        }
      )

      // Up -> down -> landing buffer
      yDown.chain(bufferUp)
      yUp.chain(yDown).start()

      // Need to handle different direction flip
      const direction = currentProp.getPosition().z === nextProp.getPosition().z
      this.flip(duration, direction)

      // Bounce back from the start
      currentProp.springbackTransition(500)
    }

    stage.render()
  }

  

  / / airspring
  flip (duration, direction) {
    const { bodyRotateSegment } = this
    let increment = 0

    animate(
      {
        from: { deg: 0 },
        to: { deg: 360 },
        duration,
        easing: TWEEN.Easing.Sinusoidal.InOut
      },
      ({ deg }) => {
        if (direction) {
          bodyRotateSegment.rotateZ(-(deg - increment) * (Math.PI/180))}else {
          bodyRotateSegment.rotateX((deg - increment) * (Math.PI/180))
        }
        increment = deg
      }
    )
  }
Copy the code

Ok, now the little man can jump and always face the center of the next box. The game is in full scale. But now there is an obvious problem, is the change of the storage value, then adjust the storage value.

Optimization of storage value

RequestAnimationFrame is not stable, so it has to be handled in a different way. How about using a timer? In fact, timer is not necessarily a time (on time), the most reliable method is to record the time when a mouse is pressed, and then according to the time difference when the mouse is released to calculate the value of storage, but this time difference has a maximum value, is the maximum time of storage. Now implement a computePowerStorageValue method to calculate the storage value through time, and then replace the parameters in the jump method.

  computePowerStorageValue () {
    const { powerStorageDuration, powerStorageTime, v0, theta } = this
    const diffTime = Date.now() - powerStorageTime
    const time = Math.min(diffTime, powerStorageDuration)
    const percentage = time / powerStorageDuration

    return {
      v0: v0 + 30 * percentage,
      theta: theta - 50 * percentage
    }
  }
Copy the code

This thought a few days can write about, did not expect the estimation error is too big, I will continue to update after a few days……

If it is helpful to you, please give a compliment, thank old iron!