news 2026/9/3 8:30:03

纯前端卡牌战斗原型:状态机与属性克制实现宝可梦对战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯前端卡牌战斗原型:状态机与属性克制实现宝可梦对战

用炫卡斗士W方式打开《宝可梦XYZ》,听起来更像一个网络梗,但落到前端开发上,它是一个很具体的问题:怎么把宝可梦对战的策略感,用卡牌游戏的交互方式重新做出来。下面从一个不依赖后端、双击就能运行的原型项目出发,拆解数据模型、战斗状态机、属性克制表、页面表现和常见排错路径。如果你已经会用 HTML/CSS/JavaScript 做简单页面,但不知道一个回合制卡牌战斗系统该怎么设计,这篇内容正好能补上这块拼图。

这里的“炫卡斗士W方式”,不指向任何商业产品,而是借用这类竖版卡牌游戏常见的体验特征:卡面占据屏幕主要位置、技能以按钮形式排列、出招前强调属性克制、伤害通过飘字和卡牌动画反馈。把这些特征用到《宝可梦XYZ》题材里,就是在做一个“宝可梦主题的卡牌对战原型”。原型不包含任何官方素材,只使用自定义示例卡牌数据,重点是学习数据结构、逻辑拆分和交互反馈。

1. 先拆解“用炫卡斗士W方式打开”的技术含义

1.1 炫卡斗士W方式指的是什么

“用X方式打开Y”原本是同人创作里常见的话术,表示换一种风格重新演绎。放到技术语境里,“炫卡斗士W方式”可以被理解为一种卡牌对战体验的设计要求:大尺寸卡面、明确的属性克制、两三个技能按钮、每回合只做一次选择,然后由系统完成结算和反馈。

从工程实现角度看,这种体验并不复杂,但它要求三个层次互相配合。

  • 数据层:卡牌有哪些字段,属性克制关系怎么表达。
  • 逻辑层:回合流程怎么流转,伤害怎么计算,胜负怎么判定。
  • 表现层:卡牌怎么渲染,血条怎么更新,技能反馈怎么播放。

很多初学者在写项目时只关注表现层,按钮点击后直接改血量,结果一旦加入属性克制、技能 PP、状态异常等规则,代码就乱成一团。这个项目先把逻辑层独立出来,用状态机管理回合,表现层只负责展示,后面扩展就会轻松很多。

1.2 卡牌化《宝可梦XYZ》要保留什么,要砍掉什么

《宝可梦XYZ》作为动画和游戏作品,本身包含探索、捕捉、培养、对战、剧情等多种系统。如果要把对战变成卡牌游戏,必须做取舍。

建议保留三种核心要素:宝可梦的属性、技能和 HP。属性决定了克制关系,技能决定攻击方式和威力,HP 决定战斗何时结束。这三样组合起来,已经能形成足够的策略深度。

建议先砍掉的东西包括:地图移动、遭遇战、精灵球捕捉、个体值、努力值、性格修正、天气和场地状态。这些不是不有趣,而是会显著增加数据模型和战斗逻辑的复杂度。第一版原型只有“选卡、出招、结算、判定”四个动作,跑通之后再逐步加状态效果。

1.3 技术方案选型:为什么用纯前端做第一版

选择纯 HTML/CSS/JavaScript 而不是立刻上 Vue 或 React,主要原因是第一版的目标是快速跑通战斗闭环。不需要构建工具,不需要安装依赖,一个浏览器就能运行,这能最大限度降低环境问题带来的干扰。

等数据模型和状态机稳定之后,再迁移到 Vue 或 React 并不难。逻辑层不依赖 DOM,换框架时只需要重写表现层。反过来,如果第一版就把状态和 UI 混在一起,换框架等于重写整个项目。所以这里采用“逻辑与渲染分离”的方式,这是整个设计里最重要的一条原则。

2. 环境准备与项目结构:先做到双击就能跑

2.1 环境要求

这个项目对环境要求非常低,只用浏览器和文本编辑器。

依赖说明最低要求
浏览器Chrome、Edge、Firefox 均可支持 ES6 语法即可
文本编辑器VS Code、WebStorm、记事本都行不强制插件
本地服务器可选,用于解决部分浏览器模块加载限制建议使用 VS Code Live Server 或 Python http.server

