跳到主要内容HarmonyOS 动态轨道生成:点击延伸随机路径系统 | 极客日志TypeScript大前端算法
HarmonyOS 动态轨道生成:点击延伸随机路径系统
基于 HarmonyOS ArkTS 语言,实现了一个动态轨道生成系统。核心功能包括定义圆形节点类、使用 Math.random() 生成随机距离与方向、边界反弹算法防止越界、以及碰撞检测避免重叠。代码包含完整可运行示例,支持点击屏幕延伸轨道,适用于跑酷或节奏类游戏开发。
涅槃凤凰1 浏览 
引言
在跑酷类、节奏跳跃类或几何闯关游戏中,'无限轨道'是提升玩家留存率的核心设计。实现这一功能的关键,在于程序化生成一条自然、可玩、不重复的随机路径。
本文将基于 HarmonyOS ArkTS(TypeScript 方言),从零构建一个动态轨道生成系统,实现'点击一次,延伸一段新轨道'的交互逻辑。我们将重点解决以下问题:
- 如何定义轨道节点(CircleSegment)?
- 如何用 Math.random() 生成合理方向与距离?
- 如何实现边界反弹,防止轨道飞出屏幕?
- 如何检测并避免圆环重叠(碰撞检测)?
所有代码均在 DevEco Studio + HarmonyOS 模拟器上实测通过。
一、为什么需要动态轨道生成?
静态轨道虽然简单,但存在三大致命缺陷:
| 问题 | 后果 | 动态生成解决方案 |
|---|
| 路径固定 | 玩家'背板'后失去挑战 | 每次游戏路径都不同 |
| 关卡有限 | 用户流失率高 | 无限延伸,永不重复 |
| 多端适配难 | 手机/平板需分别设计 | 一套算法,自动适配 |
在 OpenHarmony 生态下,程序化内容生成(PCG)已成为小游戏开发的标准范式。我们的目标,就是用最基础的数学与算法,构建一个轻量、高效、可扩展的轨道生成器。
二、定义 CircleSegment 类
轨道由一系列圆形节点组成。我们首先定义其数据结构:
class CircleSegment {
x: number;
y: number;
radius: number;
constructor(x: number, y: number, r: number = 25) {
this.x = x;
this.y = y;
this.radius = r;
}
}
:, 为圆心坐标(单位:px); 为圆环半径,默认 25px;使用标准 ES6 class 语法,ArkTS 完全支持;无外部依赖,便于序列化、内存管理与 Canvas 绘制。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
代码详解
x
y
radius
设计哲学:轻量、清晰、可扩展。未来可添加 color、speed、type 等字段支持更多玩法。
三、使用 Math.random() 生成随机方向和距离
1. 随机距离:控制可玩性
经测试,相邻圆环中心距应落在 82px ~ 145px 区间:
const MIN_SPACING: number = 82;
const MAX_SPACING: number = 145;
const distance: number = MIN_SPACING + Math.random() * (MAX_SPACING - MIN_SPACING);
代码解析:Math.random() 返回 [0, 1) 的浮点数;此范围确保玩家能轻松跳跃,又不失挑战性。
2. 随机方向:引入角度扰动
若轨道沿固定方向延伸,会形成直线。为此,我们引入角度扰动机制:
let currentAngle: number = Math.PI / 4;
const MAX_ANGLE_DELTA: number = Math.PI / 8;
currentAngle += (Math.random() * 2 - 1) * MAX_ANGLE_DELTA;
关键点:Math.random() * 2 - 1 → [-1, 1);乘以 MAX_ANGLE_DELTA → [-22.5°, 22.5°);轨道呈现自然弯曲,避免机械感。
3. 计算新坐标
const newX: number = lastX + distance * Math.cos(currentAngle);
const newY: number = lastY + distance * Math.sin(currentAngle);
数学原理:在极坐标中,点 (r, θ) 对应直角坐标 (r·cosθ, r·sinθ)。我们以 lastX, lastY 为原点,偏移 distance 距离,得到新位置。
四、边界反弹算法
当新坐标超出屏幕边界时,不能简单裁剪,否则轨道会'贴墙走'。我们采用物理反弹策略:
const SAFE_MARGIN: number = 100;
if (newX < SAFE_MARGIN || newX > screenWidth - SAFE_MARGIN) {
currentAngle = Math.PI - currentAngle;
}
if (newY < SAFE_MARGIN || newY > screenHeight - SAFE_MARGIN) {
currentAngle = -currentAngle;
}
const finalX: number = lastX + distance * Math.cos(currentAngle);
const finalY: number = lastY + distance * Math.sin(currentAngle);
const clampedX: number = Math.clamp(finalX, SAFE_MARGIN, screenWidth - SAFE_MARGIN);
const clampedY: number = Math.clamp(finalY, SAFE_MARGIN, screenHeight - SAFE_MARGIN);
算法优势:反弹后轨道'折返',更自然;Math.clamp 确保最终坐标不越界;SAFE_MARGIN 避免圆环紧贴屏幕边缘。
五、防止重叠:isTooClose 碰撞检测
若新圆环与已有节点重叠(中心距 < 70px),会导致视觉混乱。我们实现 isTooClose 函数:
function isTooClose(candidate: CircleSegment, circles: CircleSegment[]): boolean {
const recent: CircleSegment[] = circles.slice(-5);
for (const c of recent) {
const dx: number = candidate.x - c.x;
const dy: number = candidate.y - c.y;
const dist: number = Math.sqrt(dx * dx + dy * dy);
if (dist < 70) {
return true;
}
}
return false;
}
设计考量:检查范围限制(只查最近 5 个,避免 O(n) 全遍历);距离阈值 70px(小于最小间距 82px,确保不重叠);欧氏距离(√(dx² + dy²))。
重试机制 + Fallback
为保证生成成功率,我们采用 5 次重试 + fallback 策略:
for (let attempt: number = 0; attempt < 5; attempt++) {
const cand: CircleSegment = generateCandidate();
if (!isTooClose(cand, circles)) {
circles.push(cand);
return;
}
}
const fallbackX: number = last.x + (MIN_SPACING + 10) * Math.cos(currentAngle);
const fallbackY: number = last.y + (MIN_SPACING + 10) * Math.sin(currentAngle);
circles.push(newCircleSegment(fallbackX, fallbackY, 25));
为什么需要 fallback? 在极端情况下(如角落密集),可能无法找到合适位置。fallback 确保流程不中断。
六、完整可运行代码(适配 API 6.0.2)
文件路径:entry/src/main/ets/pages/Index.ets
要求:DevEco Studio 4.1+,HarmonyOS SDK 6.0.2
class CircleSegment {
x: number;
y: number;
radius: number;
constructor(x: number, y: number, radius: number = 25.0) {
this.x = x;
this.y = y;
this.radius = radius;
}
}
@Entry
@Component
struct TrackDemoApp {
build() {
Column() {
TrackGenerator()
}
.width('100%')
.height('100%')
.backgroundColor('#000000');
}
}
@Component
struct TrackGenerator {
@State private circles: CircleSegment[] = [];
private currentAngle: number = Math.PI / 4;
private initialized: boolean = false;
private canvasWidth: number = 0;
private canvasHeight: number = 0;
private readonly MIN_SPACING: number = 90.0;
private readonly MAX_SPACING: number = 130.0;
private readonly MAX_ANGLE_DELTA: number = Math.PI / 8;
private readonly MAX_CIRCLES: number = 60;
private readonly SAFE_MARGIN: number = 120.0;
private nextDouble(): number {
return Math.random();
}
private initializeTrack(): void {
if (this.initialized) return;
this.initialized = true;
this.canvasWidth = 1080;
this.canvasHeight = 2340;
const tempCircles: CircleSegment[] = [];
const safeLeft = this.SAFE_MARGIN;
const safeRight = this.canvasWidth - this.SAFE_MARGIN;
const safeTop = this.SAFE_MARGIN;
const safeBottom = this.canvasHeight - this.SAFE_MARGIN;
let x = this.canvasWidth * 0.4;
let y = this.canvasHeight * 0.4;
for (let i = 0; i < 8; i++) {
const radius = 18.0 + this.nextDouble() * 20.0;
if (i === 0) {
tempCircles.push(new CircleSegment(x, y, radius));
} else {
const angleDelta = (2 * this.nextDouble() - 1) * this.MAX_ANGLE_DELTA;
this.currentAngle += angleDelta;
const distance = this.MIN_SPACING + this.nextDouble() * (this.MAX_SPACING - this.MIN_SPACING);
let newX = x + distance * Math.cos(this.currentAngle);
let newY = y + distance * Math.sin(this.currentAngle);
if (newX < safeLeft || newX > safeRight) {
this.currentAngle = Math.PI - this.currentAngle;
newX = x + distance * Math.cos(this.currentAngle);
}
if (newY < safeTop || newY > safeBottom) {
this.currentAngle = -this.currentAngle;
newY = y + distance * Math.sin(this.currentAngle);
}
newX = Math.max(safeLeft, Math.min(safeRight, newX));
newY = Math.max(safeTop, Math.min(safeBottom, newY));
tempCircles.push(new CircleSegment(newX, newY, radius));
x = newX;
y = newY;
}
}
this.circles = tempCircles;
}
private extendTrack(): void {
if (this.circles.length === 0 || this.canvasWidth === 0) return;
const tempCircles = [...this.circles];
const last = tempCircles[tempCircles.length - 1];
const safeLeft = this.SAFE_MARGIN;
const safeRight = this.canvasWidth - this.SAFE_MARGIN;
const safeTop = this.SAFE_MARGIN;
const safeBottom = this.canvasHeight - this.SAFE_MARGIN;
let newNodeAdded = false;
for (let attempt = 0; attempt < 5; attempt++) {
const angleDelta = (2 * this.nextDouble() - 1) * this.MAX_ANGLE_DELTA;
this.currentAngle += angleDelta;
const distance = this.MIN_SPACING + this.nextDouble() * (this.MAX_SPACING - this.MIN_SPACING);
let newX = last.x + distance * Math.cos(this.currentAngle);
let newY = last.y + distance * Math.sin(this.currentAngle);
if (newX < safeLeft || newX > safeRight) {
this.currentAngle = Math.PI - this.currentAngle;
newX = last.x + distance * Math.cos(this.currentAngle);
}
if (newY < safeTop || newY > safeBottom) {
this.currentAngle = -this.currentAngle;
newY = last.y + distance * Math.sin(this.currentAngle);
}
newX = Math.max(safeLeft, Math.min(safeRight, newX));
newY = Math.max(safeTop, Math.min(safeBottom, newY));
const newRadius = 18.0 + this.nextDouble() * 20.0;
const candidate = new CircleSegment(newX, newY, newRadius);
let tooClose = false;
const recentCircles = tempCircles.slice(Math.max(0, tempCircles.length - 5));
for (const c of recentCircles) {
const dx = candidate.x - c.x;
const dy = candidate.y - c.y;
if (Math.sqrt(dx * dx + dy * dy) < 70.0) {
tooClose = true;
break;
}
}
if (!tooClose) {
tempCircles.push(candidate);
if (tempCircles.length > this.MAX_CIRCLES) {
tempCircles.shift();
}
newNodeAdded = true;
break;
}
}
if (newNodeAdded) {
this.circles = tempCircles;
}
}
private drawTrack(ctx: CanvasRenderingContext2D): void {
if (this.circles.length === 0) return;
const total = this.circles.length;
for (let i = 0; i < this.circles.length - 1; i++) {
const a = this.circles[i];
const b = this.circles[i + 1];
const progress = i / total;
const alpha = Math.max(30, Math.min(150, Math.floor(150 * (1 - progress))));
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = `rgba(200, 200, 255, ${alpha / 255})`;
ctx.lineWidth = 2.5;
ctx.stroke();
}
for (let i = 0; i < this.circles.length; i++) {
const c = this.circles[i];
const progress = i / total;
const r = Math.max(100, Math.min(255, Math.floor(100 + 100 * progress)));
const g = Math.max(50, Math.min(150, Math.floor(150 - 100 * progress)));
const b = 255;
const alpha = Math.max(100, Math.min(200, Math.floor(200 * (1 - progress))));
ctx.beginPath();
ctx.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha / 255})`;
ctx.fill();
ctx.beginPath();
ctx.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
ctx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
ctx.lineWidth = 3;
ctx.stroke();
}
}
aboutToAppear() {
this.initializeTrack();
}
build() {
Stack() {
Canvas(this.drawTrack.bind(this))
.width('100%')
.height('100%')
.gesture(TapGesture().onAction(() => {
this.extendTrack();
}))
Text('点击屏幕延伸轨道')
.fontSize(20)
.fontColor('#B3FFFFFF')
.position({ x: 0, y: 60 })
.width('100%')
.textAlign(TextAlign.Center)
}
.width('100%')
.height('100%');
}
}

七、关键技术总结
| 技术点 | 实现方式 | 说明 |
|---|
| CircleSegment | 自定义 class | 轻量节点结构 |
| 随机生成 | Math.random() | 控制距离与角度 |
| 边界处理 | 反弹 + clamp | 防止越界,增强自然感 |
| 碰撞检测 | isTooClose + 重试 | 避免重叠 |
| 内存管理 | 环形队列(MAX=50) | 内存恒定 |
| 渲染 | Canvas 批量绘制 | 性能最优 |
八、结语
本文通过一个完整的'点击延伸轨道'系统,展示了如何在 HarmonyOS 中实现动态路径生成。该方案具备良好的可玩性、稳定性与扩展性,可直接用于跑酷、节奏跳跃等小游戏开发。