Implementation of the “Lucky God Dragon” Interactive Gameplay in Douyin’s 2024 Chinese New Year Event
This article details the front‑end interactive development of Douyin’s 2024 Chinese New Year “Lucky God Dragon” activity, covering the cross‑platform framework, SAR Creator engine, bundle/prefab resources, scene construction, animation control, coordinate synchronization, terrain management, performance optimization, and device‑aware caching strategies.
During the 2024 Chinese New Year period, ByteDance’s Douyin app launched the “Happy Chinese New Year” series, with the “Lucky God Dragon” (招财神龙) as a core interactive game. The implementation leverages internal front‑end, cross‑platform, and interactive technology products.
Key Technologies
The cross‑platform framework provides a short first‑screen rendering time and supplies a Canvas for the SAR Creator rendering engine. SAR Creator is a self‑developed, TypeScript‑based high‑performance interactive solution that offers 2D/3D rendering, particle effects, physics, and a workflow for designers and developers.
Gameplay Overview
The activity includes five interactive mini‑games; this article focuses on the “Lucky God Dragon” flow, describing the home scene and treasure‑hunt scene, their UI composition, and the interaction between front‑end UI (React) and the SAR Creator SDK.
Asset Structure
Resources are packaged as bundles (binary‑serialized packages) and prefabs (pre‑configured entities containing models, textures, animations, and scripts). The front‑end loads bundles generated by SAR Creator rather than raw DCC assets.
Home Scene Implementation
The home scene mixes 3D models (girl, dragon, ground, snow) with 2D sprites (fireworks, houses, speech bubbles). Designers assemble the scene in the SAR Creator editor, setting entity hierarchies, transforms, and custom scripts. Animation playback uses the Animator component’s crossFade method and event callbacks such as animator.on('finished', cbFunc) .
export enum HomeAnimName {
HomeSleep = 'home_sleep', // 沉睡
HomeAwake = 'home_awake', // 苏醒
HomeIdle = 'home_idle', // 待机
HomeClick = 'home_click', // 点击效果1
HomeClickA = 'home_click_a', // 点击效果2
HomeClickB = 'home_click_b', // 点击效果3
HomeHappy = 'home_happy', // 完成任务
HomeGoHome = 'home_gohome', // 龙回家
HomeHoldBox = 'home_hold_box', // 宝箱状态
HomeOpenBox = 'home_open_box', // 龙推宝箱
HomeCloseBox = 'home_close_box', // 关闭宝箱
HomeCloseBoxIdle = 'home_close_box_idle', // 关闭后待机
HomeOpenBoxIdle = 'home_open_box_idle', // 开箱后待机
HomeGoOut = 'home_goout' // 龙去寻宝
}Animation graphs are built in SAR Creator to chain these states; designers connect HomeAwake → HomeIdle , etc., while developers trigger transitions via AnimationController.setValue(variableName, value) .
this._dragonAnimator.crossFade(anim as string, duration);
this._charAnimator.crossFade(anim as string, duration);
this._dragonAnimator.on('finished', () => this.onAnimEnd(anim, params));Coordinate Synchronization
To keep 2D UI (e.g., speech bubbles) aligned with moving 3D characters, each frame the 3D world position of a bone is projected into the UI canvas coordinates and applied to the UI entity.
const TEMP_VEC3 = new Vector3();
export const threeD2UICanvas = (entity: Entity, camera: PerspectiveCamera) => {
entity?.object?.getWorldPosition(TEMP_VEC3);
const vec3 = camera?.project(TEMP_VEC3) || new Vector3();
const x = vec3.x * 375; // canvas width
const y = vec3.y * 500; // canvas height
return { x, y };
};Treasure‑Hunt Scene Implementation
The treasure‑hunt scene is a pure 2D interactive area driven by a timeline received from the server. Each timeline entry describes a prop (bag or egg) with a trigger timestamp. The scene uses an orthographic camera that continuously moves along the X‑axis.
this._camEntity.position.x += deltaTime * this._moveSpeed;Prefabs representing terrain blocks are loaded asynchronously; the first block is loaded synchronously to avoid visual gaps, while subsequent blocks are pre‑loaded in the background.
async _loadTerrains(travelScene2D: Object2D): Promise
{
const terrainNames = TerrainNamesByTheme[this._theme];
const firstTerrainName = terrainNames[0];
this._firstTerrainPromise = this._loadTerrain(firstTerrainName!);
const terrainPromises = terrainNames.filter((_, idx) => idx !== 0).map(i => this._loadTerrain(i));
void Promise.all(terrainPromises).then(async () => {
await this._tryCreateFirstTerrainBlock(travelScene2D);
let lastTerrainBlock = this._firstPrefabBlock;
const terrainPos = this._terrainOffset.clone();
for (const terrainPromise of terrainPromises) {
if (lastTerrainBlock !== undefined) {
const terrainEntity = await terrainPromise;
terrainPos.x += lastTerrainBlock.getBlockSize();
lastTerrainBlock = this._createTerrainBlock(travelScene2D, terrainEntity, false, terrainPos);
}
}
});
await this._tryCreateFirstTerrainBlock(travelScene2D);
}When a terrain block leaves the screen, it is recycled to the far right, with a one‑screen delay to hide seams.
_recycleTerrain(cameraPos: Vector3): void {
const headRightX = this._terrainBlocks[0].getBlockRightPositionWorld();
const terrainScreenWidth = this._terrainBlocks[0].getTerrainScreenWidth();
const screenLeftEdge = cameraPos.x - this._halfScreenWidth!;
const screenRightEdge = cameraPos.x + this._halfScreenWidth!;
if (headRightX + terrainScreenWidth < screenLeftEdge) {
this._terrainBlocks[0]?.setVisible(false);
const headBlock = this._terrainBlocks.shift();
if (headBlock) {
const tailPos = this._terrainBlocks[this._terrainBlocks.length - 1].getPosition();
headBlock.setPositionX(tailPos.x + this._terrainPrefabLength);
this._terrainBlocks.push(headBlock);
headBlock.resetDetectors();
}
} else if (headRightX - terrainScreenWidth <= screenRightEdge && !this._terrainBlocks[1].getVisible()) {
this._terrainBlocks[1].setVisible(true);
}
}Detection Points and Dragon Logic
Each terrain block contains several detection points. When a point crosses the screen centre, it triggers a dragon animation and optionally a bag animation based on configuration (appear time, hide time, Z‑value). Detection points also carry flags for friend‑dragon scenarios.
Scene Management and Transition
Switching between the home scene and treasure‑hunt scene is handled by a dragon transition animation (start → loop → end). The loop phase is used to load the next scene while the user sees the animation.
interface TransferLifeCycle {
onStart?: () => Promise
;
onEnd?: () => void;
onRemove?: () => void;
onError?: (e: Error) => void;
}
class Transfer {
_spine: Spine;
_transfer!: TransferLifeCycle;
_canEnd = false;
async startTransfer(params: TransferLifeCycle) {
this._transfer = { ...this._transfer, ...params };
try {
this._canEnd = false;
// load and play 'start' animation …
} catch (e) {
this._transfer?.onError?.(e);
}
}
async onSpineAnimComplete(entry: any) {
const animateName = entry.animation.name;
try {
if (animateName === 'start') {
// play 'loop' and await onStart
await this._transferParams.onStart?.();
this._canEnd = true;
} else if (animateName === 'loop') {
if (this._canEnd) this._transfer?.onEnd?.();
} else if (animateName === 'end') {
this._transfer?.onRemove?.();
}
} catch (e) {
this._transfer?.onError?.(e);
}
}
}Performance Optimizations
The task page combines heavy UI with interactive Canvas, leading to CPU‑intensive rendering and memory pressure (200‑300 MB limit). Strategies include short first‑screen time, bundle/prefab reuse, pre‑loading of the next scene, caching of entities on high‑end devices, and full resource disposal on low‑end devices based on a device‑score and memory‑limit configuration.
class SceneManager {
homeRoot?: Entity;
travelRoot?: Entity;
async loadHomeRoot() {
if (!this.homeRoot) this.homeRoot = await bundle.load('HomeRoot.prefab');
if (this.homeRoot) scene.addChild(this.homeRoot);
}
async loadTravelRoot() {
if (!this.travelRoot) this.travelRoot = await bundle.load('TravelRoot.prefab');
}
dispose() {
if (USE_STORAGE) {
this.homeRoot?.parent?._deleteEntityFromChildren(this.homeRoot);
} else {
entity.dispose();
}
}
}Team Introduction
The work is done by Douyin’s Front‑End Architecture – Interactive Experience team, responsible for SAR Creator, Simple Engine, and the AnnieX interactive container, collaborating with cross‑platform, mini‑game, and growth front‑end teams.
TikTok Frontend Technology Team
We are the TikTok Frontend Technology Team, serving TikTok and multiple ByteDance product lines, focused on building frontend infrastructure and exploring community technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.