Faking 3D Depth in a 2D Game Engine with Four Points and a Fake Z-Axis
Introduction
Hello everyone, I am Yiyuan Programmer, a lead programmer with 8 years of experience in the game industry.
A few days ago, the project manager threw over a game video: a ball bounces continuously to the music, platforms fly in from the distance, enlarge and disappear when stepped on, and the whole scene has a strong sense of spatial depth.
The manager's request was also very direct:
Embed this music ball gameplay into our own project.
After watching the video, I took another look at the project in hand.
The video was a 3D game, but our project is a pure 2D project.
Our project is a pure 2D project. Adding models, materials, lighting, a 3D camera, and an entire set of spatial logic just for one independent gameplay feature would be a significant change.
So I honestly told the manager:
"This is a 3D effect, but our current project is 2D, so we can't do it directly."
I thought that would be the end of it, but the manager was silent for a few seconds and then added:
"Then can you make one using 2D?"
The fewer the words, the bigger the requirement.
Since we couldn't actually build a 3D world, there was only one path left:
Use 2D to trick the player's eyes.
Back to the topic, in this article, we will use Cocos Creator 3.8.7, without 3D models, without a perspective camera, relying only on a background, a platform, a ball, and four positioning points, to implement a pseudo-3D music ball.
Let's Analyze First
Making a 3D game usually requires modeling, a camera, lighting, and collision.
But don't rush to open your modeling software yet.
We pause the video and observe frame by frame, and we'll find that this gameplay doesn't actually need to express that much content.
Platform part:
- 1. The platform moves from the distance towards the foreground.
- 2. The closer the platform is to the camera, the larger it appears.
- 3. The distant runway is very narrow, while the near runway is very wide.
- 4. Near platforms can occlude distant platforms.
Ball part:
- 1. The ball stays in a fixed area at the bottom of the screen.
- 2. Platforms continuously approach the ball.
- 3. When each platform reaches a fixed position, the ball lands exactly on it.
- 4. After being stepped on, the platform enlarges and disappears.
Change the Project to 3D?
Some friends might be eager to ask: why not just change the project to support 3D?
Let me clarify here, it's not that we can't use 3D.
If the project itself were 3D, or if the gameplay required freely rotating the camera, real spatial collision, and multi-angle observation, then using 3D directly would certainly be more appropriate.
But the current requirements have several characteristics:
- The original project is a pure 2D project.
- The music ball is just one independent gameplay feature (a sub-gameplay).
- The game camera is always fixed and does not need to rotate freely.
- The player will only see the platform approaching from the front.
- Preparing models, materials, and lighting just for this gameplay feature has a relatively high development cost.
In other words, the manager wants an effect that looks like 3D, and didn't ask us to actually model anything.
Since the camera is fixed, and the player judges depth mainly through perspective (near large, far small), converging lines, and front-back occlusion, we just need to assemble these visual cues.
Resource Preparation
For this practical exercise, we directly take screenshots from the video to get the resources we need.
The entire example only needs three images:
background.png: The game background, size750 × 1334.platform.png: A platform image with a transparent channel.ball.png: A ball image with a transparent channel.
The background is responsible for providing the distant horizon, runway, and overall atmosphere; the platform must be separated out because it needs to be constantly moved, scaled, and duplicated later; the ball also needs to be independent for playing the bouncing effect.
Create the following node structure in Cocos Creator:
The responsibilities of each node are as follows:
background: The background node, also the mounting node for the pseudo-3D script.platform: The platform template, which will be duplicated into multiple platforms at runtime.ball: The glowing ball the player sees.p: The parent node for the four perspective positioning points.p1, p2: The left and right boundaries of the near end of the runway.p3, p4: The left and right boundaries of the far end of the runway.
The Key to Pseudo-3D: Perspective
Let's not write code yet, but first determine the perspective range of the runway on the background.
Place p1, p2 at the bottom of the screen, aligning them with the left and right sides of the near end of the runway respectively; place p3, p4 near the horizon, aligning them with the left and right sides of the far end of the runway.
The four points will form a trapezoid:
p3 ------ p4
Far end
│
│
│
Near end
p1 ---------------- p2
The platform will start from the midpoint of p3, p4 and move along the center line of the runway to the midpoint of p1, p2.
These four points are the track skeleton of the entire pseudo-3D world.
"Z-Axis"
2D nodes only have x and y, with no real spatial depth.
If there's no condition, we create the condition.
For each platform, store an additional depth:
type PlatformItem = {
node: Node;
depth: number;
};
The depth here does not belong to Cocos's coordinate system; it's just a number we define ourselves.
The convention is as follows:
| depth | Platform state |
|---|---|
| Less than 0 | Not yet entered the screen |
| Equal to 0 | Located at the far end of the runway |
| 0 to 1 | Moving from far to near |
| Equal to 1 | Reached the near end of the runway |
| Greater than 1 | Continuing to move out of the camera view |
Every frame, let this number continuously increase:
item.depth += depthSpeed * dt;
depthSpeed controls the speed at which the platform advances, and dt is the time elapsed in the current frame.
The platform node still only moves on the 2D canvas, but with depth, we have a fake Z-axis we created ourselves.
Calculating Platform Position Based on depth
With depth, the next problem to solve is: where should the platform be placed at each depth?
First, get the midpoints of the near and far ends, then calculate the platform's current position using math.lerp:
lerp is linear interpolation, which can be understood as:
Walking from A to B, what percentage of the way have you gone now?
When depth = 0, it returns the far-end position; when depth = 0.5, the platform is halfway between the far and near ends; when depth = 1, the platform reaches the near end.
Then set the result to the platform:
item.node.setPosition(x, y);
After this step is done, the platform can already move from the horizon to the bottom of the screen, but it looks more like a sticker of constant size sliding across the screen, still a bit short of 3D.
Platforms Also Need to Be Larger When Near and Smaller When Far
A very important basis for the human eye to judge the distance of an object is size.
The same object looks small when far away and large when near.
Therefore, the platform's position is controlled by depth, and its scale must also be controlled by the same depth:
For example:
farScale = 0.12: The platform is only 12% of its normal size when it first enters the far end.nearScale = 1: The platform returns to its normal size when it reaches the near end.
Directly using linear scaling already shows a depth effect, but the change can be a bit mechanical.
We can first apply a smoothing process to the depth:
const t = math.clamp01(depth);
const smooth = t * t * (3 - 2 * t);
const scale = math.lerp(farScale, nearScale, smooth);
This curve makes the platform's change relatively gentle in the distance, and the enlargement becomes more obvious as it approaches the camera, which is visually more natural than a completely uniform scaling.
One principle must be noted here:
The platform's position and size must be controlled by the same depth.
If the platform's position is still in the distance, but its size is already close to the screen width, the player will immediately realize this isn't perspective, just a picture running around randomly.
The effect is as follows:
Synchronizing the Ball's Bounce with the Platform
The platform is moving normally, now it's the ball's turn.
The most direct approach is to give the ball an infinitely looping up-and-down animation.
But this has a problem: the ball's animation and the platform's movement are unrelated to each other.
As long as the runtime is slightly long, or the platform speed is modified, the ball will gradually desynchronize from the platform and end up very earnestly stepping on air.
Therefore, the ball's bounce cannot have its own rhythm.
When the platform reaches the landing point determines when the ball falls.
We first define a ballContactDepth, representing the depth of the platform when the ball steps on it.
Then find the next platform that is about to pass this landing point:
Then calculate the current bounce progress based on the platform's depth:
phase will gradually increase from 0 to 1.
Next, use a sine function to calculate the jump height:
It just happens to form an arched curve:
| phase | jump | Ball's state |
|---|---|---|
| 0 | 0 | Jumping off the platform |
| 0.5 | 1 | Reaching the highest point |
| 1 | 0 | Landing on the next platform |
Finally, add the jump height to the ball's base position:
ball.setPosition(x, baseY + jump * jumpHeight);
Because phase comes from the platform's depth, after modifying depthSpeed, the ball's bounce rhythm will also automatically change accordingly.
After modifying depthGap, the platform spacing and the ball's bounce cycle will also change together.
Adding Platform Step Feedback
Now the ball can land on the platform in rhythm, but if the platform just continues to pass under the ball, it's still hard for the player to feel the "step".
Therefore, after the platform reaches the contact depth, add a very short step animation:
Now it no longer looks like the platform is just passing under the ball's feet, but rather the ball truly steps on and smashes through the platform.
Taking It Further
This issue only demonstrated the most basic center-line platform and automatic bouncing.
Based on the existing structure, it can be further expanded:
- Add left, center, and right tracks.
- Use taps or swipes to control the ball switching tracks.
- Control the platform generation rhythm based on the music's BPM.
- Add
Perfect,Good,Missjudgments. - Add a score and combo system.
- Create normal platforms, speed-up platforms, and special platforms.
- Add ball trails, landing particles, and camera shake.
- Dynamically generate platform sequences based on song configuration.
Especially the music rhythm part, the next step should not continue using a fixed depthGap, but instead calculate the initial depth of each platform based on the song's beat timing.
This way, the moment the platform reaches the landing point can truly align with the music's drum beats.
Conclusion
The above is the development experience of implementing a pseudo-3D game effect in a 2D project. I wonder if any friends have better ideas? Tell everyone in the comments.
I am "Yiyuan Programmer", a lead programmer with 8 years of experience in the game industry. In game development, I hope to help you, and through you, help everyone else.
To be honest, I'd like a like and a heart! Please share this article with other friends you think might need it. Thank you!
Recommended articles:
Yiyuan Cocos Mini-Game Practical Collection 2.0
Yiyuan Cocos Mini-Game Practical Collection 1.0
This game is worth 6.8 billion, aren't you going to practice it? Let's arrange it!
A friend said my jigsaw puzzle game can't batch with Mask...
Who can't make Tetris... huh? Quicksand version?
A recently popular jigsaw puzzle game, the boss asked me to make one with Cocos 3.8...
Dare to challenge replicating the once popular Cut the Rope game with Cocos 3.8?
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Great idea, learned something, big shot.