news 2026/10/18 19:41:15

Canvas游戏开发实战:用JS实现大鱼吃小鱼与碰撞检测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Canvas游戏开发实战:用JS实现大鱼吃小鱼与碰撞检测

1. 内容整体设计与思路拆解

1.1 这个项目为什么值得做

“大鱼吃小鱼”在游戏产品里算是家喻户晓的休闲玩法,看起来简单,但它其实把游戏开发中最核心的“循环”完整地串起来了:玩家输入、帧循环、碰撞检测、状态更新、画面渲染、游戏结束判定。一套走下来,你对“一个游戏是怎么跑起来的”会有非常直观的认识,而不是停留在背API的层面。

我之前带过不少刚入门的开发者,发现一个现象:很多人能写静态页面、能做组件交互,但一提到“游戏开发”就发怵,觉得要学物理引擎、要学复杂的渲染管线。其实完全不必,像这种“大鱼吃小鱼”的小游戏,用非常朴素的数学就能实现,可能比很多人想象中简单得多。它不需要额外装任何库,浏览器打开直接就能跑,拿来练手再好不过。

这个项目的核心价值有三块:第一,让你理解游戏循环(动画帧驱动)到底是怎么回事;第二,让你掌握碰撞检测这种高频交互的基础写法;第三,让你体会数值设计的重要性——为什么游戏好玩,很多时候不是画面多华丽,而是数值反馈给得准、给得舒服。

1.2 玩法规则里的隐藏设计

把玩法拆开看,规则无非这么几条:

  • 玩家控制一条鱼,在海洋场景中游动。
  • 场景中分布着大小不一的鱼,比玩家小的可以被吃掉,比玩家大的会反过来吃掉玩家。
  • 每次吃鱼后玩家体型变大,速度相应变化。
  • 一旦碰到比自己大的鱼,游戏结束。

但往细了想,这里面的每一个“简单规则”都对应一个设计决策。比如“什么算比我小”?直接比较半径看似合理,但如果两条鱼半径一模一样,互相都判定不了,就会出现“贴脸僵持”的尴尬局面。所以我一般会把判定条件做成当前鱼半径 > 目标鱼半径 * 1.1,留出10%的容差,游戏体验立刻就顺滑了。

再比如“吃鱼后怎么长大”?如果直接让半径累加,那吃掉一条中等的鱼体型就会瞬间暴涨,视觉上非常跳跃。更合理的做法是基于圆形面积相加:两个圆合并后,新的半径取sqrt(r1² + r2²),这样增长是连续的、渐变的,既符合直觉又没有爆点。这个过程其实就是很朴素的“合并面积等价”思想,理解起来也不难。

1.3 技术方案选择:为什么用 Canvas 而不是 DOM 或 SVG

这是我每次都会跟读者强调的一个点:选技术方案,不要凭“哪个熟悉”来选,要凭“哪个合适”来选。

这种实时反馈的休闲游戏,场景里同时可能有几十条鱼在移动、碰撞、翻转朝向,如果用 DOM 节点来做,每个帧都要高频操作几十个节点的样式属性,浏览器重排和重绘的压力会非常大。SVG 稍微好一些,但元素多起来后性能和内存表现也不理想。Canvas 的优势在于,它本质是“一帧一画”,每帧直接操作像素缓冲区,所有渲染集中在一次绘制里完成,对这类场景完全是量身定做。

另外,Canvas 对碰撞检测也很友好。因为鱼本身用圆形近似,我只需要维护一堆“圆心坐标 + 半径”的数据,碰撞判断退化为两个圆的距离计算。如果用 DOM,反而还得考虑元素边界、旋转角度、缩放比例这些额外信息,平白无故增加复杂度。

注意:用 Canvas 做游戏,思维模式要从“操作元素”切换成“绘制画面”。一个常见的误区是试图去“获取某个画出来的鱼”,其实画布上没有“鱼”这个东西,鱼只是数据,画布每帧重新把数据画出来而已。

2. 核心细节解析与实操要点

2.1 鱼的大小体系设计

先从数值说起,因为所有交互都建立在体型对比上。我采用的初始参数是这样的:

