Game Development 27 min read

Building a High‑Performance 818 3D Runner with Oasis Engine: Design & Optimization

This article shares a detailed post‑mortem of the 818 3D runner game developed with Oasis Engine, covering track and character design, shader tricks, asset reduction, memory and performance optimizations, code structure, and troubleshooting of crashes and overheating, offering practical insights for developers building similar high‑traffic mobile games.

Alipay Experience Technology
Alipay Experience Technology
Alipay Experience Technology
Building a High‑Performance 818 3D Runner with Oasis Engine: Design & Optimization

Preface

The 818 promotion used a parkour game as the main mechanic. Using the latest Oasis engine version, we achieved an average FPS of 58.85 for millions of users.

Game Design

Track Design

The track is modeled in two parts:

Central road

Side flower beds

The central road is a 24.5 m radius arc with a 12° central angle. The mesh is static; we replace its texture to create the running surface.

The side flower beds are positioned and rotated based on the road radius and angle. A circular motion component moves them backward and rotates them around the X‑axis, looping when they exit the view.

Lighting

All models use baked lighting to avoid runtime shader calculations. Baking improves performance but can cause visual inconsistencies if not unified across assets.

Baked result

Unbaked result

Coins

Coins are pooled; 20 are pre‑generated for a typical run. Their positions are calculated from the road radius and angle. If the pool empties, new coins are created dynamically.

Character

The character is a glTF model with multiple animation clips. When colliding with an obstacle, the running animation switches to a stunned one for two seconds.

Character Shadow Implementation

Shadows are generated via planar projection in the vertex shader, projecting each vertex onto the X‑O‑Z plane based on a light direction. The core shader code:

attribute vec4 POSITION;
vec3 ShadowProjectPos(vec4 vertPos) {
  vec3 worldPos = (u_modelMat * vertPos).xyz;
  float lightHeight = u_lightDirAndHeight.w;
  vec3 lightDir = normalize(u_lightDirAndHeight.xyz);
  vec3 shadowPos;
  shadowPos.y = min(worldPos.y , lightHeight);
  shadowPos.xz = worldPos.xz - lightDir.xz * max(0.0, worldPos.y - lightHeight) / lightDir.y;
  return shadowPos;
}
void main() {
  vec4 position = vec4(POSITION.xyz, 1.0);
  vec3 shadowPos = ShadowProjectPos(position);
  gl_Position = u_VPMat * vec4(shadowPos, 1.0);
  vec3 center = vec3(u_modelMat[3].x , u_lightDirAndHeight.w , u_modelMat[3].z);
  float falloff = 0.5 - clamp(distance(shadowPos , center) * u_planarShadowFalloff, 0.0, 1.0);
  color = u_planarShadowColor;
  color.a *= falloff;
}

Because the road is curved, the shadow plane is offset downward ~0.3 m to avoid penetration.

Character Clone Effect

The clone effect uses a vertex shader that adds noise‑based X‑offset and a uniform controlling opacity. Core shader snippets:

// Vertex shader
uniform float glitch; // jitter amplitude
uniform float iTime; // game time
float noise(float value) { return fract(sin(dot(vec2(value, 2), vec2(12.9898, 78.233)))); }
void main() {
  ...
  gl_Position.x += noise(iTime + gl_Position.y) * glitch;
}
// Fragment shader
uniform float alpha;
void main() { gl_FragColor = vec4(1.,1.,1.,alpha); }

Scene Optimizations

Images

Textures are resized to the minimum needed size. For a 750×1628 screen, a model occupying one‑third width needs at most 512×512 texture; larger sizes waste memory.

Repeated patterns (e.g., road tiles) can be as small as 128×128 and tiled in the shader.

Texture Atlases

Combining many small images into an atlas reduces HTTP requests and draw calls, improving load speed and rendering performance.

Models

We aggressively reduce polygon count: invisible faces are removed, low‑poly versions are used for hair and accessories, and pure‑color parts use vertex colors instead of textures. The extreme case totals about 122 000 faces, which can be trimmed further by ~40 % with more aggressive culling.

Skeleton Animation

Export keyframes directly (≈50 per animation) instead of baked skeletal animation to keep data lightweight.

Texture Compression

Compressed textures can cut memory usage by ~75 % but may introduce ~15 % visual loss and increase load time. Use them only when memory pressure is evident.