如果希望完全避免模块加载限制,最省事的方式是只写一个index.html文件,CSS 和 JavaScript 全部内联。文章后面会给出可拆分的工程结构,也会说明单文件方式怎么整合。

2.2 项目目录结构

poke-card-fight/ ├── index.html ├── styles.css └── src/ ├── main.js ├── data.js ├── battle.js └── ui.js
  • index.html是页面入口。
  • styles.css负责卡牌、按钮、血条、动画样式。
  • src/data.js存放卡牌数据和属性克制表。
  • src/battle.js封装战斗逻辑。
  • src/ui.js负责渲染和事件绑定。
  • src/main.js负责初始化项目。

如果不想折腾本地服务器,可以省略模块加载,把所有 JavaScript 合并成几个<script>标签或一个文件。下面先按工程化结构讲解,最后在排错部分说明单文件整合方式。

2.3 页面骨架

index.html只需要三个区域:玩家卡牌区、战斗日志区、敌方卡牌区。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>卡牌战斗原型</title> <link rel="stylesheet" href="styles.css" /> </head> <body> <div id="app"> <section id="enemy-area"></section> <section id="log-area"></section> <section id="player-area"></section> </div> <script type="module" src="./src/main.js"></script> </body> </html>

这样设计的好处是,逻辑层不需要关心 DOM 结构,只需要提供数据;UI 层拿到数据后负责填充卡牌内容、绑定技能按钮、更新血条和日志。

3. 卡牌数据模型与属性克制表:先把数据结构定清楚

3.1 卡牌字段设计

卡牌数据是整个项目的地基。字段设计不合理,后面加功能会很痛苦。对于当前原型,一张卡牌至少需要这些字段:

字段类型含义
idstring唯一标识
namestring宝可梦名称
elementstring属性,如fire
hpnumber最大生命值
attacknumber攻击力
defensenumber防御力
speednumber速度,后续决定先手顺序
skillsArray技能列表
imagestring卡面图片地址,可留空用占位色

以三张示例卡为例:

export const CARDS = [ { id: 'fire-starter', name: '焰尾狐', element: 'fire', hp: 120, attack: 85, defense: 65, speed: 90, skills: [ { name: '火球术', element: 'fire', power: 45, accuracy: 90 }, { name: '利爪', element: 'normal', power: 40, accuracy: 100 } ] }, { id: 'water-starter', name: '涌潮蛙', element: 'water', hp: 125, attack: 80, defense: 70, speed: 75, skills: [ { name: '水枪', element: 'water', power: 45, accuracy: 100 }, { name: '撞击', element: 'normal', power: 40, accuracy: 100 } ] }, { id: 'grass-starter', name: '枝冠鹿', element: 'grass', hp: 130, attack: 75, defense: 75, speed: 70, skills: [ { name: '藤鞭', element: 'grass', power: 45, accuracy: 100 }, { name: '飞叶快刀', element: 'grass', power: 35, accuracy: 95 } ] } ];

这里使用自定义名称作为示例,不包含任何官方美术素材,也不代表官方数据。真实项目中,这里的字段还可以继续扩展,比如raritymaxLevelskills.length,但第一版不要贪多。

3.2 属性克制表的实现

属性克制是宝可梦对战的核心策略。这里使用一个简化的克制表,覆盖火、水、草、电、普通五种属性。取值含义:2表示造成 2 倍伤害,0.5表示造成一半伤害,1表示正常伤害。

export const TYPE_CHART = { fire: { water: 0.5, grass: 2, electric: 1, normal: 1 }, water: { fire: 2, grass: 0.5, electric: 0.5, normal: 1 }, grass: { fire: 0.5, water: 2, electric: 1, normal: 1 }, electric: { water: 2, grass: 0.5, fire: 1, normal: 1 }, normal: { fire: 1, water: 1, grass: 1, electric: 1, normal: 1 } };

得到倍率的函数:

export function getTypeMultiplier(attackType, defenseType) { const row = TYPE_CHART[attackType]; if (!row) return 1; return row[defenseType] ?? 1; }