对象半径范围(像素)速度范围(像素/帧@60fps)说明
玩家初始20约2.5~4随体型变大而变慢
小鱼8~152~3玩家一开始就能吃
中型鱼16~301.5~2.5前期碰到会死
大鱼31~501~2和中型鱼一起构成威胁

这个表看似随意,实际每一行都有意图。速度设定上有个反直觉的点:越大的鱼反而应该越慢。这既符合自然直觉(大个头的敏捷度低),也给玩家留了操作空间——见到大鱼时可以靠鼠标绕开,而不是绝对必死。如果大鱼跑得和玩家一样快,几个大块头聚在一起时根本没法玩。

玩家的速度衰减我也用了公式控制,核心是让“变大”这件事有代价,而不是无脑叠数值。大致逻辑是:速度上限随半径增加线性下降,但保底一个最小值,保证任何时候玩家都能移动。具体的衰减系数我建议多试,不同手感预期差别很大,偏休闲就把衰减放慢,偏挑战就加大惩罚。

2.2 移动控制:鼠标跟随与朝向计算

玩家控制我用的是鼠标跟随,而不是键盘方向键。为什么?

理由很简单:键盘的另一个缺点是操作视角和动作意图之间隔了一层,“方向”不够自然。鼠标跟随模式下,玩家手指的移动和鱼的运动完全同步,反馈链路最短,适合休闲游戏“上手即玩”的调性。

实现上,我在鼠标事件里记录一个目标坐标,然后在每帧更新时让鱼朝目标坐标移动。鱼的位置不等于鼠标位置,而是一步步“追过去”,这样动效有惯性,看起来像鱼在游动而不是瞬间瞬移。

鱼游动时应该脸朝移动方向,这里用atan2(dy, dx)算出朝向角,绘制时统一按这个角度旋转。没做朝向处理的鱼会“横着游”,非常出戏。

2.3 碰撞检测原理与常见坑

碰撞检测的核心就是计算两个圆的圆心距离,然后和半径之和进行比较:

dist = sqrt((x1 - x2)^2 + (y1 - y2)^2) 如果 dist < r1 + r2,说明发生了碰撞

这个判断本身很简单,但实际项目里有一堆细节坑。

第一个坑是平方根计算的性能。每帧要对几十条鱼做两两碰撞检测,每次都算Math.sqrt其实没必要。效率更高的做法是先比较距离的平方和半径之和的平方,只在最终判定时才解算根号。不过对于这种几十条鱼的小场景,性能差异几乎看不出来,但养成好习惯不是坏事。

第二个坑是相邻帧的抖动。两条鱼如果同时互相检测“我能不能吃你”,会出现一帧吃掉对方、下一帧又被对方吃掉,结果两条鱼同时消失。我的策略是逐一判断:先取当前帧的玩家,遍历所有其他鱼,只由玩家单方面发起“吃”的动作,其他鱼之间不互相吃。这样逻辑简单、无歧义。

第三个坑是“吃到”和“碰撞”的区分。碰撞是双方半径之和的几何接触,但能否吃掉是大小比的判定。中间要有个缓冲带:接触但不满足吃鱼条件时,不给任何惩罚,让玩家推开——这样小体型的玩家贴到比自己大一点点鱼边上,不至于莫名其妙暴毙。

2.4 鱼群的生成与回收机制

场景中的鱼不能一次性全部生成,那样每局体验差别太大。我采用了动态生成策略:每秒钟补充1到2条鱼,当场上总数量低于一个阈值(比如30条)时持续补充。生成位置要在可视范围内随机,但必须避开玩家周围一定距离,否则玩家刚开局就被大鱼骑脸,挫败感拉满。

被吃掉的鱼直接从数组中删掉。但如果一局游戏玩得久,反复创建销毁对象,会产生大量垃圾回收压力。更优雅的方案是对象池:初始化时预创建一个对象池,被吃掉的鱼标记为“死亡”,重置一下数据后放回池里,下次生成时直接复用。这个技巧在这个小项目里属于“优化溢出”,但值得了解,因为任何帧循环游戏,减少GC都是提高流畅度的重要思路。

3. 实操过程与核心环节实现

3.1 初始化页面与 Canvas 适配