Shader Optimization

Remove unused code paths and include only necessary shader modules. Replace heavy Perlin noise with a compact version:

float hash(vec3 p){ p = fract(p*0.3183099+.1); p *= 17.0; return fract(p.x*p.y*p.z*(p.x+p.y+p.z)); }
float noise(in vec3 x){ vec3 i = floor(x); vec3 f = fract(x); f = f*f*(3.0-2.0*f); return mix(mix(mix(hash(i+vec3(0,0,0)),hash(i+vec3(1,0,0)),f.x),mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y),mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x),mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z); }

General Coding Tips

Prefer multiplication over division.

Use bit‑shifts for powers‑of‑two scaling.

Prefer WeakMap for weak references.

Clear arrays with

arr.length = 0

.

Avoid

for…in

and

for…of

loops; use indexed

for

loops.

Destroy objects and nullify references to aid GC.

Development Process

Resource Loading

Essential assets (character, road, background, coins) are loaded first; optional effects load after the 321 countdown, reducing initial wait time.

Scene Assembly

Positions are fine‑tuned in the editor, then the resulting transforms are hard‑coded for runtime efficiency.

Logic Development

The game is split into modules: main flow control, event bus, tool manager, resource manager, and gesture handling. Example pseudocode:

const GameControl = {
  fadeRate: 0,
  gameTime: 0,
  wholeTime: 10000,
  deltaTime: 0,
  toolTime: 0,
  initScene() { /* init and start 321 */ },
  start() { /* start run */ },
  stop() { /* end animation and clean up */ },
  onUpdate(deltaTime) { this.deltaTime = deltaTime; this.gameTime += deltaTime; this.toolTime += deltaTime * this.fadeRate; },
  createObjOnRoad() { /* spawn coins, obstacles, accelerators */ }
};
class GameToolManager { setShield(b) {} setAccelerate(b) {} setCharacterClone(b) {} }
const resources = { coinPool: [], barricadePool: [], assets: { character: mesh, road: mesh2, /* ... */ }, onLoad() { GameControl.initScene(); GameEvent.dispatch('onLoad'); }, loadImportantAssets(list) { let assets = await engine.resourceManager.load(list); this.onLoad(); } };

Gesture handling listens for touch events and moves the character left or right when the swipe exceeds a threshold.

let isDown = false, isSwipe = false, startX = 0;
function onTouchStart(e){ if(!GameState.interactive) return; isDown = true; isSwipe = false; startX = e.targetTouches[0].clientX; }
function onTouchMove(e){ if(!isDown) return; let curX = e.targetTouches[0].clientX; let deltaX = curX - startX; if(deltaX > 20){ /* move right */ isDown = false; } else if(deltaX < -20){ /* move left */ isDown = false; } }
function onTouchEnd(){ isDown = false; }

Collision detection for coins uses simple AABB checks instead of full physics simulation.

class EatCoinScript extends Script {
  onUpdate(time) {
    // if |coin.position.z| < 0.5 && |coin.position.x - character.position.x| < 0.5 then collect
  }
}

Code Optimizations

Shader Optimizations

Strip unused includes and use the compact noise function shown earlier.

Other Recommendations

Replace division with multiplication where possible.

Use left/right shifts for powers of two.

Prefer WeakMap for weak references.

Clear arrays via

arr.length = 0

.

Avoid

for…in

/

for…of

loops.

Destroy objects and nullify references after use.

Issues Encountered

Memory Leaks & Overheating

The only memory leak stemmed from Oasis’s Lottie plugin destroy method (fixed in the latest version). React component leaks were solved by upgrading to React 18 and optimizing renders.

Overheating was caused by frequent events sent to React components, triggering full re‑renders. Optimizing those components eliminated the heat issue.

WebGL Crashes

Crash rate was 0.027 % after launch. Crashes can be caused by memory exhaustion, oversized textures (>2048 px), or external factors. Proper asset sizing and diligent memory management mitigate most crashes.

Conclusion

For large‑scale promotional games, stability outweighs visual complexity. Careful resource optimization, staged loading, and robust fallback strategies ensure smooth performance even on weak networks and low‑end devices.

Performance OptimizationGame DevelopmentShaderOasis engine3D Runner
Alipay Experience Technology
Written by

Alipay Experience Technology

Exploring ultimate user experience and best engineering practices

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.