Java游戏开发实践:从ECS架构到经典游戏实现

news2026/5/8 8:54:26
1. 项目概述与核心价值最近在整理个人开源项目时我重新审视了“huazie/flea-game”这个仓库。这不仅仅是一个简单的游戏代码集合它更像是一个面向Java开发者的、以游戏为载体的综合技术实践平台。很多开发者尤其是刚入行不久的朋友常常苦恼于如何将学校里学的设计模式、架构思想、性能优化等理论知识应用到真实、有趣且有完整闭环的项目中去。教科书上的例子往往过于抽象而大型商业项目又过于复杂难以窥其全貌。这个项目正是为了解决这个痛点而生。“flea-game”的核心定位是通过实现一系列经典、轻量的游戏如贪吃蛇、俄罗斯方块、扫雷等来系统性地演练和展示Java后端开发中的关键技术栈与最佳实践。它不是一个追求极致画面和复杂玩法的游戏引擎而是一个“技术演示沙盒”。在这里你可以看到如何用纯Java或配合轻量级框架构建一个可运行的游戏逻辑核心如何设计可扩展的游戏架构如何处理状态同步、数据持久化甚至如何为简单的游戏引入AI对手。对于学习者而言它是一个绝佳的“脚手架”和“参考实现”对于面试者它是一个能体现你综合技术能力的“活简历”对于技术爱好者它则是一个充满乐趣的编码游乐场。2. 项目整体架构与设计思路拆解2.1 为什么选择“游戏”作为技术载体在决定技术演示项目的形态时我考虑过博客系统、电商秒杀、OA办公等常见选题。但最终选择游戏基于以下几点考量趣味性与动力游戏的趣味性天然能激发开发和学习的热情。实现一个能动的角色、一个可交互的界面其成就感远超完成一个增删改查接口。逻辑完整性一个哪怕再简单的游戏也包含了状态管理、用户输入、实时或回合更新、规则判定、渲染展示等完整闭环。这比一个孤立的CRUD模块更能锻炼系统思维。技术场景丰富游戏虽小五脏俱全。它可以涉及并发与线程安全游戏主循环、AI计算线程、网络通信线程。数据结构与算法地图的网格表示二维数组、寻路算法A*、碰撞检测、游戏状态树的搜索与评估用于AI。设计模式游戏循环模板方法模式、游戏实体组合模式、状态管理状态模式、事件处理观察者模式。数据持久化玩家存档、高分榜、游戏配置。网络通信实现一个简单的多人对战或排行榜同步。2.2 核心架构设计分层与模块化为了避免代码随着游戏种类的增加而变成一团乱麻项目从一开始就采用了清晰的分层架构。这不是一个庞然大物但遵循了良好的工程实践。1. 核心层 (Core Layer)这是项目的基石完全独立于任何具体的游戏实现。它定义了整个游戏世界运转的基本规则和接口。GameEngine游戏引擎接口与基础实现。它封装了经典的游戏主循环initialize - update - render - dispose控制着游戏的节奏通过帧率或回合。不同的游戏可以继承或组合这个引擎。GameContext游戏上下文。这是一个中心化的状态容器持有当前游戏的配置、资源管理器、事件总线等全局访问点。它避免了大量的静态变量和单例使依赖注入和测试更容易。Entity-Component-System (ECS) 雏形虽然没有实现完整的ECS框架但借鉴了其思想。游戏中的一切蛇身、方块、地雷都被视为GameObject实体其行为由附加的Component组件如MovementComponent,RenderComponent,CollisionComponent定义。System系统则负责处理所有拥有特定组件的实体。这种设计极大地提升了代码的复用性和灵活性添加新功能只需组合现有组件或创建新组件而无需修改大量实体类。2. 游戏逻辑层 (Game Logic Layer)这一层包含各个具体游戏的实现。每个游戏都是一个独立的模块例如snake,tetris,minesweeper。每个游戏模块实现自己的GameWorld用于管理游戏独有的状态如贪吃蛇的蛇身坐标数组、俄罗斯方块的当前方块和下一个方块队列。实现具体的GameRule定义游戏的胜负条件、得分规则、碰撞响应等。定义该游戏特有的Component和System。例如贪吃蛇模块会有SnakeMovementSystem俄罗斯方块模块会有LineClearSystem。3. 表示层 (Presentation Layer)负责将游戏状态展示给用户。为了保持核心逻辑的纯净这一层通过接口抽象。Renderer接口定义渲染方法。项目提供了两种主要实现ConsoleRenderer基于控制台的字符渲染。这是最简单、最轻量的实现无需任何GUI库适合快速验证逻辑和服务器端无头运行。SwingRenderer/JavaFXRenderer基于桌面GUI框架的图形渲染。提供更友好的视觉体验。输入处理同样抽象为InputHandler接口可以对接键盘、鼠标或网络指令。4. 数据层 (Data Layer)处理游戏数据的持久化。PersistenceService接口定义保存和加载游戏存档、高分榜的方法。提供基于文件JSON/XML序列化、简单数据库如H2的实现示例。设计心得这种分层和模块化的设计使得添加一个新游戏变得非常标准化。你基本上只需要在游戏逻辑层新建一个模块实现特定的GameWorld和GameRule必要时添加新组件然后通过配置将其组装起来。核心引擎和通用组件完全不需要改动。这正体现了“对修改封闭对扩展开放”的开闭原则。3. 关键技术点深度解析与实现3.1 游戏主循环的精妙控制游戏主循环是游戏的心跳。一个稳定、高效的主循环至关重要。在flea-game中我实现了一个可配置、自适应性的主循环。public class DefaultGameEngine implements GameEngine { private volatile boolean running false; private long targetUpdateIntervalNs; // 目标每次更新间隔纳秒由目标FPS计算得出 private double timeAccumulator 0.0; // 时间累积器用于固定时间步长更新 Override public void start() { running true; long lastTime System.nanoTime(); // 游戏循环 while (running) { long currentTime System.nanoTime(); long elapsedTime currentTime - lastTime; // 实际经过的时间纳秒 lastTime currentTime; // 处理输入 inputHandler.processInput(); // 使用固定时间步长进行游戏状态更新避免帧率波动影响物理逻辑 timeAccumulator elapsedTime; while (timeAccumulator targetUpdateIntervalNs) { gameWorld.update(targetUpdateIntervalNs / 1_000_000_000.0); // 转换为秒 timeAccumulator - targetUpdateIntervalNs; } // 渲染渲染频率可以与更新频率不同 renderer.render(gameWorld); // 简单的帧率控制如果更新渲染太快则休眠一小会儿 long frameTime System.nanoTime() - currentTime; if (frameTime targetUpdateIntervalNs) { try { Thread.sleep((targetUpdateIntervalNs - frameTime) / 1_000_000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); stop(); } } } } }关键点解析固定时间步长更新这是实现稳定物理和游戏逻辑的关键。无论实际帧率是60FPS还是30FPSgameWorld.update(deltaTime)中的deltaTime都是固定的例如1/60秒。这确保了蛇的移动速度、方块的下落速度不会因为帧率波动而忽快忽慢。timeAccumulator用来累积多余的时间确保在“卡顿”后能补上应有的更新次数。渲染与更新解耦渲染的频率可以独立于更新频率。例如更新可以固定为60Hz而渲染可以根据显示器刷新率或性能自适应。这提供了更好的灵活性。简单的帧率控制通过Thread.sleep进行粗略的帧率控制。对于更精确的控制可以考虑使用LockSupport.parkNanos或在图形框架中使用垂直同步。避坑指南在早期版本中我直接使用elapsedTime作为deltaTime进行更新这导致了在性能不同的机器上游戏速度完全不同。切换到固定时间步长后问题得以解决。另外timeAccumulator要使用浮点数double来存储避免整数除法的精度损失导致更新次数计算错误。3.2 基于组件Component的实体系统实现如前所述项目采用了ECS思想的简化版。我们来看看一个典型的GameObject和Component是如何工作的。// 游戏对象本质上是一个ID和一组组件的容器 public class GameObject { private final String id; private final MapClass? extends Component, Component components new ConcurrentHashMap(); public T extends Component T getComponent(ClassT componentClass) { return componentClass.cast(components.get(componentClass)); } public void addComponent(Component component) { components.put(component.getClass(), component); component.setOwner(this); } // ... 其他方法 } // 组件基类 public abstract class Component { protected GameObject owner; public void setOwner(GameObject owner) { this.owner owner; } public abstract void update(double deltaTime); } // 具体组件示例位置组件 public class PositionComponent extends Component { private int x; private int y; // getters and setters... Override public void update(double deltaTime) { /* 位置组件可能不每帧更新由MovementSystem驱动 */ } } // 具体组件示例移动组件 public class MovementComponent extends Component { private int velocityX; private int velocityY; Override public void update(double deltaTime) { PositionComponent pos owner.getComponent(PositionComponent.class); if (pos ! null) { pos.setX(pos.getX() (int)(velocityX * deltaTime)); pos.setY(pos.getY() (int)(velocityY * deltaTime)); } } }系统System负责遍历所有拥有特定组件组合的实体并执行逻辑public class MovementSystem implements GameSystem { Override public void update(double deltaTime, GameWorld world) { // 获取所有同时拥有PositionComponent和MovementComponent的实体 ListGameObject entities world.getEntitiesWith(PositionComponent.class, MovementComponent.class); for (GameObject entity : entities) { MovementComponent mover entity.getComponent(MovementComponent.class); mover.update(deltaTime); // 驱动移动组件更新进而修改位置 } } }这种设计的好处高度复用PositionComponent和RenderComponent可以被贪吃蛇的蛇身、俄罗斯方块的方块、甚至UI按钮共用。灵活组合要创建一个新的敌人类型你只需要实例化一个GameObject然后为其添加PositionComponent,MovementComponent,AiComponent,RenderComponent即可。无需创建复杂的继承树。数据与逻辑分离组件是纯数据或包含少量自更新逻辑系统是纯逻辑。这使得并行处理和数据缓存优化成为可能虽然本项目未深入。实操心得在实现ECS时组件的查询效率是关键。本项目使用了一个简单的ConcurrentHashMap来存储实体列表并通过遍历来匹配组件。对于实体数量很少的2D游戏这完全足够。但如果实体数量上万就需要考虑更高效的数据结构如为每个实体分配一个位掩码Bitmask来表示其拥有的组件类型系统通过位运算快速筛选实体。3.3 游戏状态管理与事件驱动通信游戏中有很多状态需要管理如“开始菜单”、“游戏中”、“游戏暂停”、“游戏结束”。同时各个系统之间需要通信比如“碰撞系统”检测到蛇撞墙需要通知“游戏规则系统”结束游戏。1. 状态模式管理游戏流程我使用状态模式来管理游戏的整体流程。public interface GameState { void enter(GameContext context); void update(double deltaTime); void render(Renderer renderer); void exit(); } public class PlayingState implements GameState { private GameWorld currentWorld; Override public void enter(GameContext context) { currentWorld context.getCurrentWorld(); currentWorld.initialize(); } Override public void update(double deltaTime) { if (context.getInputHandler().isKeyPressed(KeyEvent.VK_P)) { // 按下P键切换到暂停状态 context.getStateMachine().changeState(new PausedState()); return; } currentWorld.update(deltaTime); if (currentWorld.isGameOver()) { context.getStateMachine().changeState(new GameOverState(currentWorld.getScore())); } } // ... render 和 exit 方法 }一个GameStateMachine负责状态的切换和委托更新/渲染调用。这使得游戏流程清晰每个状态的责任明确易于调试和扩展比如添加一个“关卡选择”状态。2. 轻量级事件总线解耦模块为了让CollisionSystem不必直接调用GameRule的方法我引入了一个简单的事件总线发布-订阅模式。// 事件类 public class CollisionEvent implements GameEvent { private GameObject entityA; private GameObject entityB; // ... getters } // 事件总线 public class SimpleEventBus { private final MapClass? extends GameEvent, ListConsumerGameEvent handlers new HashMap(); public E extends GameEvent void subscribe(ClassE eventType, ConsumerE handler) { handlers.computeIfAbsent(eventType, k - new ArrayList()).add((ConsumerGameEvent) handler); } public void publish(GameEvent event) { ListConsumerGameEvent eventHandlers handlers.get(event.getClass()); if (eventHandlers ! null) { for (ConsumerGameEvent handler : eventHandlers) { handler.accept(event); } } } } // 使用示例在CollisionSystem中 public class CollisionSystem implements GameSystem { Override public void update(double deltaTime, GameWorld world) { // ... 检测碰撞逻辑 if (collisionDetected) { world.getEventBus().publish(new CollisionEvent(snakeHead, wall)); } } } // 在GameRule中订阅 public class SnakeGameRule implements GameRule { public SnakeGameRule(GameContext context) { context.getEventBus().subscribe(CollisionEvent.class, this::handleCollision); } private void handleCollision(GameEvent event) { CollisionEvent collision (CollisionEvent) event; if (collision.getEntityB().hasTag(WALL)) { this.gameOver true; // 蛇撞墙游戏结束 } } }事件总线极大地降低了系统间的耦合度。CollisionSystem只负责发布“发生了碰撞”这个事实至于碰撞后是扣血、加分还是游戏结束由订阅该事件的GameRule或其他系统来决定。4. 具体游戏模块实现剖析以“贪吃蛇”为例让我们深入到具体的游戏模块看看上述架构是如何落地的。贪吃蛇是一个非常好的起点逻辑清晰但涵盖了实体管理、输入处理、碰撞检测、状态更新等核心概念。4.1 贪吃蛇世界的构建SnakeWorld继承自BaseGameWorld它需要管理蛇用一个LinkedListGameObject来表示蛇身。每个蛇身段是一个拥有PositionComponent和RenderComponent的GameObject。蛇头额外拥有MovementComponent和CollisionComponent。食物一个拥有PositionComponent和RenderComponent的GameObject。墙壁/边界一组静态的GameObject拥有PositionComponent、RenderComponent和CollisionComponent。初始化时创建这些实体并添加到世界中。SnakeWorld还维护了当前得分、游戏是否结束等状态。4.2 输入处理与蛇的移动贪吃蛇的输入相对简单上下左右控制方向。在SnakeInputHandler中监听键盘事件并设置蛇头MovementComponent的速度方向。这里有一个关键细节防止原地掉头。例如蛇正在向右移动此时快速按下左键如果直接响应蛇头会瞬间左转撞到自己第二节身体导致非玩家本意的死亡。因此需要缓冲输入指令在每次移动更新前只允许蛇转向至与当前方向垂直的方向。public class SnakeInputHandler implements InputHandler { private Direction nextDirection Direction.RIGHT; // 下一个允许的方向 private Direction currentDirection Direction.RIGHT; Override public void processInput() { // 从键盘监听器获取按键状态isKeyPressed if (isKeyPressed(UP) currentDirection ! Direction.DOWN) { nextDirection Direction.UP; } else if (isKeyPressed(DOWN) currentDirection ! Direction.UP) { nextDirection Direction.DOWN; } // ... 处理LEFT和RIGHT } public Direction getDirectionForNextMove() { Direction dir nextDirection; currentDirection dir; // 移动后当前方向更新 // nextDirection 保持不变直到有新的有效输入覆盖它 return dir; } }SnakeMovementSystem在每帧更新时会调用inputHandler.getDirectionForNextMove()获取方向然后设置蛇头MovementComponent的速度最后由MovementSystem统一计算位置更新。4.3 碰撞检测与响应贪吃蛇的碰撞检测很简单因为所有物体都在网格上。我们使用基于网格的检测。与食物的碰撞检查蛇头的位置是否与食物位置重合。如果重合则发布FoodEatenEvent事件。SnakeGameRule订阅此事件增加分数并在随机空位置生成新食物。蛇身不立即增长而是在下一帧移动时不移除蛇尾这是经典贪吃蛇的增长逻辑。与墙壁或自身的碰撞检查蛇头位置是否与任何墙壁实体或蛇身其他部分的位置重合。如果重合则发布CollisionEvent事件。SnakeGameRule处理此事件将游戏状态置为结束。高效的自身碰撞检测蛇身可能很长逐段比较效率低。一个优化方法是使用一个与游戏网格同大小的二维布尔数组occupiedGrid在每帧更新所有实体位置后同步更新这个数组。检测碰撞时只需检查蛇头目标位置在occupiedGrid中是否为true即可时间复杂度O(1)。4.4 渲染从控制台到图形界面为了展示分层架构的威力贪吃蛇模块提供了两种渲染器。ConsoleSnakeRenderer用字符表示游戏元素。例如用表示蛇头用o表示蛇身用*表示食物用#表示墙。它遍历游戏世界网格根据occupiedGrid和实体类型打印相应字符。这种渲染器对于远程SSH调试或性能基准测试非常有用。SwingSnakeRenderer使用Java Swing的JPanel和Graphics2D进行绘制。每个网格单元绘制为一个填充的矩形或圆角矩形使用不同的颜色区分蛇头、蛇身、食物和墙壁。它可以提供更平滑的动画通过双缓冲和更丰富的视觉反馈如吃食物时的闪烁效果。两种渲染器实现了同一个Renderer接口游戏主循环完全不需要知道当前使用的是哪一种实现了表示层与逻辑层的彻底解耦。5. 项目工程化与进阶实践5.1 依赖管理与构建工具项目使用Maven作为构建工具。pom.xml文件清晰地划分了模块依赖。flea-game-core核心模块包含引擎、ECS基础、工具类等。其他所有模块都依赖它。flea-game-snake,flea-game-tetris等具体游戏模块依赖core模块。flea-game-launcher启动器模块负责组装具体的游戏世界、选择渲染器、启动引擎。它依赖所有游戏模块。这种结构允许用户只引入他们感兴趣的游戏模块。Maven的父子工程管理使得版本号和通用依赖如JUnit, Log4j2可以集中管理。5.2 单元测试与模拟游戏逻辑的单元测试非常重要尤其是规则复杂的游戏如俄罗斯方块的旋转和消行判定。我们利用架构优势可以轻松地对GameRule和Component进行测试。模拟输入和渲染在测试中我们可以注入一个MockInputHandler来模拟按键序列注入一个MockRenderer来捕获渲染调用从而验证游戏在特定输入序列下是否产生了正确的状态变化和输出。测试游戏规则直接实例化GameWorld和GameRule通过程序化操作实体如手动移动方块然后断言游戏状态分数、是否结束等。例如测试俄罗斯方块在填满一行后是否正确消行并加分。使用JUnit 5和AssertJ使测试代码更简洁、表达力更强。Test void testSnakeGrowsAfterEatingFood() { // 1. 初始化世界蛇长度为3食物在蛇头前方一格 SnakeWorld world new SnakeWorld(10, 10); PositionComponent foodPos world.getFood().getComponent(PositionComponent.class); // 将蛇头移动到食物位置 world.getSnakeHead().getComponent(PositionComponent.class).setPosition(foodPos.getX(), foodPos.getY()); // 2. 触发更新碰撞检测和响应在update中发生 world.update(1.0/60.0); // 3. 断言分数增加蛇身长度在下一帧移动后会增长可通过检查蛇身链表长度或世界状态 assertThat(world.getScore()).isEqualTo(INITIAL_SCORE FOOD_SCORE); // 更精确的断言需要模拟一次移动更新 }5.3 性能考量与优化点虽然这些2D小游戏对性能要求不高但良好的实践习惯值得培养。对象池游戏中频繁创建和销毁GameObject如俄罗斯方块的方块、射击游戏中的子弹会产生GC压力。可以实现一个简单的GameObjectPool来复用对象。空间分区如果游戏实体很多碰撞检测的复杂度会变成O(n²)。可以使用空间划分数据结构如网格Grid、四叉树Quadtree来快速缩小检测范围。本项目中的occupiedGrid就是一种简单的网格空间分区。渲染优化对于图形渲染只重绘发生变化的区域脏矩形。在Swing中可以通过repaint(int x, int y, int width, int height)来指定。日志与监控集成SLF4J和Logback在关键路径如每帧时间、事件处理添加DEBUG级别日志方便在出现性能问题时进行诊断。5.4 扩展可能性网络与AI项目的架构为扩展提供了便利。网络对战可以创建一个NetworkComponent和NetworkSystem。NetworkSystem负责序列化游戏世界的关键状态如所有实体的位置、速度通过WebSocket或自定义TCP协议发送给客户端。客户端作为另一个渲染器接收状态并渲染。服务器端负责运行权威的游戏逻辑处理所有输入和判定。这可以演变成一个简单的多人贪吃蛇对战demo。AI玩家为游戏添加AI对手非常有趣。以贪吃蛇为例可以创建一个AiComponent附加到AI蛇的蛇头上。AiSystem根据当前食物位置和地图障碍物使用寻路算法如BFS或A*计算出一条路径然后控制MovementComponent的方向。这直接演示了如何在游戏架构中集成算法模块。6. 常见问题、调试技巧与项目运行指南6.1 环境准备与项目导入必备环境JDK 8或以上Maven 3.6Git。获取代码git clone https://github.com/huazie/flea-game.git导入IDE推荐使用IntelliJ IDEA或Eclipse直接打开项目根目录包含父pom.xml的文件夹IDE会自动识别为Maven项目并下载依赖。依赖问题如果出现依赖下载失败检查Maven配置文件settings.xml的镜像源建议使用阿里云等国内镜像加速。6.2 运行与体验不同游戏项目的主入口在flea-game-launcher模块中。通常会有多个带有main方法的启动类。ConsoleSnakeLauncher启动控制台版贪吃蛇。SwingTetrisLauncher启动Swing图形界面的俄罗斯方块。 运行前可能需要先对整个项目执行mvn clean install确保所有模块编译打包成功。6.3 开发中常见问题与解决问题现象可能原因排查与解决思路游戏运行速度异常快或慢游戏主循环的帧率控制未生效或计算错误。1. 检查targetUpdateIntervalNs的计算是否正确1e9 / targetFPS。2. 在循环内打印每帧实际耗时看是否远小于或大于目标间隔。3. 确认是否使用了固定时间步长更新timeAccumulator逻辑是否正确。控制台输入无响应控制台渲染器阻塞了线程或输入监听器未正确初始化。1. 确保使用Scanner或ConsoleAPI时放在独立线程不要阻塞主游戏循环。2. 对于Swing/JavaFX确保事件监听器被正确添加到UI组件上。碰撞检测失灵碰撞检测系统未正确执行或实体位置未同步更新。1. 调试CollisionSystem.update方法打印参与检测的实体位置。2. 检查实体组件的更新顺序MovementSystem更新位置后CollisionSystem才能检测到新位置。3. 检查碰撞条件判断如相等判断对于浮点数需用容差。游戏状态切换混乱状态机可能在单帧内被多次触发状态切换。1. 在StateMachine.changeState()方法中添加日志查看切换序列。2. 确保状态切换逻辑如按键检测只在每帧的固定阶段执行一次避免在事件回调中重复触发。内存使用缓慢增长可能存在内存泄漏如未正确注销事件监听器、对象池未回收对象。1. 使用JProfiler或VisualVM监控堆内存查看哪个类的实例数持续增长。2. 检查在GameObject被销毁时是否从其所属的所有System中移除并取消事件订阅。6.4 为项目贡献新游戏如果你想实现一个新游戏比如打砖块、坦克大战以下是标准步骤新建模块在项目根目录下参照flea-game-snake创建一个新的Maven模块例如flea-game-breakout。定义游戏世界创建BreakoutWorld类继承BaseGameWorld。在其中定义球拍、球、砖块等实体并初始化它们。定义游戏规则创建BreakoutGameRule类实现GameRule接口。定义球与砖块、边界、球拍的碰撞响应规则和计分规则。创建专属组件与系统如果需要创建如BallPhysicsComponent处理球的反弹角度计算、PaddleControlComponent等。创建对应的BallPhysicsSystem、PaddleControlSystem。创建渲染器至少实现一个ConsoleBreakoutRenderer。如果想更美观可以实现SwingBreakoutRenderer。创建启动器在launcher模块或新模块中创建一个BreakoutLauncher负责组装以上部分并启动游戏引擎。编写测试为关键的游戏规则如砖块消除、球拍反弹角度计算编写单元测试。更新文档在项目的README中介绍你的新游戏并可能的话添加运行截图。遵循这个流程你的代码将完美融入现有架构享受所有基础设施引擎、ECS、事件、状态机带来的便利。这个项目就像一套精心设计的乐高积木。核心模块提供了各种标准件引擎、组件、系统而具体的游戏则是你用这些标准件搭建出的不同模型。我希望通过这个项目不仅能让你重温经典游戏的乐趣更能深刻理解如何用工程化的思维去构建一个灵活、可维护、可扩展的软件系统。无论是用于学习、面试还是纯粹的技术娱乐它都提供了一个扎实的起点和丰富的可能性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2594277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…