页面骨架我用了最朴素的写法:一个铺满全屏的 Canvas,监听鼠标移动。需要注意高分屏适配的问题,否则画出来的画面在 Retina 屏上会模糊。适配方案是读取设备的devicePixelRatio,把 Canvas 的内部尺寸乘以这个比例,再用 CSS 控制显示尺寸。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>大鱼吃小鱼</title> <style> body { margin: 0; overflow: hidden; background: #04142a; } canvas { display: block; background: linear-gradient(180deg, #0a2f5a 0%, #061e3a 100%); } </style> </head> <body> <canvas id="gameCanvas"></canvas> <script src="game.js"></script> </body> </html>

在game.js里先做尺寸适配和鼠标状态的初始化。鼠标坐标要存成“画布坐标系”,而不是“页面坐标系”,这个转换很容易漏。如果用clientX - canvas.getBoundingClientRect().left拿到的是CSS像素坐标,Canvas内部实际尺寸可能已经乘以了DPR,所以我的做法是初始化时记录一个scale = canvas.width / canvas.clientWidth,用它把CSS像素坐标还原成画布坐标。

3.2 鱼的类设计与渲染

鱼用一个极简的类来表示,字段只有位置、半径、速度方向、颜色、存活状态。不需要额外继承,不需要多态,够用就行。

class Fish { constructor(x, y, r, vx, vy, color) { this.x = x; this.y = y; this.r = r; this.vx = vx; this.vy = vy; this.color = color; this.isPlayer = false; // 标记是否为玩家本体 } }

绘制逻辑是重点,鱼要画得“像个鱼”,而不是一个圆。我的做法是:沿着朝向角度旋转坐标系,然后画出身体椭圆、三角尾巴、眼睛三个部分。

身体是中心对称的椭圆,半径决定长轴和短轴。尾巴画成身体尾部的一个三角形,颜色用比身体深一档的色值。眼睛是一个小黑点,放在靠近头部方向的位置。鱼嘴的方向和移动方向一致,所以绘制时在身体朝向的前端加一个小半圆缺口,视觉上就形成“嘴”的错觉。

function drawFish(ctx, fish, angle) { ctx.save(); ctx.translate(fish.x, fish.y); ctx.rotate(angle); // 身体 ctx.beginPath(); ctx.ellipse(0, 0, fish.r * 1.2, fish.r * 0.8, 0, 0, Math.PI * 2); ctx.fillStyle = fish.color; ctx.fill(); // 尾巴 ctx.beginPath(); ctx.moveTo(-fish.r * 0.8, -fish.r * 0.4); ctx.lineTo(-fish.r * 1.6, 0); ctx.lineTo(-fish.r * 0.8, fish.r * 0.4); ctx.closePath(); ctx.fillStyle = darkenColor(fish.color); ctx.fill(); // 眼睛 ctx.beginPath(); ctx.arc(fish.r * 0.5, -fish.r * 0.3, fish.r * 0.16, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.beginPath(); ctx.arc(fish.r * 0.6, -fish.r * 0.3, fish.r * 0.08, 0, Math.PI * 2); ctx.fillStyle = '#000000'; ctx.fill(); ctx.restore(); }

颜色深一档的实现可以用ctx.fillStyle配HSL值的调整,也可以简单地把RGB各分量乘以一个系数,在小项目里没必要引库。

3.3 主循环与更新逻辑

主循环用requestAnimationFrame驱动,这是标准做法,比setInterval更平滑,且天然跟浏览器刷新率同步。我把更新和绘制分开,物理计算全部在update里完成,draw只负责根据当前状态绘制画面。

let fishes = []; let player = new Fish(innerWidth / 2, innerHeight / 2, 20, 0, 0, '#ffd700'); player.isPlayer = true; let score = 0; let gameOver = false; let state = 'playing'; // 'playing' | 'over' function spawnFish() { if (fishes.length < 30) { const r = 8 + Math.random() * 42; const side = Math.floor(Math.random() * 4); let x, y; // 从屏幕四边生成,避免出现在玩家正前方 switch (side) { case 0: x = -r; y = Math.random() * innerHeight; break; case 1: x = innerWidth + r; y = Math.random() * innerHeight; break; case 2: x = Math.random() * innerWidth; y = -r; break; case 3: x = Math.random() * innerWidth; y = innerHeight + r; break; } // 直接生成在外侧,让鱼游入视野 ... } }

玩家移动的细节是“平滑跟随”,不是直接跳到鼠标位置。我用一个lerp思想:每帧按比例向鼠标目标靠近,比例取一个与帧率无关的经验值。如果直接用固定像素偏移,刷新率不同的设备手感差别巨大。

为了方便,我这里直接用速度方案:计算当前鱼朝向目标的单位向量,乘以最大速度,再乘一个阻尼系数。这样即使鼠标不动,鱼也有最小的游动惯性,看起来更自然。

吃鱼判定放在更新循环的最后,遍历所有其他鱼,命中碰撞且满足大小条件时,把对方标记为死亡,同时更新玩家半径、分数。

成长公式在代码里非常直观:

const newR = Math.sqrt(player.r * player.r + target.r * target.r); player.r = Math.min(newR, 120); // 设置一个最大半径上限

上限很重要,否则数值溢出后鱼的尺寸会大到屏幕都装不下,而到了那个阶段,游戏其实已经没有挑战性了。

3.4 计分与游戏结束状态

计分我用了最简单直接的方案:按吃掉的鱼半径累计分数。半径越大加分越多,这样鼓励玩家挑战中等偏大的鱼,而不是只挑最小最容易的吃。我用的公式是score += Math.floor(target.r * target.r / 10),本质上还是按面积折算。

游戏结束状态要做成独立的state分支。一旦玩家碰到比自己大的鱼,立即把state置为over,主循环里的碰撞检测逻辑不再执行。绘制时遮罩一层半透明的深色背景,居中显示“游戏结束”和最终得分,再画一个“重新开始”的提示区域。点击后重置所有数据,重新初始化玩家位置和鱼群。

3.5 边界处理与细节打磨

鱼碰到边界时,我采用的是“环绕”策略:游出左侧边缘,就从右侧进入;游出顶部,就从底部继续。这样做的好处是鱼群永远存在,不会堆在边界出不去,玩家不需要掉头找鱼。但要注意,环绕的瞬间不能把玩家坐标清掉,否则分数和状态容易乱。

我当时实现时踩过一个小坑:环绕只对普通鱼生效,但玩家也继承了同一个类的字段,导致鼠标跟随目标在跨边界后出现跳变。后来加了isPlayer标记,玩家的移动完全靠鼠标坐标驱动,不做边界环绕,只在绘制时判断一下位置是否超出画布,超出就把鱼“夹”回边界内(另一种柔和处理)。

4. 常见问题与排查技巧实录

4.1 一帧吃掉多只鱼导致体型暴涨

现象:玩家游过鱼群密集区域时,体型瞬间变大,视觉上很突兀,甚至分数跳变。

原因:遍历碰撞时,连续判断了多只鱼,且所有吃鱼动作都加成到同一帧。

解决:每次玩家吃鱼后,限制一轮更新中的连吃次数为1~2次,或者把吃鱼动作放到碰撞收集结束后的统一结算里。还有一种方案是加入一个短暂的“消化”间隔,比如50毫秒内最多吃一条鱼。从游戏性角度,这个缓冲让玩家能清楚看到自己到底吃的是哪条,而不是“糊里糊涂变大了”,体验反而更好。

4.2 两条体型相近的鱼互相卡住

现象:玩家在边界或角落遇到一条略大的鱼,无法前进也无法后退,一直在贴脸抖动。

原因:碰撞检测和朝向计算相互冲突——检测到了碰撞,但大小判定不满足被吃条件,而鼠标朝向的另一侧又被其他障碍堵住。

解决:碰撞后加入一个“推开”的力。当不满足被吃条件时,把玩家和对方沿圆心连线方向各自推开一段距离。加上这段位移后,玩家可以绕过威胁,而不是被钉在原地。

我实际在代码里加了这样一个块:

if (dist < player.r + fish.r) { if (player.r > fish.r * 1.1) { // 吃掉鱼 } else if (fish.r > player.r * 1.1) { // 游戏结束 } else { // 推开 const angle = Math.atan2(fish.y - player.y, fish.x - player.x); player.x -= Math.cos(angle) * 3; player.y -= Math.sin(angle) * 3; fish.x += Math.cos(angle) * 3; fish.y += Math.sin(angle) * 3; } }

这个细节非常影响手感,加了之后,同体型僵持基本消失了。

4.3 帧率下降导致鱼群“漂移”

现象:在性能较弱的设备上,鱼的运动速度看起来不稳定,一会儿快一会儿慢。

原因:主循环基于requestAnimationFrame,它按浏览器刷新率回调。在60Hz屏幕上和120Hz屏幕上,每秒执行帧数不同,而运动增量是固定的像素/帧,所以高刷屏上鱼明显更快。

解决:引入deltaTime,把单位从“像素/帧”改成“像素/秒”。每帧获取当前时间戳和上一帧的差值,位移量乘以dt系数。这样无论屏幕刷新率如何,鱼的绝对速度都一致。

这在移动端尤其重要,因为手机和高刷显示器的刷新率差异非常大,不做时间修正,游戏在不同设备上体验完全不同。

4.4 鱼的数量无限堆积内存暴涨

现象:玩到后期,如果玩家一直不吃小鱼,场上鱼的数量不断累积,导致内存和绘制开销持续上升。

原因:生成逻辑只看“场上鱼数量是否少于阈值”,但阈值判断是在生成触发点,如果触发生成后鱼因为玩家不主动去吃、且相互之间不碰撞,它们就一直存在。

解决:除了上限数量,我再增加一个“生存时间”机制:每条鱼生成时记录时间戳,超过20秒后如果还存活,就逐步淡化并移除。这样即使玩家挂机,场景也会自动清理旧鱼、引入新鱼,保持动态平衡。

这个机制我后来发现其实也是一种难度调节器:旧鱼消失、新鱼重新刷新,玩家面临的鱼群分布不会固化,刷新的随机性保证了每一局都有新鲜感。

4.5 首次点击页面后鱼不响应

现象:游戏加载出来了,画布也正常显示,但鼠标移动没有效果,鱼在原处僵住。

原因:多半是事件监听没有正确挂载,或者监听的元素不对。我自己真的犯过挂在document上、又使用了canvas.addEventListener来取坐标的错,两者坐标基准不同。

解决:把mousemove事件统一挂在window上,坐标统一走event.clientX/clientY,再用画布边界做偏移修正。触摸端可以用touchmove事件做类似处理,坐标用touches[0].pageX/pageY。

4.6 分数显示位置和大小字号要适配不同屏幕

这个不算BUG,属于细节打磨。分数如果直接用CSS像素写死,在手机屏幕上会显得过大。简单方案是用canvas.width / 20之类的比例动态计算字体大小,或者把分数绘制在一个固定比例的位置和字号上,再乘以DPR缩放。我当时图省事直接固定字号,后面发现在不同分辨率下效果差别很大,还是花了几分钟做了动态适配。

5. 进阶玩法与扩展方向

5.1 给鱼加简单AI

当你把基础版做完后,最值得做的扩展是给普通鱼加简单的“逃跑和追击”AI。实现思路也不复杂:每条鱼每帧检查附近是否出现比自己大或比自己小的鱼,各自设定一个“威胁半径”,朝相反方向或靠近方向调整运动向量。这个改动立刻就把“死水一潭”的场景盘活了,鱼群会自主聚散,游戏可玩性提升一大截。

AI加完之后,数值平衡会变得微妙起来:如果AI鱼太聪明,玩家永远追不上小鱼;如果太迟钝,一开局鱼群就全自动送死。我的建议是先设定“玩家无法直接观察到的AI反应延迟”,给AI一个200到400毫秒的决策间隔,这样模拟出鱼群的“应激迟钝感”,不至于让游戏变得太硬核。

5.2 道具与事件机制

经典的加强道具无非三种:加速、护盾、缩小药剂。加速让玩家短时间内追上大鱼,护盾让玩家撞到大鱼时不死,缩小药剂把体型往回调,用来应对体型过大后速度过慢的尴尬。

道具在场景中随机刷新,做成发光的圆形能量球,玩家吃到之后进入一段持续时间的Buff状态,绘制时给玩家外圈加一圈辉光,直观可见。这个扩展的工程量和基础版差不多,但因为引入了“临时状态”的概念,你会学到状态管理和计时器的配合,比一直只做大小判定有趣得多。

5.3 存档与排行榜

用localStorage保存最高分、游戏场次、达到的最大体型,对新手来说是一个完整的“持久化数据”练手案例。如果想进一步,可以把最高分按日期排序,画一个简单的历史分数曲线,这就是一个迷你数据可视化了。排行榜的引入有一个好处:游戏的重复可玩性立刻上升,玩家为了刷分数会对数值反馈更加敏感。

5.4 从 2D 到 3D 的想象空间

做完2D版本,如果你有兴趣,可以试试用 Three.js 把场景换成 3D 水下视角。虽然改动很大,但你之前积累的“碰撞检测思想”“移动控制思路”仍然适用,只是把圆和半径换成了球和三维向量。这种“小步跨越”的过渡方式,比直接啃一个大型3D游戏项目要舒服得多。

6. 实操中的经验与心得

做这个小游戏的过程里,我个人收获最大的不是某个API或某个公式,而是“如何把一个模糊的玩法描述拆成可执行的代码模块”。很多人一拿到“大鱼吃小鱼”这种题目,脑子里立刻蹦出的是画面、动画、音效这些表象,反而忽略了底层的数据结构和逻辑分层。但代码跑起来之后你会发现,真正支撑画面的永远是那几个简单的数学关系和清晰的状态流转。

在实际操作中,我做事的顺序是这样的:先把规则用最朴素的文字写清楚,列成表格;然后把数据模型确定下来——玩家是什么、鱼是什么、它们在每一帧如何更新;最后才写绘制代码,让数据可视化。这个顺序反过来做,往往会把代码写成一团乱麻,因为画面驱动逻辑很容易让状态变得不可控。

还有一点我想多强调一次:数值调优需要反复试,没有一次能调到位。我每一次改速度和成长系数,都要开一局游戏实际跑几十秒,观察手感是否“顺滑”。这个“顺滑”的感觉很难量化,但你玩过之后一定能判断。这也是做游戏和做普通程序的本质区别之一,游戏的体验好坏,不只看功能正不正确,更要看感受对不对。

如果你是自己练手,建议从一个小改动开始:试试把成长公式的系数改一下,从sqrt(r1² + r2²)改成sqrt(r1² + r2²) * 0.8,去吃几条鱼看体型变化。你会立刻明白,哪怕公式只差一个系数,整个游戏的节奏都会天差地别。这种直观体验,比我在这里写一千字文字描述都更有用。

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

GMP大数库嵌入式静态链接与中文文档实战指南

简介&#xff1a;本资源是面向C/C开发者与密码学、数学计算领域学习者的GMP大数运算库实用入门套件&#xff0c;专为解决中文用户因缺乏本地化文档和便捷集成方式而难以快速上手的问题。压缩包共4个文件&#xff08;2个静态库lib、1个PDF中文文档、1个头文件h&#xff09;&…

作者头像 李华
网站建设 2026/10/18 19:37:14

夸克网盘免费扩容1TB实操指南:从空间清理到任务领取全流程

1. 免费扩容1TB的底层逻辑&#xff0c;先搞清楚再决定要不要花时间我一开始并不是很相信“免费扩容1TB”这种标题&#xff0c;毕竟天上掉馅饼的事&#xff0c;通常都埋着套路。可是真到自己操作了一遍之后&#xff0c;我发现夸克网盘这波扩容活动走的是官方任务奖励路线&#x…

作者头像 李华
网站建设 2026/10/18 19:36:31

diagram-design:从草图到系统,一套可复用的图表设计方法论

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题第一次看到 diagram-design 这个词&#xff0c;很多人会以为它只是“画图”的另一种说法。但如果你真正在技术团队、产品团队或者咨询项目里待过&#xff0c;就会明白&#xff1a;画图本身从来不是难点&a…

作者头像 李华
网站建设 2026/10/18 19:33:13

多智能体强化学习与动态定价:重塑用户侧需求响应新范式

上周有位做电网侧优化的朋友问我说&#xff1a;现在用户侧的空调、充电桩、储能这么多&#xff0c;能不能靠一个价格全部协调起来&#xff1f;我说能&#xff0c;但那个价格不能是拍脑袋定的&#xff0c;它得跟着用户行为一起长出来。这件事其实已经有一条比较清晰的技术路线&a…

作者头像 李华