这里有一个很容易踩的坑:属性字符串大小写不一致。如果数据里写的是'Fire',克制表里却查'fire',结果一定是undefined,倍率会退化成1。统一用小写字母表达属性,或者在入口处做一次toLowerCase()

3.3 技能数据结构

技能是攻击动作的最小单位。除了名称,还需要知道技能属性、威力、命中率。accuracy的意义是“这次攻击有多少概率命中”,100表示必定命中。命中判定要在伤害计算之前做。

技能示例:

const skill = { name: '火球术', element: 'fire', power: 45, accuracy: 90 };

如果还要做技能 PP 限制,可以增加ppmaxPp字段。当前原型先不做次数限制,避免第一版逻辑分支太多。

4. 战斗引擎:用状态机管理回合流程

4.1 为什么回合制战斗需要状态机

回合制战斗看起来只是“你打我一下、我打你一下”,实际流程里会有玩家选择、技能结算、伤害计算、敌方出手、胜负判定等多个阶段。如果不用状态机,只靠if/else控制,代码很容易在某个分支里漏掉状态更新,导致界面和逻辑不同步。

状态机把战斗过程拆成多个互斥状态,任何时候系统都只处于一个明确状态。当前原型定义四个状态:

export const BattleState = { SELECT_SKILL: 'SELECT_SKILL', PLAYER_ATTACK: 'PLAYER_ATTACK', ENEMY_ATTACK: 'ENEMY_ATTACK', END: 'END' };
  • SELECT_SKILL:等待玩家点击技能按钮。
  • PLAYER_ATTACK:正在执行玩家攻击。
  • ENEMY_ATTACK:正在执行敌方攻击。
  • END:战斗结束。

状态转移规则很简单:玩家选择技能后进入PLAYER_ATTACK,玩家攻击结算完成且双方都活着时进入ENEMY_ATTACK,敌方攻击结算完成后回到SELECT_SKILL,任一方 HP 小于等于 0 时进入END

4.2 战斗类封装

把战斗逻辑封装成一个类,不直接操作 DOM。这样在测试阶段可以直接在 Node 环境或浏览器控制台调用,不依赖页面。

import { getTypeMultiplier } from './data.js'; import { BattleState } from './battleState.js'; export class Battle { constructor(playerCard, enemyCard) { this.playerCard = structuredClone(playerCard); this.enemyCard = structuredClone(enemyCard); this.playerHp = playerCard.hp; this.enemyHp = enemyCard.hp; this.round = 0; this.state = BattleState.SELECT_SKILL; this.logs = []; } playerMove(skillIndex) { if (this.state !== BattleState.SELECT_SKILL) return { ok: false }; if (!this.playerCard.skills[skillIndex]) return { ok: false }; const skill = this.playerCard.skills[skillIndex]; const result = this.executeAttack(this.playerCard, this.enemyCard, skill); this.applyDamageToEnemy(result.damage); this.state = BattleState.PLAYER_ATTACK; if (this.enemyHp <= 0) { this.state = BattleState.END; return { ok: true, result, end: true }; } const enemyResult = this.executeEnemyAttack(); this.state = BattleState.ENEMY_ATTACK; if (this.playerHp <= 0) { this.state = BattleState.END; return { ok: true, result, enemyResult, end: true }; } this.state = BattleState.SELECT_SKILL; this.round++; return { ok: true, result, enemyResult, end: false }; } executeAttack(attacker, defender, skill) { if (!this.hit(skill.accuracy)) { return { miss: true, damage: 0 }; } const damage = this.calcDamage(attacker, defender, skill); return { miss: false, damage }; } hit(accuracy) { return Math.random() * 100 < accuracy; } calcDamage(attacker, defender, skill) { const typeMultiplier = getTypeMultiplier(skill.element, defender.element); const random = 0.85 + Math.random() * 0.15; const base = (skill.power * attacker.attack) / Math.max(1, defender.defense); return Math.max(1, Math.floor(base * typeMultiplier * random)); } executeEnemyAttack() { const randomIndex = Math.floor(Math.random() * this.enemyCard.skills.length); const skill = this.enemyCard.skills[randomIndex]; const result = this.executeAttack(this.enemyCard, this.playerCard, skill); this.playerHp = Math.max(0, this.playerHp - result.damage); return result; } applyDamageToEnemy(damage) { this.enemyHp = Math.max(0, this.enemyHp - damage); } isEnd() { return this.state === BattleState.END; } }

