Original: html5gamedev.org/?p=2383
Always in a self-thought very advanced concept soon after the Internet to find the realization of others!
Today, let's go into a world where we can reach a touch. In this article, we will quickly complete the first person exploration from scratch. This article does not deal with complex mathematical calculations, only the use of light projection technology. You may have seen this technology, such as "The Elder Scrolls 2: Dagger Rain", "The Duke of Doom 3D" and Notch Persson's recent entries on the Ludum Dare. Notch thinks it's good enough, I think it's good enough!
Using the light projection is like hanging, as a lazy programmer, I am very fond of the oil. You can easily immerse yourself in the 3D environment without being constrained by the "true 3D" complexity. For example, the ray-Casting algorithm consumes linear time, so it can load a huge world without optimization, and it executes as fast as a small world. The horizontal plane is defined as a simple mesh rather than a polygon mesh tree, so even without a 3D modeling foundation or a PhD in mathematics, you can go directly into the study.
With these tips, it's easy to do something that makes people explode. After 15 minutes, you'll be filming the walls of your office and checking your HR documentation to see if there are any rules prohibiting "workplace shootout modeling"
Players
Where do we project the light? This is the role of the Player object (player) and requires only three attribute x,y,direction.
12345 |
function Player(x, y, direction) { this .x = x; this .y = y; this .direction = direction; } |
Map
We save the map as a simple two-dimensional array. In the array, 0 represents no wall, and 1 represents a wall. You can also do more complex things, such as setting a wall to any height, or wrapping a "floor (stories)" of multiple wall data into an array. But as our first attempt, using 0-1 is enough.
?
1234 |
function Map(size) { this .size = size; this .wallGrid = new Uint8Array(size * size); } |
Cast a beam of light
Here's the trick: the Ray-casting engine doesn't draw the entire scene at once. Instead, it divides the scene into separate columns and renders them one by two. Each column represents a light that is projected from the player's specific angle. If the light touches the wall, the engine calculates the player's distance to the wall and draws a rectangle in the column. The height of the rectangle depends on the length of the light-the farther away the shorter.
The more light you draw, the smoother the display will be.
1. Find the angle of each light
We first find out the angle of each ray projection. The angle depends on three points: the direction the player faces, the camera's field of view, and the column being painted.
?
12 |
var angle = this .fov * (column / this .resolution - 0.5); var ray = map.cast(player, player.direction + angle, this .range); |
2. Track each ray through the grid
Next, we'll examine the walls that each light passes through. The goal here is to finally come up with an array that lists each wall that the light passes after it leaves the player.
Starting with the player, we find the closest horizontal (stepx) and longitudinal (stepy) grid coordinate lines. Move to the nearest place and check if there is a wall (inspect). Repeat the check until you have traced all the lengths of each line.
?
12345678910 |
function ray(origin) {
var stepX = step(sin, cos, origin.x, origin.y);
var stepY = step(cos, sin, origin.y, origin.x,
true
);
var nextStep = stepX.length2 < stepY.length2
? inspect(stepX, 1, 0, origin.distance, stepX.y)
: inspect(stepY, 0, 1, origin.distance, stepY.x);
if (nextStep.distance > range)
return [origin];
return [origin].concat(ray(nextStep));
}
|
Finding the intersection of meshes is simple: Just take the x down (...). ), then multiply the slope of the Light (Rise/run) to derive Y.
?
12 |
var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x; var dy = dx * (rise / run); |
Do you see the highlights of this algorithm now? We don't care how big the map is! Just focus on the specific points on the grid-roughly the same as the number of points per frame. The map in the sample is 32x32, and 32,000x32,000 's map runs so fast!
3. Draw a column
After tracing a light, we will draw all the walls that it passes through on the path.
?
12 |
var z = distance * Math.cos(angle); var wallHeight = this .height * height / z; |
We feel its height by dividing the height of the wall by the maximum of Z. The farther the wall, the shorter it will be drawn.
Well, what's with the Cos here? If the original distance is used directly, a super wide-angle effect (Fisheye lens) is produced. Why? Imagine you are facing a wall, the left and right edge of the wall is farther from you than the center of the wall. So the vertical wall Center will swell up! To render the wall in the way we actually see it, we construct a triangle with each ray of the projection and calculate the vertical distance through the Cos.
I assure you, this is the hardest math in this article.
Render out
We use the Camera object camera to draw each frame of the map from the player's perspective. When we sweep the screen from left to right, it is responsible for rendering each column.
Before we draw the wall, we render a Sky box (skybox)--A large background, a star and a horizon, and we'll put a weapon in the foreground after we finish drawing the wall.
12345 |
Camera.prototype.render = function (player, map) { this .drawSky(player.direction, map.skybox, map.light); this .drawColumns(player, map); this .drawWeapon(player.weapon, player.paces); }; |
The most important properties of a camera are resolution (resolution), Field of view (FOV), and Range (range).
- The resolution determines how many columns to draw per frame, that is, how many rays to cast.
- The horizon determines the width we can see, the angle of the light.
- The range determines how far we can look, that is, the maximum length of the light.
Combine them
Use control object controls to monitor the arrow keys (and Touch events). Use the game Loop object Gameloop call the Requestanimationframe request to render the frame. The gameloop here are only three lines.
?
12345 |
oop.start( function frame(seconds) { map.update(seconds); player.update(controls.states, map, seconds); camera.render(player, map); }); |
Details Rain
Raindrops are simulated with a large number of randomly placed short walls.
?
123456 |
var rainDrops = Math.pow(Math.random(), 3) * s; var rain = (rainDrops > 0) && this .project(0.1, angle, step.distance); ctx.fillStyle = ‘#ffffff‘ ; ctx.globalAlpha = 0.15; while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height); |
Instead of drawing the full width of the wall, it draws the width of a pixel.
Lighting and Lightning
Lighting is actually a shading process. All walls are painted in full brightness and then covered with a black rectangle with a certain opacity. The opacity is determined by the distance and the direction of the wall (n/s/e/w).
?
123 |
ctx.fillStyle = ‘#000000‘ ; ctx.globalAlpha = Math.max((step.distance + step.shading) / this .lightRange - map.light, 0); ctx.fillRect(left, wall.top, width, wall.height); |
To simulate lightning, the map.light randomly reaches 2 and then fades out quickly.
Collision detection
To prevent the player from wearing a wall, we just use the location he wants to compare to the map. Check the x and Y players separately to glide against the wall.
?
123456 |
Player.prototype.walk =
function
(distance, map) {
var dx = Math.cos(
this
.direction) * distance;
var dy = Math.sin(
this
.direction) * distance;
if (map.get(
this
.x + dx,
this
.y) <= 0)
this
.x += dx;
if (map.get(
this
.x,
this
.y + dy) <= 0)
this
.y += dy;
};
|
Wall Map
Walls that don't have stickers (texture) will look boring. But how do we map a part of a decal to a particular column? This is actually very simple: take the decimal part of the intersection coordinates.
?
12 |
step.offset = offset - Math.floor(offset); var textureX = Math.floor(texture.width * step.offset); |
For example, the intersection of a wall is (10,8.2), so take the decimal part of 0.2. This means that the intersection is 20% far away from the left edge of the wall (8) and 80% far from the right edge of the wall (9). So we use 0.2 * texture.width to derive the x-coordinate of the map.
Give it a try. Stroll through the ruins of terror.
What do you do next?
Because light projectors are so fast and simple, you can quickly implement many ideas. You can be a dungeon Seeker (Dungeon Crawler), first-person shooter, or Grand theft-auto sandbox. By! Constant-level time consumption really makes me want to be an old-fashioned massively multiplayer online role-playing game that contains a large number of worlds that are automatically generated by the program. Here are some of the problems that start with you:
- Immersion experience. The example asks you to add a full screen, mouse positioning, rain background, and lightning when you have a thunder ring.
- Indoor level. Replace the Sky box with a symmetrical gradient. Or, if you think you're a dick, try using porcelain tiles to render floors and ceilings. (Think of it this way: After all the walls are painted, the rest of the screen is the floor and ceiling)
- Lighting objects. We've got a pretty robust lighting model. Why not put the light on the map and calculate the wall illumination? The light source accounts for 80% of the atmosphere.
- A good touch event. I've taken care of some basic touch operations, mobile and tablet partners can try the same demo. But there is a huge room for improvement.
- Camera effects. such as zooming in and out, blurring, drunken patterns and so on. With light projectors, these are especially simple. Start by modifying Camera.fov from the console.
Source Address: https://github.com/hunterloftis/playfuljs/
Original link: http://www.playfuljs.com/a-first-person-engine-in-265-lines
"Go" 265 lines of code to implement the first person game engine