当前位置: 首页 > java >正文

纯血HarmonyOS ArKTS NETX 5 打造小游戏实践:大鱼吃小鱼(附源文件)

一、游戏整体架构设计

这款基于ArkUI的鱼类捕食游戏采用了经典的MVC架构思想,将游戏逻辑与UI渲染分离,主要包含以下核心模块:

  • 模型层(Model)Fish基类及其子类定义游戏实体
  • 视图层(View)play_6组件负责UI渲染
  • 控制层(Controller):游戏循环、交互逻辑与AI控制

这种架构使代码具有良好的可维护性和扩展性,下面我们将逐模块解析核心代码。

二、Fish基类与继承体系
1. 基类核心属性与功能
class Fish {size: number = 0;          // 体型决定捕食关系,数值越大越能捕食小鱼speed: number = 0;         // 移动速度,与体型负相关positionX: number = 0;     // 笛卡尔坐标系X轴位置positionY: number = 0;     // 笛卡尔坐标系Y轴位置health: number = 100;      // 生命值系统,被大鱼攻击会减少scoreValue: number = 0;    // 被吃掉时提供的分数,与体型正相关direction: number = 1;     // 移动方向(1:右,-1:左)constructor(size: number, speed: number, x: number, y: number,isPlayer: boolean, name: string, direction: number) {this.size = size;this.speed = speed;this.positionX = x;this.positionY = y;// 分数价值 = 体型*1.5并取整,体现体型与价值的线性关系this.scoreValue = Math.floor(size * 1.5);this.direction = direction;}
}

设计亮点

  • 通过size属性构建捕食关系链,形成游戏核心玩法
  • scoreValuesize的线性关系设计,使游戏进度与视觉反馈一致
  • 统一的坐标系统为碰撞检测和移动逻辑提供基础

2. 玩家鱼PlayerFish实现
export class PlayerFish extends Fish {private isDragging: boolean = false; // 拖拽状态标记constructor(size: number, speed: number, x: number, y: number) {super(size, speed, x, y, true, "Player");}// 边界检测核心逻辑checkBoundary() {const displayClass = display.getDefaultDisplaySync();const screenWidth = displayClass.width / displayClass.densityPixels;const screenHeight = displayClass.height / displayClass.densityPixels;// 四个方向的边界限制,确保玩家鱼不会移出屏幕if (this.positionX < 0) this.positionX = 0;if (this.positionY < 0) this.positionY = 0;if (this.positionX > screenWidth - this.size * 2) {this.positionX = screenWidth - this.size * 2;}if (this.positionY > screenHeight - this.size * 2) {this.positionY = screenHeight - this.size * 2;}}
}

交互逻辑解析

  • checkBoundary方法通过获取屏幕实际尺寸,实现玩家鱼的边界限制
  • 边界计算考虑了鱼的体型大小(size * 2),避免鱼体超出屏幕显示范围
  • 拖拽控制通过isDragging状态标记与偏移量计算实现精准操作

3. NPC鱼智能行为NPCFish
class NPCFish extends Fish {private directionX: number = 0; // X轴移动方向分量private directionY: number = 0; // Y轴移动方向分量private lastDirectionChange: number = 0; // 上次改变方向的时间戳private playerFish: PlayerFish | null = null; // 玩家鱼引用constructor(size: number, speed: number, x: number, y: number,playerFish: PlayerFish, direction: number) {super(size, speed, x, y, false, `NPC-${Math.random() * 1000}`, direction);this.playerFish = playerFish;this.resetDirection(); // 初始化移动方向}// 随机重置移动方向private resetDirection() {// X方向根据初始方向确定基本趋势,添加随机扰动this.directionX = this.direction * (Math.random() * 0.5 + 0.3);// Y方向完全随机,模拟鱼类自然游动this.directionY = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 0.3);this.lastDirectionChange = Date.now();}
}

AI行为核心

  • resetDirection方法通过随机数生成扰动向量,使NPC鱼移动更自然
  • lastDirectionChange时间戳控制方向变化频率,避免NPC鱼行为过于规律
  • 方向向量的X分量与初始方向关联,Y分量完全随机,形成"定向游动+随机摆动"的效果