这里使用structuredClone深拷贝卡牌数据,防止战斗过程中修改原始CARDS数组。如果浏览器不支持structuredClone,可以用JSON.parse(JSON.stringify(card))替代,代价是丢失函数和undefined字段,当前数据是纯 JSON,所以没问题。

4.3 敌方 AI 出招逻辑

第一版的敌方 AI 非常简单:从技能列表里随机选一个技能。这样做的好处是代码量少,逻辑清晰。后续升级可以让 AI 优先选择克制玩家属性的技能,也可以根据当前血量决定是否使用防御技能。

随机出招也有一个设计点:不要让敌方在玩家攻击前出手。当前类中,玩家攻击完成后再执行敌方攻击,保证回合顺序稳定。等加入速度属性后,可以改成“速度高的一方先出手”,那时需要对回合顺序做更细的控制。

4.4 胜负判定与回合计数

胜负判定只有两个条件:

  • 玩家 HP 小于等于 0,玩家失败。
  • 敌方 HP 小于等于 0,玩家胜利。

回合计数放在所有攻击动作完成之后,也就是双方都出手一次算一回合。这样日志里能看到“第 1 回合”“第 2 回合”这样的进度,方便测试和调试。

5. 界面渲染:把卡牌和战斗过程画到页面上

5.1 渲染玩家和敌方卡牌

表现层从Battle实例中读取卡牌数据,然后生成 HTML。玩家卡牌区显示己方技能按钮,敌方卡牌区不显示按钮,只显示卡牌和血条。

