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
-
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
-
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
-
-
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
-
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
-
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
-
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
-
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 destroyed
dispose
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
< a
To calculate they
(You can take a value within an interval) - known
< b
To 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:
- 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.
- 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 back
PropCreator
The default item generator handles it and determines if the first two boxes are fixed size - 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 be
0
I don’t know what the height of the box will be. It’s up to you - 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
- When the camera moved a certain distance, the shadow of the prop was no longer visible
- The position of the shadow changes every time, which is not the desired effect
- The camera needs smooth transition animation
- 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?
- He has two parts, head and body
- There is a momentum process before the jump
- The box has an extrusion process when the force is stored
- There are special effects surrounding the power storage.
- 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
- The box has a rebound animation when it takes off
- There’s a flip in the air
- There were lingering shadows in the air
- The body has a brief cushion when it hits the ground
- 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?
- 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)
- 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:
- Without clicking to start the game, go to the edge of the stage (except on items), and then
Stant cast movement
Jump to the first box - After clicking to start the game, the little man appears directly above the first box, and then
The marbles fall
Jump 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!