三、游戏核心循环与逻辑控制
1. 游戏主循环机制
private gameLoop() {if (this.gameOver) return;// 更新所有NPC鱼状态this.npcFishes.forEach(npc => {npc.autoMove();});// 检测碰撞与生存状态this.checkCollisions();this.checkPlayerSurvival();// 下一帧循环(30ms约33FPS)setTimeout(() => {this.gameLoop();}, 30);
}

循环逻辑解析

  • 采用setTimeout实现递归循环,控制游戏更新频率
  • 先更新NPC鱼状态,再进行碰撞检测,确保逻辑顺序正确
  • gameOver状态标记可暂停循环,优化资源占用

2. 碰撞检测与捕食逻辑
// 核心碰撞检测算法
private isColliding(fish1: Fish, fish2: Fish): boolean {const dx = fish1.positionX - fish2.positionX;const dy = fish1.positionY - fish2.positionY;const distance = Math.sqrt(dx * dx + dy * dy);// 碰撞判定:两鱼中心距离小于体型半径和return distance < (fish1.size + fish2.size) / 2;
}// 捕食逻辑实现
private checkCollisions() {const toRemove: number[] = [];for (let i = 0; i < this.npcFishes.length; i++) {const npc = this.npcFishes[i];// 玩家鱼捕食NPC鱼的条件:玩家鱼更大且发生碰撞if (this.playerFish.size > npc.size && this.isColliding(this.playerFish, npc)) {// 玩家鱼成长:体型增加NPC鱼体型的15%this.playerFish.size += npc.size * 0.15;// 分数增加:获取NPC鱼的scoreValuethis.score += npc.scoreValue;// 速度调整:体型越大速度越慢,最小速度为1this.playerFish.speed = Math.max(1, 3 - this.playerFish.size / 20);// 标记NPC鱼为移除toRemove.push(i);}}// 从后往前移除,避免索引错误for (let i = toRemove.length - 1; i >= 0; i--) {this.npcFishes.splice(toRemove[i], 1);}
}

算法关键点

  • 碰撞检测采用欧几里得距离计算,时间复杂度O(n)
  • 捕食后玩家鱼的成长比例(15%)与速度衰减公式(3 - size/20)经过平衡设计
  • toRemove数组收集待移除索引,避免遍历时修改数组导致的逻辑错误

3. NPC鱼智能反应逻辑
autoMove() {// 定期改变方向(1000-3000ms随机间隔)if (Date.now() - this.lastDirectionChange > 1000 + Math.random() * 2000) {this.resetDirection();}// 基础移动:方向向量*速度this.positionX += this.directionX * this.speed;this.positionY += this.directionY * this.speed;// 边界循环机制if (this.positionX < -this.size * 2) {// 从左侧消失,右侧重新生成this.positionX = this.screenWidth + this.size * 2;this.positionY = Math.random() * (this.screenHeight - 100) + 50;} else if (this.positionX > this.screenWidth + this.size * 2) {// 从右侧消失,左侧重新生成this.positionX = -this.size * 2;this.positionY = Math.random() * (this.screenHeight - 100) + 50;}// 对玩家鱼的智能反应if (this.playerFish) {const playerDistX = this.playerFish.positionX - this.positionX;const playerDistY = this.playerFish.positionY - this.positionY;const dist = Math.sqrt(playerDistX * playerDistX + playerDistY * playerDistY);if (this.playerFish.size > this.size && dist < 200) {// 玩家鱼更大时,NPC鱼躲避(向远离玩家鱼的方向移动)this.directionX = -playerDistX / dist * 0.3;this.directionY = -playerDistY / dist * 0.3;} else if (this.size > this.playerFish.size && dist < 300) {// NPC鱼更大时,追逐玩家鱼(向玩家鱼方向移动)this.directionX = playerDistX / dist * 0.3;this.directionY = playerDistY / dist * 0.3;}}
}

AI策略解析

  • 边界循环机制实现NPC鱼从屏幕一侧消失后从另一侧出现,形成持续流动感
  • 200300作为触发躲避/追逐的距离阈值,经过游戏平衡设计
  • 向量归一化处理(playerDistX/dist)确保方向向量长度为1,避免速度突变

四、UI渲染与用户交互
1. 组件渲染核心逻辑
build() {Stack() {// 背景层Row().backgroundColor('#e2e9ef').width('100%').height('100%')// 信息显示层Text(`分数: ${this.score}`).fontSize(24).fontColor('#FFFFFF')Text(`生命值: ${this.playerFish.health}`).backgroundColor('rgba(0, 0, 0, 0.5)')// 玩家鱼渲染(红色图标,可拖拽)Text('🐟').fontSize(this.playerFish.size).backgroundColor('#FF0000').gesture(PanGesture().onActionStart((event) => {// 计算拖拽偏移量,确保手势起点精准this.dragOffsetX = event.offsetX - (this.playerFish.positionX - (this.playerFish.size * 2) / 2);this.dragOffsetY = event.offsetY - (this.playerFish.positionY - (this.playerFish.size * 2) / 2);}))// NPC鱼列表渲染(蓝色图标,方向动态变化)ForEach(this.npcFishes, (npc) => {Text(npc.direction > 0 ? '🐠' : '🐟').fontSize(npc.size).backgroundColor('#ADD8E6')})// 游戏结束层(半透明遮罩)if (this.gameOver) {Column().backgroundColor('rgba(0, 0, 0, 0.7)').children([Text('游戏结束!'),Button('重新开始').onClick(() => {// 重置游戏状态})])}}
}

UI设计关键点

  • Stack布局实现多层叠加效果,背景层、信息层、鱼群层、结束层有序排列
  • ForEach组件实现NPC鱼列表的动态渲染,每条鱼根据direction属性显示不同图标
  • 半透明遮罩(rgba(0,0,0,0.7))在游戏结束时覆盖整个屏幕,突出显示结束信息

2. 屏幕适配与动态生成
// 屏幕尺寸获取与初始化
aboutToAppear() {try {const displayClass = display.getDefaultDisplaySync();this.screenWidth = displayClass.width / displayClass.densityPixels;this.screenHeight = displayClass.height / displayClass.densityPixels;console.info(`屏幕尺寸: ${this.screenWidth}x${this.screenHeight}`);} catch (error) {// 异常处理:获取失败时使用默认值console.error("获取屏幕尺寸失败", error);}// 初始化8条NPC鱼this.initNPCFishes(8);// 启动游戏循环this.gameLoop();// 每1000ms生成新鱼setInterval(() => {this.spawnRandomFish();}, 1000);
}// NPC鱼动态生成
private spawnRandomFish() {const size = Math.floor(Math.random() * 25) + 10; // 10-35的随机体型const speed = 3 - size / 10; // 速度与体型负相关// 随机从左侧或右侧生成const fromLeft = Math.random() > 0.5;let x = fromLeft ? -size * 2 : this.screenWidth + size * 2;const direction = fromLeft ? 1 : -1;// Y坐标限制在屏幕中间区域(70-430,假设屏幕高度500)const y = Math.floor(Math.random() * (500 - 70)) + 70;this.npcFishes.push(new NPCFish(size, speed, x, y, this.playerFish, direction));console.info(`生成NPC鱼: 位置(${x},${y}), 方向${direction}, 大小${size}`);
}

适配与生成逻辑

  • aboutToAppear生命周期钩子中获取屏幕尺寸,确保UI元素正确布局
  • 动态生成频率(1000ms)与初始数量(8条)经过游戏节奏设计
  • Y坐标限制在中间区域(70-430),避免NPC鱼生成在屏幕边缘

五、附:源代码
import { display } from "@kit.ArkUI";// 优化后的Fish基类(增加生命值、分数等属性)
class Fish {size: number = 0;          // 体型(决定捕食关系)speed: number = 0;         // 移动速度positionX: number = 0;     // X坐标positionY: number = 0;     // Y坐标isPlayer: boolean = false; // 是否为玩家控制name: string = "";         // 角色标识health: number = 100;      // 生命值(可被大鱼攻击扣除)scoreValue: number = 0;    // 被吃掉时提供的分数direction: number = 1;     // 移动方向(1:右, -1:左)constructor(size: number,speed: number,x: number,y: number,isPlayer: boolean,name: string,direction: number = 1) {this.size = size;this.speed = speed;this.positionX = x;this.positionY = y;this.isPlayer = isPlayer;this.name = name;this.scoreValue = Math.floor(size * 1.5); // 体型越大分数越高this.direction = direction;}
}// 玩家鱼(增加拖拽控制逻辑)
export class PlayerFish extends Fish {private isDragging: boolean = false; // 是否正在拖拽constructor(size: number, speed: number, x: number, y: number) {super(size, speed, x, y, true, "Player");}// 设置拖拽状态setDragging(isDragging: boolean) {this.isDragging = isDragging;}// 获取拖拽状态getIsDragging(): boolean {return this.isDragging;}// 边界检测(优化版)checkBoundary() {let displayClass: display.Display = display.getDefaultDisplaySync();const screenWidth = displayClass.width / displayClass.densityPixels;const screenHeight = displayClass.height / displayClass.densityPixels;if (this.positionX < 0) this.positionX = 0;if (this.positionY < 0) this.positionY = 0;if (this.positionX > screenWidth - this.size * 2) {this.positionX = screenWidth - this.size * 2;}if (this.positionY > screenHeight - this.size * 2) {this.positionY = screenHeight - this.size * 2;}}
}// NPC鱼(增加AI行为策略)
class NPCFish extends Fish {private directionX: number = 0;private directionY: number = 0;private lastDirectionChange: number = 0;private playerFish: PlayerFish | null = null;constructor(size: number,speed: number,x: number,y: number,playerFish: PlayerFish,direction: number) {super(size, speed, x, y, false, `NPC-${Math.floor(Math.random() * 1000)}`, direction);this.playerFish = playerFish;this.resetDirection();}// 重置移动方向(随机生成)private resetDirection() {// 根据初始方向设置基础X方向this.directionX = this.direction * (Math.random() * 0.5 + 0.3); // 增大X方向速度范围// 随机Y方向this.directionY = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 0.3);this.lastDirectionChange = Date.now();}// AI自动移动(包含躲避与追逐逻辑)autoMove() {// 定期改变移动方向(模拟自然游动)if (Date.now() - this.lastDirectionChange > 1000 + Math.random() * 2000) {this.resetDirection();}// 基础移动this.positionX += this.directionX * this.speed;this.positionY += this.directionY * this.speed;// 边界检测(优化为从一侧消失后从另一侧出现)let displayClass: display.Display = display.getDefaultDisplaySync();const screenWidth = displayClass.width / displayClass.densityPixels;const screenHeight = displayClass.height / displayClass.densityPixels;// 确保鱼在屏幕内生成和移动if (this.positionX < -this.size * 2) {// 从左侧消失,重新出现在右侧this.positionX = screenWidth + this.size * 2;this.positionY = Math.random() * (screenHeight - 100) + 50;} else if (this.positionX > screenWidth + this.size * 2) {// 从右侧消失,重新出现在左侧this.positionX = -this.size * 2;this.positionY = Math.random() * (screenHeight - 100) + 50;}if (this.positionY < 0) {this.directionY *= -1;} else if (this.positionY > screenHeight) {this.directionY *= -1;}// 特殊行为:NPC鱼对玩家鱼的反应if (this.playerFish && this.playerFish.size > this.size) {// 玩家鱼更大时,NPC鱼尝试躲避const playerDistX = this.playerFish.positionX - this.positionX;const playerDistY = this.playerFish.positionY - this.positionY;const dist = Math.sqrt(playerDistX * playerDistX + playerDistY * playerDistY);if (dist < 200) { // 当玩家鱼靠近时// 向远离玩家鱼的方向移动this.directionX = -playerDistX / dist * 0.3;this.directionY = -playerDistY / dist * 0.3;}} else if (this.playerFish && this.size > this.playerFish.size) {// NPC鱼更大时,尝试追逐玩家鱼const playerDistX = this.playerFish.positionX - this.positionX;const playerDistY = this.playerFish.positionY - this.positionY;const dist = Math.sqrt(playerDistX * playerDistX + playerDistY * playerDistY);if (dist < 300) { // 当玩家鱼在范围内时// 向玩家鱼方向移动this.directionX = playerDistX / dist * 0.3;this.directionY = playerDistY / dist * 0.3;}}}
}@Component
export struct play_6 {@State playerFish: PlayerFish = new PlayerFish(30, 3, 150, 300); // 玩家鱼初始位置@State npcFishes: Array<NPCFish> = [];@State score: number = 0;@State gameOver: boolean = false;@State isDragging: boolean = false; // 拖拽状态@State dragOffsetX: number = 0;    // 拖拽偏移量X@State dragOffsetY: number = 0;    // 拖拽偏移量Y@State screenWidth: number = 800;  // 屏幕宽度@State screenHeight: number = 400; // 屏幕高度// 初始化游戏aboutToAppear() {console.info("游戏初始化,玩家鱼位置: " + this.playerFish.positionX + ", " + this.playerFish.positionY);// 获取屏幕尺寸try {let displayClass: display.Display = display.getDefaultDisplaySync();this.screenWidth = displayClass.width / displayClass.densityPixels;this.screenHeight = displayClass.height / displayClass.densityPixels;console.info(`屏幕尺寸: ${this.screenWidth}x${this.screenHeight}`);} catch (error) {console.error("获取屏幕尺寸失败,使用默认值", error);}this.initNPCFishes(8);this.gameLoop(); // 启动游戏循环// 定时生成新鱼setInterval(() => {this.spawnRandomFish();}, 1000);}// 初始化NPC鱼private initNPCFishes(count: number) {for (let i = 0; i < count; i++) {this.spawnRandomFish();}}// 随机生成NPC鱼(从左右两侧)private spawnRandomFish() {const size = Math.floor(Math.random() * 25) + 10;const speed = 3 - size / 10;// 随机决定从左侧还是右侧生成const fromLeft = Math.random() > 0.5;let x: number, direction: number;if (fromLeft) {// 从左侧生成,向右移动x = -size * 2; // 左侧屏幕外direction = 1;} else {// 从右侧生成,向左移动x = this.screenWidth + size * 2; // 右侧屏幕外direction = -1;}// 确保Y坐标在屏幕中间区域const y = Math.floor(Math.random() * (500 - 70)) + 70;this.npcFishes.push(new NPCFish(size, speed, x, y, this.playerFish, direction));console.info(`生成NPC鱼: 位置(${x},${y}), 方向${direction}, 大小${size}`);}// 游戏主循环private gameLoop() {if (this.gameOver) return;// 更新NPC鱼状态this.npcFishes.forEach(npc => {npc.autoMove();// 调试:输出NPC鱼位置console.info(`NPC鱼位置: ${npc.positionX},${npc.positionY}`);});// 检查碰撞和生存状态this.checkCollisions();this.checkPlayerSurvival();// 下一帧setTimeout(() => {this.gameLoop();}, 30);}// 碰撞检测与捕食逻辑private checkCollisions() {const toRemove: number[] = [];for (let i = 0; i < this.npcFishes.length; i++) {const npc = this.npcFishes[i];// 玩家鱼捕食NPC鱼if (this.playerFish.size > npc.size && this.isColliding(this.playerFish, npc)) {// 玩家鱼成长this.playerFish.size += npc.size * 0.15;this.score += npc.scoreValue;// 更新玩家鱼速度(体型越大速度越慢)this.playerFish.speed = Math.max(1, 3 - this.playerFish.size / 20);// 标记NPC鱼为移除toRemove.push(i);}}// 从后往前移除,避免索引问题for (let i = toRemove.length - 1; i >= 0; i--) {this.npcFishes.splice(toRemove[i], 1);}}// 检测玩家鱼是否被吃掉private checkPlayerSurvival() {for (const npc of this.npcFishes) {if (npc.size > this.playerFish.size && this.isColliding(this.playerFish, npc)) {// 玩家鱼生命值减少this.playerFish.health -= 20;if (this.playerFish.health <= 0) {// 游戏结束this.gameOver = true;}}}}// 碰撞检测函数private isColliding(fish1: Fish, fish2: Fish): boolean {const dx = fish1.positionX - fish2.positionX;const dy = fish1.positionY - fish2.positionY;const distance = Math.sqrt(dx * dx + dy * dy);return distance < (fish1.size + fish2.size) / 2;}build() {Stack() {// 背景Row().backgroundColor('#e2e9ef').width('100%').height('100%')// 分数显示Text(`分数: ${this.score}`).fontSize(24).fontColor('#FFFFFF').position({ x: 20, y: 20 })// 玩家鱼(拖拽控制)Text('🐟').fontSize(this.playerFish.size).width(this.playerFish.size * 2).height(this.playerFish.size * 2).borderRadius(this.playerFish.size).backgroundColor('#FF0000').position({x: this.playerFish.positionX - (this.playerFish.size * 2) / 2,y: this.playerFish.positionY - (this.playerFish.size * 2) / 2}).gesture(PanGesture().onActionStart((event: GestureEvent) => {// 开始拖拽,计算偏移量this.isDragging = true;this.dragOffsetX = event.offsetX - (this.playerFish.positionX - (this.playerFish.size * 2) / 2);this.dragOffsetY = event.offsetY - (this.playerFish.positionY - (this.playerFish.size * 2) / 2);}).onActionUpdate((event: GestureEvent) => {// 拖拽中更新位置if (this.isDragging) {this.playerFish.positionX = event.offsetX - this.dragOffsetX;this.playerFish.positionY = event.offsetY - this.dragOffsetY;this.playerFish.checkBoundary();}}).onActionEnd(() => {// 拖拽结束this.isDragging = false;}))// NPC鱼(修复显示问题)ForEach(this.npcFishes, (npc: NPCFish) => {Text(npc.direction > 0 ? '🐠' : '🐟').fontSize(npc.size).width(npc.size * 1.5).height(npc.size * 1.5).borderRadius(npc.size * 0.75).backgroundColor('#ADD8E6').position({x: npc.positionX - (npc.size * 1.5) / 2,y: npc.positionY - (npc.size * 1.5) / 2})})// 游戏结束提示if (this.gameOver) {Column() {Text('游戏结束!').fontSize(48).fontWeight(FontWeight.Bold).fontColor('#FF0000')Text(`最终分数: ${this.score}`).fontSize(32).fontColor('#FFFFFF')Button('重新开始').onClick(() => {this.playerFish = new PlayerFish(30, 3, 150, 300);this.npcFishes = [];this.score = 0;this.gameOver = false;this.initNPCFishes(8);})}.width('80%').height('30%').backgroundColor('rgba(0, 0, 0, 0.7)').borderRadius(20).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).position({x: (this.screenWidth - this.screenWidth * 0.8) / 2,y: (this.screenHeight - this.screenHeight * 0.3) / 2})}}.width('100%').height('100%')}
}
http://www.xdnf.cn/news/13672.html

相关文章:

  • G1周打卡——GAN入门
  • 考研系列—408真题操作系统篇(2015-2019)
  • 煜邦智源SNEC全球首发智慧储能系统,携手德国莱茵TÜV加速全球化布局
  • Java 中使用 Redis 注解版缓存——补充
  • Qt Creator 从入门到项目实战
  • 「pandas 与 numpy」数据分析与处理全流程【数据分析全栈攻略:爬虫+处理+可视化+报告】
  • 图论 算法1
  • 2022年TASE SCI2区,学习灰狼算法LGWO+随机柔性车间调度,深度解析+性能实测
  • 手写muduo网络库(七):深入剖析 Acceptor 类
  • 【leetcode】226. 翻转二叉树
  • 专题:2025年跨境B2B采购买家行为分析及采购渠道研究报告|附160+份报告PDF汇总下载
  • 公网 IP 地址SSL证书实现 HTTPS 访问完全指南
  • 暴雨亮相2025中关村论坛数字金融与金融安全大会
  • Guava 在大数据计算场景下的使用指南
  • 《性能之巅》第十章 网络
  • Linux下OLLAMA安装卡住怎么办?
  • 为什么TCP有粘包问题,而UDP没有
  • RK3568 1U机箱,支持电口光口B码对时,适用于电力、交通等
  • Oracle Form判断表单数据重复方法
  • linux 中pdf 的自动分页工具
  • 单片机的中断功能-简要描述(外部中断为例)(8)
  • ArkUI-X在Android上使用Fragment开发指南
  • 多节点并行处理架构
  • Linux 下 pcie 初始化设备枚举流程代码分析
  • 【软件开发】上位机 下位机概念
  • C++11 Type Aliases:从入门到精通
  • Linux笔记之Ubuntu22.04安装 fcitx5 输入法
  • pandas 字符串列迁移至 PyArrow 完整指南:从 object 到 string[pyarrow]
  • Nodejs特训专栏-基础篇:2. JavaScript核心知识在Node.js中的应用
  • STM32 开发 - STM32CubeMX 下载、安装、连接服务器