跪拜 Guibai
← Back to the summary

Faking 3D Depth in a 2D Game Engine with Four Points and a Fake Z-Axis

Cover image

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. Insert image description here

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.

The key is actually the music part

Platform part:

Ball part:

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:

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.

Extracted from the video, for learning purposes only

The entire example only needs three images:

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:

Structure

The responsibilities of each node are as follows:

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.

Perspective

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:

Core source code

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.

Still a bit short

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:

Scale

For example:

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:

Getting interesting

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.

By now, you have beaten 89% of your peers

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:

By now, you have beaten 90% of your peers

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:

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

The boss said this game is very popular recently and told me to copy it, but I can't even understand how to play it...

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...

The boss said the jigsaw puzzle game market is too competitive, asked me to make a 3D version with Cocos...

Dare to challenge replicating the once popular Cut the Rope game with Cocos 3.8?

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

努力锻炼的志勇

Great idea, learned something, big shot.