import { CARDS } from './data.js'; export function renderCard(card, hp, options = {}) { const root = document.createElement('div'); root.className = `card card--${card.element}`; root.innerHTML = ` <div class="card__cover">${card.image ? `<img src="${card.image}" alt="" />` : `<span class="card__name">${card.name}</span>`}</div> <div class="card__info"> <span class="card__element">${card.element}</span> <div class="card__hp"> <div class="hp-bar" style="width: ${(hp / card.hp) * 100}%"></div> <span>${hp} / ${card.hp}</span> </div> </div> ${options.showSkills ? renderSkillButtons(card) : ''} `; return root; } function renderSkillButtons(card) { return ` <div class="card__skills"> ${card.skills.map((skill, index) => ` <button class="skill-btn">export function updateHp(container, currentHp, maxHp) { const bar = container.querySelector('.hp-bar'); const hpText = container.querySelector('.card__hp span'); if (bar) { const percent = Math.max(0, (currentHp / maxHp) * 100); bar.style.width = percent + '%'; if (percent <= 30) bar.classList.add('hp-bar--danger'); } if (hpText) hpText.textContent = `${currentHp} / ${maxHp}`; } export function appendLog(logContainer, message) { const line = document.createElement('div'); line.className = 'log-line'; line.textContent = message; logContainer.appendChild(line); logContainer.scrollTop = logContainer.scrollHeight; }

血条低于 30% 时变成警示色,这是卡牌游戏里很常见的反馈设计。日志滚动到底部,让玩家不需要手动查看最新内容。

5.3 “炫卡斗士W式”特效:抖动、飘字和属性反馈

表现力不只是颜色,还需要动画。这里用 CSS 实现两种核心反馈:卡牌受击抖动和伤害飘字。动画类名由 UI 层在适当时候添加。

.card { border-radius: 12px; background: linear-gradient(135deg, #2b2b3a, #1e1e2a); padding: 16px; min-height: 360px; display: flex; flex-direction: column; gap: 12px; } .card--fire { border: 2px solid #f97316; } .card--water { border: 2px solid #3b82f6; } .card--grass { border: 2px solid #22c55e; } .hp-bar { height: 12px; background: #22c55e; border-radius: 6px; transition: width 0.3s ease; } .hp-bar--danger { background: #ef4444; } @keyframes card-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } } .card--hit { animation: card-shake 0.2s linear; } @keyframes float-up { 0% { opacity: 0; transform: translateY(10px); } 20% { opacity: 1; } 100% { opacity: 0; transform: translateY(-40px); } } .damage-float { position: absolute; top: 40%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 28px; font-weight: 700; text-shadow: 0 0 8px rgba(0, 0, 0, 0.6); animation: float-up 0.8s ease forwards; }

伤害飘字需要动态插入到卡牌容器中,动画结束后移除节点。这样做可以避免大量无效节点堆积在页面里。

export function showDamage(container, amount) { const el = document.createElement('div'); el.className = 'damage-float'; el.textContent = `-${amount}`; container.appendChild(el); el.addEventListener('animationend', () => el.remove()); }

6. 运行验证与常见问题排查

6.1 验证流程

在本地服务器方式下启动后,预期流程如下:

  1. 页面显示敌方卡牌和玩家卡牌。
  2. 玩家卡牌下方出现两个技能按钮。
  3. 点击一个技能,日志区显示“焰尾狐使用火球术”,敌方卡牌抖动。
  4. 敌方血条下降,伤害数字飘出。
  5. 如果敌方未倒下,系统自动执行敌方攻击,玩家卡牌也抖动、掉血。
  6. 任一方 HP 归零,页面出现胜负字样。

如果没有出现上述流程,优先打开浏览器开发者工具检查 Console 面板。

6.2 常见问题排查

这里整理几个常见问题,对应现象、原因和解决方案。

问题现象常见原因检查方式处理建议
打开index.html后页面空白,控制台提示 CORS 或模块加载失败使用import/export语法时直接双击文件,浏览器禁止通过file://加载模块看 Console 是否有Access to script报错使用 Live Server、python -m http.server或把代码整合成单个 HTML 文件
点击技能按钮没有任何反应状态不是SELECT_SKILL,或事件绑定失败playerMove内打印this.state,在按钮 click 回调打印索引确认按钮事件绑定在渲染完成后执行,且playerMove中检查状态
属性克制倍率不生效,所有伤害都一样属性大小写不一致,getTypeMultiplier查不到数据在函数内console.log(attackType, defenseType)统一使用小写属性名,或在入口处做toLowerCase()
敌人被打败后,玩家还能继续操作没有在 UI 层判断BattleState.END检查playerMove返回值中的end战斗结束时禁用所有技能按钮并显示结算界面
动画只播放一次,后面再点击没效果CSS 动画 class 未重置检查元素classList是否一直包含动画 class去掉动画 class 后强制触发重新布局,或监听animationend移除

6.3 单文件整合方式

如果不想使用本地服务器,可以把全部代码合并到一个index.html文件中,用普通<script>标签而不是<script type="module">。对应的做法是:删除所有import/export,把数据、战斗类、UI 函数按顺序放到同一个<script>标签里。这样双击文件就能运行,适合分享和快速验证。

单文件方式虽然方便,但只适合学习原型。工程化项目仍然建议拆成多个模块,配合构建工具处理依赖、压缩和资源加载。

7. 最佳实践与扩展方向

7.1 数据和渲染分离是长期维护的关键

整个项目最值得坚持的设计原则,是Battle类完全不操作 DOM。所有状态变化都通过返回值或更新后的字段暴露给 UI 层。这样做的直接收益是:未来加入多局对战、回放功能、单元测试时,不需要打开浏览器就能验证逻辑。

例如可以写一组简单的 Node 测试:

// test-battle.js import { Battle } from './src/battle.js'; import { CARDS } from './src/data.js'; const battle = new Battle(CARDS[0], CARDS[1]); const result = battle.playerMove(0); console.log(result);

只要能正确输出伤害和剩余血量,战斗逻辑就是可验证的。UI 层出问题时,可以独立排查表现层,不需要担心逻辑被 UI 绑定。

7.2 状态机的可扩展方向

当前状态机只有四个状态,对于原型已经足够。如果后续要加入更丰富的机制,可以按这个方向扩展:

扩展功能需要增加的状态或字段影响
技能 PP 限制skill.ppskill.maxPp选择技能时判断 PP 是否为 0
先手速度比较双方speed不再固定玩家先手,需要调整状态机
中毒、灼伤等状态status字段回合开始或结束时结算状态伤害
天气weather字段伤害计算时根据天气调整倍率
换人上场bench数组增加换人状态和对应界面

每加一个功能,都要评估它是否影响状态机的转移条件。先写状态转移图,再改代码,能减少很多遗漏。

7.3 生产化时候的注意点

如果要把这个原型做成真正的项目,以下事项不能忽略。

  • 卡牌图片不要直接放仓库。使用对象存储或 CDN,数据字段里只保存图片 URL。
  • 伤害公式和属性倍率不能只写在前端。涉及玩家 PvP 时,所有攻击结果必须由后端校验,防止通过抓包篡改伤害。
  • 卡牌数据要增加版本号。属性调整、技能平衡、数值修改会直接影响线上战斗,必须能区分不同数据版本。
  • 使用 TypeScript 定义卡牌和状态类型。当前原型是 JavaScript,数据结构一旦复杂,类型错误会很难排查。
  • 增加日志持久化。战斗日志要写到本地或服务端,方便排查玩家反馈的异常回合。

7.4 下一步可以怎么练

最推荐的方向,是先在当前原型上加一个“火、水、草三张卡互相克制”的完整对战循环,然后逐步增加以下内容:

  • 增加卡牌图鉴,支持选择不同宝可梦。
  • 增加技能 PP 和道具系统。
  • 把单一 HTML 文件迁移到 Vite + Vue 3。
  • setTimeout模拟回合之间的延迟,让表现更接近卡牌游戏节奏。
  • 接入 WebSocket,把单机对战改成双人同屏对战。

练习时可以从“改成任天堂式的 2D 回合制动画”和“加重卡牌抽卡展示”两条路线选一条。前者的重点在动画调度,后者的重点在卡牌数据结构和随机逻辑。两条路线都很有价值,但不要同时铺开,否则容易分散注意力。

最后留一个可复用清单。每次做卡牌战斗相关项目时,先对照检查一遍:

  • 卡牌字段是否满足核心玩法,是否包含不必要的字段。
  • 属性克制的键名是否统一小写。
  • 战斗状态机是否只有一条明确的状态转移路径。
  • 伤害计算是否被独立函数封装。
  • UI 是否只从战斗实例读取数据,不直接修改战斗结果。
  • 是否已经处理了敌方死后继续操作的边界情况。
  • 动画节点是否在结束之后被移除。
  • 如果计划发布,是否已经做出包含版本、日志、异常回退的方案。

这套原型虽然简单,但已经把回合制卡牌对战的骨架立起来了。后续所有复杂功能,都可以在这个骨架上逐步加进去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 8:29:13

SWAN风浪边界条件生成:基于波浪分割与数据分析的二维谱构建方法

简介&#xff1a;本资源是一套面向海洋工程、环境科学及气候研究领域的SWAN波浪模型Python实现方案&#xff0c;聚焦风浪边界条件生成与二维能谱构建&#xff0c;适用于具备Python基础与数据分析经验的科研人员和工程师。资源包共13个文件&#xff0c;含6个Jupyter Notebook&am…

作者头像 李华
网站建设 2026/9/3 8:29:07

状态机菜单项目到CLion+HAL库移植

介绍本文以 STM32 标准库编写的 OLED 菜单模块为例&#xff0c;介绍如何将其移植至 HAL 库&#xff0c;并完成 CLion 开发环境的工程搭建、编译、下载与调试配置。本次移植旨在将原有标准库工程转换为基于HAL库的工程架构&#xff0c;以便后续集成串口、ADC、DMA、SPI、IC 等外…

作者头像 李华
网站建设 2026/9/3 8:29:02

从Billie Eilish专辑看数据驱动下的音乐产品矩阵运营

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 8:28:49

Matlab多视角三维重建实战:从图像采集到网格生成

简介&#xff1a;本资源是一套面向本科及硕士阶段图像处理与计算机视觉教学科研的多视角三维重建实践方案&#xff0c;基于Matlab实现从图像匹配、基础矩阵估计、相机位姿恢复到稀疏点云三角化与光束法平差的完整流程。压缩包共95个文件&#xff0c;含62张多视角标定图像&#…

作者头像 李华
网站建设 2026/9/3 8:28:29

空调选购指南:从能效比到舒适性,揭秘真正省电的三大核心

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 8:27:55

海迅软件层板托孔位调整:从三排到一排的拆单实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华