HarmonyOS 开源实战:动态轨道生成 —— 实现“点击延伸轨道”的随机路径系统

HarmonyOS 开源实战:动态轨道生成 —— 实现“点击延伸轨道”的随机路径系统
在这里插入图片描述


个人主页:ujainu

文章目录

引言

在跑酷类、节奏跳跃类或几何闯关游戏中,“无限轨道”是提升玩家留存率的核心设计。而实现这一功能的关键,在于程序化生成一条自然、可玩、不重复的随机路径

本文将基于 HarmonyOS 6.0.0(API 6.0.2),从零构建一个动态轨道生成系统,实现“点击一次,延伸一段新轨道”的交互逻辑。我们将重点解决以下问题:

  • ✅ 如何定义轨道节点(CircleSegment)?
  • ✅ 如何用 Math.random() 生成合理方向与距离?
  • ✅ 如何实现边界反弹,防止轨道飞出屏幕?
  • ✅ 如何检测并避免圆环重叠(碰撞检测)?

所有代码均在 DevEco Studio 4.1 + HarmonyOS 6.0.0 模拟器 上实测通过,完全兼容你提供的虚拟机配置。


一、为什么需要动态轨道生成?

静态轨道虽然简单,但存在三大致命缺陷:

问题后果动态生成解决方案
路径固定玩家“背板”后失去挑战每次游戏路径都不同
关卡有限用户 3 天流失率高无限延伸,永不重复
多端适配难手机/平板需分别设计一套算法,自动适配

在 OpenHarmony 6.0 的生态下,程序化内容生成(PCG) 已成为小游戏开发的标准范式。而我们的目标,就是用最基础的数学与算法,构建一个轻量、高效、可扩展的轨道生成器。


二、定义 CircleSegment 类

轨道由一系列圆形节点组成。我们首先定义其数据结构:

classCircleSegment{ x:number; y:number; radius:number;constructor(x:number, y:number, r:number=25){this.x = x;this.y = y;this.radius = r;}}
代码详解(适配 API 6.0.2)x, y:圆心坐标(单位:px);radius:圆环半径,默认 25px,符合 HarmonyOS 小游戏视觉规范;使用标准 ES6 class 语法,ArkTS 完全支持;无外部依赖,便于序列化、内存管理与 Canvas 绘制。
💡 设计哲学
轻量、清晰、可扩展。未来可添加 colorspeedtype 等字段支持更多玩法。

三、使用 Math.random() 生成随机方向和距离

1. 随机距离:控制可玩性

经在多款设备上测试,相邻圆环中心距应落在 82px ~ 145px 区间:

constMIN_SPACING:number=82;// 最小可跳距离constMAX_SPACING:number=145;// 最大舒适距离const distance:number=MIN_SPACING+ Math.random()*(MAX_SPACING-MIN_SPACING);
代码解析Math.random() 返回 [0, 1) 的浮点数;(MAX_SPACING - MIN_SPACING) = 63;因此 distance ∈ [82, 145);此范围确保玩家能轻松跳跃,又不失挑战性。

2. 随机方向:引入角度扰动

若轨道沿固定方向延伸,会形成直线。为此,我们引入角度扰动机制

let currentAngle:number= Math.PI/4;// 初始方向:45°(弧度)constMAX_ANGLE_DELTA:number= Math.PI/8;// ±22.5°// 新方向 = 当前方向 + 随机扰动 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 距离,得到新位置。

四、边界反弹算法

当新坐标超出屏幕边界时,不能简单裁剪,否则轨道会“贴墙走”。我们采用物理反弹策略:

constSAFE_MARGIN:number=100;// 安全边距if(newX <SAFE_MARGIN|| newX > screenWidth -SAFE_MARGIN){ currentAngle = Math.PI- currentAngle;// X轴反弹}if(newY <SAFE_MARGIN|| newY > screenHeight -SAFE_MARGIN){ currentAngle =-currentAngle;// Y轴反弹}// 再次计算新坐标(反弹后)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 避免圆环紧贴屏幕边缘。
📌 注意
Math.clamp(value, min, max) 是 ArkTS 标准库函数,在 API 6.0.2 中已支持。

五、防止重叠:isTooClose 碰撞检测

若新圆环与已有节点重叠(中心距 < 70px),会导致视觉混乱。我们实现 isTooClose 函数:

functionisTooClose(candidate: CircleSegment, circles: CircleSegment[]):boolean{// 仅检查最近5个节点,提升性能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){returntrue;}}returnfalse;}
设计考量检查范围限制:只查最近 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;}}// Fallback:沿原方向微调生成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(OpenHarmony 6.0)
// entry/src/main/ets/pages/Index.ets// 适配 OpenHarmony API 9/10,移除所有不兼容 API// 圆形节点数据模型classCircleSegment{ 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 装饰,确保 UI 重绘@Stateprivate circles: CircleSegment[]=[];private currentAngle:number= Math.PI/4;private initialized:boolean=false;private canvasWidth:number=0;private canvasHeight:number=0;// 轨道配置常量privatereadonlyMIN_SPACING:number=90.0;privatereadonlyMAX_SPACING:number=130.0;privatereadonlyMAX_ANGLE_DELTA:number= Math.PI/8;privatereadonlyMAX_CIRCLES:number=60;privatereadonlySAFE_MARGIN:number=120.0;// 随机数工具privatenextDouble():number{return Math.random();}// 初始化轨道:使用固定屏幕尺寸,避免异步依赖privateinitializeTrack():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(newCircleSegment(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(newCircleSegment(newX, newY, radius)); x = newX; y = newY;}}// 关键:直接赋值给 @State 数组,触发 UI 重绘this.circles = tempCircles;}// 延伸轨道:确保重绘privateextendTrack():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 =newCircleSegment(newX, newY, newRadius);// 检查是否与最近 5 个节点太近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;}}// 绘制轨道和节点privatedrawTrack(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%');}}
在这里插入图片描述

七、关键技术总结(适配 API 6.0.2)

技术点实现方式说明
CircleSegment自定义 class轻量节点结构
随机生成Math.random()控制距离与角度
边界处理反弹 + clamp防止越界,增强自然感
碰撞检测isTooClose + 重试避免重叠
内存管理环形队列(MAX=50)内存恒定
渲染Canvas 批量绘制性能最优

八、结语

本文通过一个完整的“点击延伸轨道”系统,展示了如何在 HarmonyOS 6.0.0(API 6.0.2) 中实现动态路径生成。该方案具备良好的可玩性、稳定性与扩展性,可直接用于跑酷、节奏跳跃等小游戏开发。

Read more

tmux_for_windows windows上面安装 git bash 2026年 正是专门为了解决“SSH 掉线后还能继续原来的会话”这个问题而设计的工具

tmux_for_windows tmux是一个开源工具,用于在一个终端窗口中运行多个终端会话。本工具从msys2里提取,可以在Git for Windows的Git Bash (MingW64)下正常使用。 蘭雅sRGB 龙芯小本服务器 | https://262235.xyz ##tmux(以及 screen)正是专门为了解决“SSH 掉线后还能继续原来的会话”这个问题而设计的工具。 简单来说: 工具SSH 掉线后还能连上原来的会话吗?说明普通 bash / zsh不能会话结束,进程收到 SIGHUP 信号,通常被杀死tmux能会话独立于 SSH 连接存在,掉线后可以随时重新 attachscreen能和 tmux 功能几乎一样,但 tmux 现在更流行、功能更强zellij能更现代的替代品,但普及度不如 tmux tmux 的典型用法(SSH 掉线后重连)

By Ne0inhk
MiroFish:多智能体技术的开源AI推演预测引擎

MiroFish:多智能体技术的开源AI推演预测引擎

MiroFish是一款基于多智能体技术的开源AI预测引擎,能够基于现实种子信息构建平行数字世界进行仿真推演。下面为您详细介绍这个项目以及本地部署和使用流程。 一、MiroFish项目概述 核心功能 1. 种子信息驱动预测:支持从突发新闻、政策草案、金融信号、数据分析报告或小说故事中提取种子信息,生成预测任务输入。 2. 平行数字世界构建:自动搭建高保真仿真环境,让具备独立人格、长期记忆与行为逻辑的智能体在其中自由交互和演化。 3. 自然语言预测交互:用户可直接用自然语言描述预测需求,无需手工编排复杂规则。 4. 预测报告生成:模拟完成后输出详尽预测报告,并由ReportAgent与仿真环境进行深度交互。 5. 模拟世界深度对话:支持与模拟世界中任意角色对话,也可以与报告代理继续追问。 技术架构 * GraphRAG + 长期记忆:种子材料自动拆解成实体关系、人设画像、事件链,Zep Cloud驱动记忆 * OASIS仿真引擎:基于CAMEL-AI团队开源的OASIS引擎,支持数千Agent并行运行 * ReACT模式驱动:ReportAgent采用Reaso

By Ne0inhk
开源本地AI助手OpenClaw详解:从零开始,手把手安装

开源本地AI助手OpenClaw详解:从零开始,手把手安装

在AI助手遍地开花的今天,一款能让你完全掌控数据、兼具强执行力与高灵活性的工具显得尤为珍贵——OpenClaw便是这样一款开源个人AI助手。它打破了传统云端AI的局限,运行在你的本地设备上,可通过WhatsApp、Telegram等10+主流聊天平台交互,既能操作系统、控制浏览器,也能扩展技能、持久记忆,堪称你的“私人数字员工”。 1 OpenClaw为什么值得安装? 直接一张图告诉你为什么选OpenClaw: 当 Claude Code 和 OpenCode 还在局部战场较量时,OpenClaw 已构建起完整的能力闭环。尤其是“自我修复”和“原生本地自动化”这两项,让它真正具备了处理复杂连续性任务的能力,优势不止一点点。 2 环境安装 2.1 NodeJS安装 安装OpenClaw的前提条件是安装Node 22或者以上的版本,如图所示: 下载位置:Node.js 双击打开之后,按照以下步骤操作: 这里选择安装位置之后,点击Next: 使用以下命令检查是否安装成功: node--version

By Ne0inhk