- 彻底搞清微信小游戏开发中的循环的使用
循环是游戏开发中一定会用到的逻辑,不论是你想控制移动,或者进行遍历,亦或者不停的去执行某一段逻辑,都需要使用循环。那么对于循环的使用你彻底了解了吗?今天这篇文章就帮助你彻底的弄懂微信小游戏开发中的循环的用法。 首先我们来看一下微信小游戏制作工具中与循环有关的积木块。 [图片] 前三个为重复执行,最后一个为逻辑循环。在使用之前,我们先弄清楚重复执行和逻辑循环的区别。 先来看一下官方文档的解释。 重复执行:重复执行频率跟游戏的刷新频率一致,约16.67ms一次重复执行(n)次:有限次的循环,执行频率跟游戏的刷新频率一致,约16.67ms一次重复执行直到<条件>:一直重复执行,直到<条件>为真逻辑循环(n)次:有限次的循环,在当前帧全部执行完毕,在逻辑循环中的克隆会立即克隆 看不懂没关系,先接着往下看。 游戏开发中的帧的概念 想要彻底理解这两种循环的区别,首先必须要理解一个概念,叫做“帧”。 [图片] “帧”在游戏开发中是一个常用的概念,想象一下小时候看的胶片电影,放映机上要安放一大盘胶片,最终看到的流畅的电影的画面其实是由胶片中的一个一个的图片构成的。因为人眼会有“视觉停留”,所以只要图片切换的足够快,那看上去就是流畅的画面。 你可以把游戏中的一帧类比成胶片中的一个图片,电影中的一帧只需要处理图像和声音,游戏中的一帧除了图像和声音,还需要处理更多的东西,例如输入操作(是否点击了屏幕,手指是否在屏幕上滑动等),计算操作(游戏中的角色位于哪里?他当前正在攻击吗?还是正在跳跃...)等。 当前的游戏通常都是1秒钟运行60帧,1秒钟等于1000毫秒,那么1帧也就相当于16.67(1000/60)毫秒了。 “帧”的概念理解了,我们再看一下这两种循环。 重复执行:1帧循环一次。假设循环60次,那么总计需要1秒钟的时间执行完。逻辑循环:在1帧之内执行完所有循环。假设循环60次,那么这60次的循环会在一帧之内执行完毕,也就是只需要16.67毫秒就可以执行完。为了更好的理解两者间的区别,我们在开发工具中制作一个小例子。 [图片] 如图,在场景中增加了一个红球,一个篮球,和一个“开始移动”按钮。我们要实现如下的逻辑:点击“开始移动”按钮,两个小球同时开始向上移动,不同的是“红球”使用“重复执行”积木块来处理移动逻辑,而“蓝球”使用“逻辑循环”积木块来处理移动逻辑。 红球的移动逻辑。 [图片] 蓝球的移动逻辑。 [图片] 按钮上增加点击时发送“移动”通知。然后两个小球收到“移动”通知后会同时向上移动。 [图片] 预览场景,看一下结果: [图片] 使用“重复执行”的红球大概需要将近1.67秒(循环100次/每秒60次)的时间移动到目标位置,而使用“逻辑循环”的篮球几乎是瞬间移动到目标位置。 现在再回到上方看一下官方文档中对于两种循环的解释,现在是不是比较好理解了呢? 从上方的例子中可以看出“重复执行”的循环积木适用于游戏中的物体的运动,例如不停移动的飞机,云彩,除了移动当然也可以用于旋转或者缩放,总之如果是与运动相关的循环,我就应该选用“重复执行”积木。 使用循环进行克隆 接下来我们再来看一下循环在克隆中的使用。通常我们需要在游戏中克隆很多的物体,例如不停来进攻的敌人,不停发射的子弹等等,当需要克隆很多的物体时,我们就需要使用循环了。 现在假设我们需要连续的克隆10个小球,并且让这些小球从下往上依次排列。 [图片] 如图这里我们仍然是使用了一个红球和一个蓝球,红球使用“重复执行”进行克隆,篮球使用“逻辑循环”进行克隆。 新建两个全局变量,用于计算新克隆的球的位置。 [图片] 接着看一下红球的逻辑。 [图片] 蓝球与红球的逻辑一样,唯一的区别就是使用“逻辑循环”积木。 [图片] 运行一下,看看最终结果。 [图片] 可以看到点击克隆后,篮球是瞬间克隆了 10 个,而红球是有一个过渡的,结合上面我们所讲的帧的内容,红球是一帧克隆一个的。 在游戏开发中,如果需要在短时间内大量的克隆物体,需要选择正确的循环去进行克隆操作,例如在射击游戏中发射子弹时,子弹应该是有一定的时间间隔的,此时如果你使用了“逻辑循环”,就会在瞬间创建出大量的子弹,很显然这不是我们想要的效果。另外,在循环克隆时,尤其需要注意“逻辑循环”的使用,因为在 1 帧之内克隆大量的物体时,很有可能会导致你的游戏崩溃。 利用“重复执行”积木块的每帧执行一次的特性,可以实现逐个创建物体的效果,看起来非常不错,在精致1010中创建方块时,就是使用了这样的方式。 [图片] 使用双重循环 在游戏开发中我们也会经常遇到需要使用双重循环的时候,例如我们需要遍历一个 5 行 5 列的表格,或者需要创建一个 5 行 5 列的对象等。所谓的双重循环其实就是同时使用两个循环,只不过这两个循环是嵌套的,即循环中的循环。 下面我们就结合着克隆,使用双重循环来实现创建 5 行 5 列的小球。 首先,创建一个场景,放置一个红色的小球和按钮,当点击按钮后,会创建 5x5 的共计25个小球。 [图片] 新建两个全局变量“行”和“列”用于计算新克隆出来的小球的水平和竖直的位置。 [图片] 接着来看一下红色小球的逻辑。 [图片] 运行一下看看最终的结果。 [图片] 使用双重循环最重要的地方在于要捋清自己的思路,是想一行一行的创建还是想一列一列的创建,然后根据具体的需求决定哪层循环在外,哪层循环在内。至于三重循环,即循环中的循环中的循环,在游戏开发中用到的比较少,其实道理都一样,只不过又多了一层循环而已。 在循环的中退出 在使用循环时,我们也经常会遇到需要中途退出的情况,比如说在循环执行到达某个条件时,我们就不需要再继续循环了。 [图片] 我们可以使用这两块积木块来退出当前的循环。 以上方的创建小球为例,比如说当前我们只想要创建三行小球就满足条件了,这时我们要让循环退出,不需要再继续创建剩余的两行了。这时我们就可以通过条件判断,使用退出循环积木块来达到目的。 [图片] 再次运行一下。 [图片] 创建 3 行后退出循环,不会再继续创建了。 “退出逻辑循环”积木与“退出循环”积木的用法一样,只不过使用于逻辑循环中。 带条件的循环 最后,我们再来看一下重复执行中的一个非常好用的积木块“重复执行直到….”,这个积木块可以让这个循环一直执行,直到达成某个条件时循环才停止。 一个简单的小例子,我们让正方形不停的旋转,当点击“停”按钮后,停止旋转。 [图片] 首先创建一个“停下来”的全局变量。 [图片] 看一下正方形的逻辑。 [图片] 当点击按钮时,将“停下来”设置为1。 [图片] 预览一下。 [图片] 红色正方形一直旋转,点击“停”后,停止旋转。 这个带停止条件的循环可以用于很多的场景,例如敌人一直在巡逻,知道你靠近时停止巡逻,改为追击,雨一直下直到太阳出来后停止等等。 最后总结一下,在这篇文章中我们了解了下面这些关于循环内容: 帧的概念重复执行与逻辑循环的区别循环在物体运动中的使用循环在克隆中的使用使用双重循环在循环中途停止循环使用带条件的循环这就是游戏开发中常用的循环的使用方式了,你还能想到哪些循环的使用方式呢?欢迎留言讨论。 文中项目已开源到社区,有需要的朋友可以自行获取。项目地址:https://gamemaker.weixin.qq.com/#/game?game_id=lbNTM2Yjc5ZDctYWFmMi00MWJmLWI5ZTktZTVkNTQwYjdlYWRi 最后,欢迎试玩我的微信小游戏作品“精致1010”,最新版本已上线,增加更多玩法和内容,欢迎前往体验。 [图片] 如果你也对微信小游戏开发感兴趣,欢迎关注我的公众号:小蚂蚁游戏开发,获取更多的游戏开发教程或者与游戏开发有关的原创内容。 [图片]
2021-08-06 - [开盖即食]基于canvas的“刮刮乐”刮奖组件
[图片] 工作中有时候会遇到一些关于“抽奖”的需求,这次以“刮刮乐项目”举例,分享一个实战抽奖功能。 本人对之前网上流传的一些H5刮刮乐JS插件版本进行了一些改造,使其能适用于实际项目,并且支持小程序canvas 2D的新API,这里顺便提下2D API和实际H5 canvas中JS写法非常类似,只有少数不同。 [图片] 1、方法介绍: 1.1 刮刮乐JS组件 [代码]class Scratch { constructor(page, opts) { opts = opts || {}; this.page = page; this.canvasId = opts.canvasId || 'canvas'; this.width = opts.width || 300; this.height = opts.height || 300; this.bgImg = opts.bgImg || ''; //覆盖的图片 this.maskColor = opts.maskColor || '#edce94'; this.size = opts.size || 15, //this.r = this.size * 2; this.r = this.size; this.area = this.r * this.r; this.showPercent = opts.showPercent || 0.2; //刮开多少比例显示全部 this.rpx = wx.getSystemInfoSync().windowWidth / 750; //设备缩放比例 this.scale = opts.scale || 0.5; this.totalArea = this.width * this.height; this.startCallBack = opts.startCallBack || false; //第一次刮时触发刮奖效果 this.overCallBack = opts.overCallBack || false; //刮奖完触发 this.init(); } init() { let self = this; this.show = false; this.clearPoints = []; const query = wx.createSelectorQuery(); //console.log(this.canvasId); query.select(this.canvasId) .fields({ node: true, size: true }) .exec((res) => { //console.log(res); this.canvas = res[0].node; this.ctx = this.canvas.getContext('2d') this.canvas.width = res[0].width; this.canvas.height = res[0].height; //const dpr = wx.getSystemInfoSync().pixelRatio; //this.canvas.width = res[0].width * dpr; //this.canvas.height = res[0].height * dpr; self.drawMask(); self.bindTouch(); }) } async drawMask() { let self = this; if (self.bgImg) { //判断是否是网络图片 let imgObj = self.canvas.createImage(); if (self.bgImg.indexOf("http") > -1) { await wx.getImageInfo({ src: self.bgImg, //服务器返回的图片地址 success: function (res) { imgObj.src = res.path; //res.path是网络图片的本地地址 }, fail: function (res) { //失败回调 console.log(res); } }); } else { imgObj.src = self.bgImg; //res.path是网络图片的本地地址 } imgObj.onload = function (res) { self.ctx.drawImage(imgObj, 0, 0, self.width * self.rpx, self.height * self.rpx); //方法不执行 } imgObj.onerror = function (res) { console.log('onload失败') //实际执行了此方法 } } else { this.ctx.fillStyle = this.maskColor; this.ctx.fillRect(0, 0, self.width * self.rpx, self.height * self.rpx); } //this.ctx.draw(); } bindTouch() { this.page.touchStart = (e) => { this.eraser(e, true); } this.page.touchMove = (e) => { this.eraser(e, false); } this.page.touchEnd = (e) => { if (this.show) { //this.page.clearCanvas(); if (this.overCallBack) this.overCallBack(); this.ctx.clearRect(0, 0, this.width * this.rpx, this.height * this.rpx); //this.ctx.draw(); } } } eraser(e, bool) { let len = this.clearPoints.length; let count = 0; let x = e.touches[0].x, y = e.touches[0].y; let x1 = x - this.size; let y1 = y - this.size; if (bool) { this.clearPoints.push({ x1: x1, y1: y1, x2: x1 + this.r, y2: y1 + this.r }) } for (let item of this.clearPoints) { if (item.x1 > x || item.y1 > y || item.x2 < x || item.y2 < y) { count++; } else { break; } } if (len === count) { this.clearPoints.push({ x1: x1, y1: y1, x2: x1 + this.r, y2: y1 + this.r }); } //添加计算已清除的面积,达到标准值后,设置刮卡区域刮干净 let clearNum = parseFloat(this.r * this.r * len) / parseFloat(this.scale * this.totalArea); if (!this.show) { this.page.setData({ clearNum: parseFloat(this.r * this.r * len) / parseFloat(this.scale * this.totalArea) }) }; if (this.startCallBack) this.startCallBack(); //console.log(clearNum) if (clearNum > this.showPercent) { //if (len && this.r * this.r * len > this.scale * this.totalArea) { this.show = true; } this.clearArcFun(x, y, this.r, this.ctx); } clearArcFun(x, y, r, ctx) { let stepClear = 1; clearArc(x, y, r); function clearArc(x, y, radius) { let calcWidth = radius - stepClear; let calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth); let posX = x - calcWidth; let posY = y - calcHeight; let widthX = 2 * calcWidth; let heightY = 2 * calcHeight; if (stepClear <= radius) { ctx.clearRect(posX, posY, widthX, heightY); stepClear += 1; clearArc(x, y, radius); } } } } export default Scratch [代码] 1.2 JS 调用方法 [代码]new Scratch(self, { canvasId: '#coverCanvas', //对应的canvasId width: 600, height: 300, //maskColor:"", //封面颜色 showPercent: 0.3, //刮开多少比例显示全部,比如0.3为 30%面积 bgImg: "./cover.jpg", //封面图片 overCallBack: () => { //刮奖刮完回调函数 }, startCallBack: () => { //当用户触摸canvas板的时候触发回调 } }) [代码] 实际中还支持其他很多的配置项,比如缩放比例,刮开比例,放置区域等等,大家可以根据实际需求设置。 1.3 实际页面中的JS调用方法: [代码]//引入刮刮乐部分 import Scratch from './scratch.js'; const app = getApp() Page({ data: { firstTouch: 0, isOver: 0, }, onLoad() { let self = this; new Scratch(self, { canvasId: '#coverCanvas', width: 600, height: 300, //maskColor:"", //封面颜色 bgImg: "./cover.jpg", //封面图片 overCallBack: () => { this.setData({ isOver: "结束啦" }) //this.clearCanvas(); }, startCallBack: () => { this.setData({ firstTouch: "开始刮啦" }) //this.postScratchSubmit(); } }) }, //刮卡已刮干净 clearCanvas() { let self = this; console.log("over"); }, }) [代码] 1.4 HTML/CSS [代码]<-- html --> <view class="wrap"> <canvas class="cover_canvas" type="2d" disable-scroll="false" id='coverCanvas' bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd"></canvas> <image class="img" src="reward.jpg" mode="widthFix" /> </view> /* css */ .wrap { width: 600rpx; height: 300rpx; margin: 100rpx auto; border: 1px solid #000; position: relative; } .cover_canvas { width: 600rpx; height: 300rpx; z-index: 9; } .wrap .img { position: absolute; left: 0; top: 0; z-index: 1; width: 600rpx; height: 300rpx; } [代码] 这里注意 type=“2d” 写法,这里使用的是新的2D canvas。 2、注意事项 canvas一些效果不支持真机调试,直接预览就行了 如果刮奖结果是通过第一次触碰canvas触发的,这里的请求需要写一个同步方法 刮刮乐JS的配置会优先判断bgImg这个属性,再判断maskColor 需要反复刮奖,可以反复new 它。 3、代码片段 地址: https://developers.weixin.qq.com/s/RxiaHam574or 建议将IDE工具升级到 1.03.24以上,避免一些BUG [图片] 觉得有用,请点个赞,这是我继续分享的动力~
2021-02-18 - 小游戏版消消乐
概述:最近看了点算法,为了对其有深刻的体会,利用周末时间撸了一个简易版的三消游戏,采用JS+Canvas实现,没有使用额外的游戏引擎,对于初学者来说,也比较容易入门的。下面是小游戏效果展示: 效果展示# [图片]; 这还是一个比较初级的版本,大家有什么想法欢迎点评; 界面设计: 第一步开始游戏界面设计: 思路如下: 先在项目下新建一个js/runtime/background.js,来渲染不同阶段的背景(具体代码见github) 同时创建一个js/runtime/music.js来存放游戏各个阶段所需要的背景音乐(具体代码见github) 同时在背景图片上绘制开始游戏,并且进入游戏区域; 代码如下: step1() { ctx.clearRect(0, 0, canvas.width, canvas.height) //清楚上一局动画 this.bg.render(ctx);// 背景渲染 this.login.render(ctx);//开始游戏 渲染 this.touchHandler = this.touchEventHandler.bind(this) //开始游戏点击事件 canvas.addEventListener('touchstart', this.touchHandler) } touchEventHandler(e) { e.preventDefault() let x = e.touches[0].clientX let y = e.touches[0].clientY let area = this.login.btnArea //开始游戏可点区域 if ( x >= area.startX && x <= area.endX && y >= area.startY && y <= area.endY ) { this.secenceChange() //进入游戏主题区域 } } 效果如下:[图片] 点击开始游戏,进入消除页面,先绘制消除界面 secenceChange() { canvas.removeEventListener('touchstart',this.touchHandler) //移除上一局事件 this.up = this.update.bind(this) this.bgupdate() this.aniId = window.requestAnimationFrame(this.up,canvas) this.music.secen2() } update() { this.enemyGenerate();//随机生成小动物 this.initEvent();//为画布初始化事件 this.gameinfo.renderImage(ctx,this.score.num)//记录分数和关卡 } 效果如下: [图片] 触摸小动物使其发生位移,核心代码如下:(具体见github地址) touchHanderMove(e) { e.preventDefault() let x = e.touches[0].clientX-ceilX let y = e.touches[0].clientY-ceilY this.endCoordinates = [x, y]; //点击 交换区域 if(x>0&&y>0&&x<(ceilX+imgW*numberX)&&y 核心算法 消除算法: 我采用了动态规划的思想,在双重遍历的时候判断相同就给一个match标记,在消除的时候就判断match是否大于0, 大于0就表示该小动物可以消除,然后再用canvas清空方格; 先判断是否有重复元素:使用isReapet函数 [图片] 游戏下落算法: 在我们将相应的方块白色之后,其上面的方块应该下落,在这里我的思想是:下落我采用的是从下到上,从左到右的思想,将重复的小动物跟上面未重复的小动物进行位置交换,按照列遍历二维数组,定义一个变量temp,指向为0 的小动物的位置,一旦遇到晓东为为 0 的格子就将其与temp所指的小动物进行交换,一次类推这样的话我们就可以把为空的上移到最顶层,并且不打乱顺序,然后我们在随机填充顶部的空方块就可以了。做完填充之后我们要再做一次消除算法; [图片] down() { for(let x =numberY-1;x>-1;x--) { for(let y=0 ; y-1;k--) { if(this.boxData[y][k].match==0) { let temp = this.boxData[y][k]; this.boxData[y][k] = this.boxData[y][x] this.boxData[y][x] = temp; break; } } } } this.downgame(); } 点击重玩,重新生成小动物 在交换和小动物下落的时候再加入音乐素材,后续就是绘制动画了;然后一个低配的消消乐就完成了; 然后再申请一个小游戏账号把代码上传后续其他人就可以进行体验了; 游戏性能优化一款好的游戏,离不开性能优化,我从以下几点来为改游戏做了优化; 1:离屏 Canvas 在点击开始游戏,进入玩区后,需要反复的擦除和重绘,为了动画的流畅,我使用离屏 Canvas进行绘制的,像网格背景、背景图、关卡等。,并且使用了 小游戏封装的requestAnimationFrame函数来绘制动画,在小游戏 首次调用创建的是显示在屏幕上的画布,之后调用创建的都是离屏画布。初始化时将静态场景绘制完备,需要时直接拷贝离屏Canvas的图像即可。Canvas 绘制本身就是不断的更新帧从而达到动画的效果,通过使用离屏 Canvas,就大大减少了一些静态内容在上屏Canvas的绘制,从而提升了绘制性能使动画流畅。 2:内存优化 玩家在游戏过程中拖动小动物的移动其实就是不断更新坐标信息,然后不断的清空画布再重新绘制,可以想象,这个绘制是非常频繁的,按照普通的做法就需要不断去创建多个新的对象。针对游戏中需要频繁更新的对象,我使用对象池的方法进行优化,对象池维护一个装着空闲对象的池子,如果需要对象的时候,不是直接new,而是从对象池中取出,如果对象池中没有空闲对象,则新建一个空闲对象,就像官方demo实现的对象池类那样; 3:垃圾回收 小游戏中,JavaScript 中的每一个 Canvas 或 Image 对象都会有一个客户端层的实际纹理储存,实际纹理储存中存放着 Canvas、Image 的真实纹理,通常会占用相当一部分内存。 每个客户端实际纹理储存的回收时机依赖于 JavaScript 中的 Canvas、Image 对象回收。在 JavaScript 的 Canvas、Image 对象被回收之前,客户端对应的实际纹理储存不会被回收。通过调用 wx.triggerGC() 方法,可以加快触发 JavaScriptCore Garbage Collection(垃圾回收),从而触发 JavaScript 中没有引用的 Canvas、Image 回收,释放对应的实际纹理储存。 但 GC 具体触发时机还要取决于 JavaScriptCore 自身机制,并不能保证调用 wx.triggerGC() 能马上触发回收,本游戏在点击游戏开始或重玩都会触发一下,及时回收内存垃圾,以保证拥有良好的游戏体验。 兼容测试: 我让我身边的小伙伴们玩了一下,目前在ios或者安卓上基本上都是ok的 结语: 本文章讲述有点简单,详情见https://github.com/xfbaby/game.git,在游戏的优化上还有写需要改进,后续还会持续为大家分享,希望能对小游戏感兴趣的鞋童们有所帮助; 源码地址:https://github.com/xfbaby/game.git
2021-01-19 - 最佳实践丨云数据库实现联表+聚合查询
聚合是云开发 CloudBase 数据库中非常重要的一种数据批处理操作方式。聚合操作可以将数据分组(或者不分组,即只有一组/每个记录都是一组),然后对每组数据执行多种批处理操作,最后返回结果。 有了聚合能力,可以方便的解决很多没有聚合能力时无法实现或只能低效实现的场景,包括分组查询、只取某些字段的统计值或变换值返回、流水线式分阶段批处理、获取唯一值(去重)等。 本文就以一个简单的实例解释如何在云数据库中,实现十分常用的联表+聚合查询操作。 场景说明假设数据库内存在两个集合:[代码]class[代码] 与 [代码]student[代码],存在以下数据: class(班级信息): [图片] student(学生信息): [图片] 现在需要查询徐老师所带的班级里面所有学生的平均成绩。 代码示例1、lookup 联表查询首先我们需要把 student 内的所有数据,按照 class_id 进行分组,这里我们使用云数据库的 lookup 操作符: lookup({ from: "student", //要关联的表student localField: "id", //class表中的关联字段 foreignField: "class_id", //student表中关联字段 as: "stu" //定义输出数组的别名 }).end(); 这个语句会查出来下面的结果,会查出班级的信息以及该班级所对应的所有学生的信息: {"list": [{ "id":1, "teacher":"王老师", "cname":"一班", "stu":[ { "sname":"宁一", "class_id":1, "score":90 } ] }, { "id":2, "teacher":"徐老师", "cname":"二班", "stu":[ { "class_id":2, "sname":"张二", "score":100 }, { "class_id":2, "sname":"李二", "score":80 } ] }] } 但是我们只需要徐老师所在班级学生的数据,所以需要进一步过滤。 2、match 条件匹配现在就只是返回徐老师所在班级的学生数据了,学生数据在 stu 对应的数组里面: .lookup({ from: 'student', localField: 'id', foreignField: 'class_id', as: 'stu' }) .match({ teacher:"徐老师" }) .end() 现在就只是返回徐老师所在班级的学生数据了,学生数据在 stu 对应的数组里面: { "list": [ { "_id": "5e847ab25eb9428600a512352fa6c7c4", "id": 2, "teacher": "徐老师", "cname": "二班", //学生数据 "stu": [ { "_id": "37e26adb5eb945a70084351e57f6d717", "class_id": 2, "sname": "张二", "score": 100 }, { "_id": "5e847ab25eb945cf00a5884204297ed8", "class_id": 2, "sname": "李二", "score": 80 } ] } ] } 接下来我们继续优化代码,直接返回学生的平均分数。 3、直接返回学生成绩平均值如果想要在被连接的表格中(本课程中的 student)做聚合操作,就用 pipeline 方法: .lookup({ from: 'student', pipeline: $.pipeline() .group({ _id: null, score: $.avg('$score') }) .done(), as: 'stu' }) .match({ teacher:"徐老师" }) .end() 现在输出的数据是这样的: { "list": [ { "_id": "5e847ab25eb9428600a512352fa6c7c4", "id": 2, "teacher": "徐老师", "cname": "二班", "stu": [{ "_id": null, "score": 90 }] } ] } 但是现在输出的数据有点复杂,如果只想显示 teacher 和 score 这两个值,我们再进行下面的操作。 4. 只显示 teacher 和 score 这两个值我们使用 replaceRoot、mergeObjects 和 project 进行最后的处理: .lookup({ from: 'student', pipeline: $.pipeline() .group({ _id: null, score: $.avg('$score') }) .done(), as: 'stu' }) .match({ teacher:"徐老师" }) .replaceRoot({ newRoot: $.mergeObjects([$.arrayElemAt(['$stu', 0]), '$$ROOT']) }) .project({ _id:0, teacher:1, score:1 }) .end() 现在输出的数据是这样的: { "list": [{ "score": 90, "teacher": "徐老师" }] } 相关文档:云开发聚合搜索:https://docs.cloudbase.net/database/aggregate.html 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2021-04-08