游戏 CP 专访| InOutPath 技术干货分享!

编辑语:STEAM 上的 3D 解密游戏《InOutPath》以其清新的画面,独特的玩法,受到了广大 STEAM 玩家,以及 Cocos 开发者们的关注。今天有幸邀请到了这款游戏的开发商,为大家做一次技术分享。希望能够对在用 Cocos Creator 开发 3D 游戏的朋友们,有所启发。

在《InOutPath》的关卡中,除了画面渲染效果外,我们还设计了许多细节和彩蛋,相信大家在玩的时候就能体会到这款游戏带来的惊喜和挑战。

363adfd8243a72812a2859ffe58b5993.png

今天,我们就来聊聊《InOutPath》的制作细节,看看我们团队是如何利用 Cocos 游戏引擎,实现这个游戏的画风和独特的关卡机制的,以及在打包上架 Steam 的过程中,获得的一些实用经验。

df770da9b77aab0d7b6518621d693847.png

团队介绍

《InOutPath》的研发是一支小型独立游戏团队,团队成员都是身经百战的游戏老兵和游戏研发老兵,源于团队对益智解谜游戏类型的喜爱和在纪念碑谷/linelight中吸取的灵感,由此制作了《InOutPath》这款冒险解谜游戏。

项目简介

《InOutPath》是一款冒险解谜游戏,一共 7 个大章节 300+ 小章节。包含了包括初晨草原、忘竹林、黄金乐园、奥秘云谷、分界地、蔚蓝边境、水下世界等 7 种完全不同风格的场景,也包含了 20 多种迥异的解谜机制。玩家将扮演一只可爱的小猫咪寻找和主人的记忆。

cebcce7ef755b428c20caf947737535e.png

不同于点击解密的解密场景完全处于静止状态,在InOutPath中由于猫咪和解密要素机关都处于动态中,这导致整个场景的状态一直在变化,进而除了考验玩家的逻辑推理能力也同时考验玩家的操作能力。在设计关卡的规则当中,遵循优先设计一条看起来是正确但是实际却是错误的原则来设计关卡。

技术分享

编辑器插件

由于关卡的复杂性和策划多变的需求,开发团队基于 Cocos 编辑器插件 API 开发了一套制作关卡功能的辅助插件,使整个游戏都架构在曲线数据上。

其次是在地图数据和渲染上进行了分离,这可以很方便地调整关卡。由于制作关卡的便利性,我们一共搭建了不下于 300 关的关卡。

接下来,我们说说是如何实现的。

首先数据部分,如果屏蔽掉渲染物体,只保留逻辑物体,场景是这样的:

fae3a236af9d95d83c8b0274fcfd8bc2.png

如图所见,场景非常简单,但是却包含了最主要的逻辑数据,这是整个游戏驱动的关键所见。当把渲染物体显示出来的时候,场景是这样的:

62cd52d3e35a7fd6405a6382b0073d88.png

当然这里要特别感谢 2youyou2 提供的思路。可以利用 gizmos.ControllerBase 创建一个控制器,然后调用gizmos.ControllerUtils.cube 创建可以在世界空间中被选择方块选择器。最后在使用 gizmos.ControllerBase.initHandle 将他们关联起来就能实现图中的效果,这样策划就可以随意编辑关卡了,程序只需要在这些曲线上实现各种功能即可。

let cube = window.cce.gizmos.ControllerUtils.cube(SPLINE_NODE_SIZE,SPLINE_NODE_SIZE,SPLINE_NODE_SIZE,Color.YELLOW);cube.parent = this.shape;this.positionNode = cube;this.initAxis(cube, SplineMoveType.Position);cube = window.cce.gizmos.ControllerUtils.cube(SPLINE_NODE_SIZE,SPLINE_NODE_SIZE,SPLINE_NODE_SIZE,Color.GREEN);cube.parent = this.shape;this.directionNode = cube;this.initAxis(cube, SplineMoveType.Direction);cube = window.cce.gizmos.ControllerUtils.cube(SPLINE_NODE_SIZE,SPLINE_NODE_SIZE,SPLINE_NODE_SIZE,Color.RED);cube.parent = this.shape;this.invDirectionNode = cube;this.initAxis(cube, SplineMoveType.InvDirection);initAxis(node: Node, axisName: string ) {return window.cce.gizmos.ControllerBase.prototype.initHandle.call(this,node,axisName);}

提示系统

首先开发的是关卡提示系统。本身解密游戏的提示功能要做到"Show,don't tell",但要做一个"Show,don't tell"实在是太难了。

而由于游戏架构设计就是奔着多人合作架构去,游戏天然的支持帧同步,帧数据(数据中主要操作数据)播放出录像。所以我们做了一个录像系统,录制了策划的指令,当玩家打开提示系统的时候,就会实时播放这个指令。还原策划当时的场景。结构如下:

export const enum OperateEventType {DEFALUT = 'defalut',UP = 'up',DOWN = 'down',LEFT = 'left',RIGHT = 'right',FRONT = 'front',BACK = 'back'... more value}export interface LevelFrame {/**帧数 */id: number;/**添加事件操作 */adds?: Array<OperateEventType | string>;/**移除事件操作 */deletes?: Array<OperateEventType | string>;/**帧dt时间 */dt?: number;}最后录制的数据差不多如此:'lv1-1': {'1': { id: 1, dt: 0.016 },'31': { id: 31, adds: ['right'] },'101': { id: 101, adds: ['back'] },'102': { id: 102, deletes: ['right'] },'182': { id: 182, adds: ['right'] },'184': { id: 184, deletes: ['back'] }},'lv1-2': {'1': { id: 1, dt: 0.016 },'160': { id: 160, adds: ['right'] },'231': { id: 231, adds: ['back'] },'233': { id: 233, deletes: ['right'] },'280': { id: 280, adds: ['right'] },'281': { id: 281, deletes: ['back'] }},

碰撞

在游戏中有比肩火箭速度一样的鸟,早期偶然会有物体穿透现象发生,而且由于逻辑表现分离,使用的是时间和速度位移,这种情况下无法使用物理系统的 ccd 功能。于是模拟一个类似 CCD 的检测机制,最大限度针对性对场景中的角色做碰撞检测。

于是我们在探照灯/雪球和玩家碰撞检测,从上一帧obb 到当前帧 obb 逐渐插值 obbWithOBB 碰撞检测,然后普通行动角色碰撞检测,从上一帧到当前帧位置线段的射线检测,然后由于角色都是跑在曲线上,因为贝塞尔曲线是非匀速的,角色逻辑 dt 不能当作曲线上行动时间。游戏中将曲线时间和行动时间分成两个时间。

// 激光/子弹const lasers = player.getAllLaser();const tempOBB = new geometry.OBB();for (let i = 0, len = lasers.length; i < len; i++) {const laser = lasers[i];for (let k = 0, kLen = characters.length; k < kLen; k++) {const character = characters[k];const cobb = character.getOBB();for (let m = 0; m < 10; m++) {const lobb = laser.lerpOBB(m / 9, tempOBB);const isCP = geometry.intersect.obbWithOBB(cobb, lobb);if (isCP) {const bCollider = character.node.getComponent(Collider);laser.onColliderEnter({ otherCollider: bCollider } as any);}}}}

操作适配

游戏目前支持键盘/手柄等多种操作模式。这就需要开发一个较为方便的游戏控制器系统。于是我们将键盘和手柄按键转换到游戏操作事件,这样在处理逻辑的地方监听操作事件即可。

渲染技术分析

渲染部分得益于 Cocos 引擎本身在渲染上就已经足够强大,所以我们做的事情并不是很多,这里列几个简单的应用场景。

深度图获取

游戏里面很多处理都依赖一张深度图,特别是后处理阶段,由于游戏采用的是前向渲染管线(Forward Rendering Pipeline),所以为了不增加不必要的深度图渲染代价,我们采取的方案是直接在渲染半透明物体之前拷贝出深度图进行使用。这需要修改 Cocos目前的内置前向渲染管线。先在 ForwardStage 阶段初始化的时候创建一张目标深度纹理。

if (!this._depthTexture) {this._depthTexture = device.createTexture(new gfx.TextureInfo(gfx.TextureType.TEX2D,gfx.TextureUsageBit.DEPTH_STENCIL_ATTACHMENT | gfx.TextureUsageBit.SAMPLED,gfx.Format.DEPTH_STENCIL,this._renderArea.width * pipeline.shadingScale,this._renderArea.height * pipeline.shadingScale));}

然后获取当前的深度缓冲区数据后,调用 blitTexture 将深度缓冲器的深度数据拷贝到我们自己创建的深度图中。

const bufferCopy = new gfx.TextureBlit();bufferCopy.srcOffset.x = 0;bufferCopy.srcOffset.y = 0;bufferCopy.srcExtent.width = depth.width;bufferCopy.srcExtent.height = depth.height;bufferCopy.dstOffset.x = 0;bufferCopy.dstOffset.y = 0;bufferCopy.dstExtent.width = depth.width;bufferCopy.dstExtent.height = depth.height;cmdBuff.blitTexture(depth, this._depthTexture!, [bufferCopy], gfx.Filter.POINT);

这样就可以很方便的在渲染半透明物体和后处理渲染阶段进行深度图依赖了。而且也不需要增加一次 PreDepthPass 的损耗。

全屏抓屏Pass

全屏抓屏Pass主要拿来制作毛玻璃和水下物体波动效果。

这里讲一下水下效果的制作。由于光的折射/反射问题会导致人在水面上看水面下的物体的时候,会发现物体断开和波动。

9c6b34cdcd98e586a5906c1b0b672fc4.png

由于渲染一个模型要波动,一般都会想到采用顶点扰动的方式,但由于我们水面下物体实在太多,这种方式不可取。所以我们采用了对全屏抓屏Pass进行扰动的方式。效果如图水面下的波动和断开。

b938231ded7fce4e92839dab0d4a63e5.gif

老版本由于渲染水面是半透明物体,所以我们只需要在渲染不透明物体之后,在渲染半透明物体之前拷贝当前颜色缓冲器即可。首先在任何需要使用到 GrabPass 的半透明物体里面任意定义

#pragma define-meta USE_GRAB_PASS

然后老办法在 ForwardStage 阶段对 define进 行判断,如果渲染 pass 有 USE_GRAB_PASS 的定义,则需要进行一次 GrabPass。

if (pass.defines['USE_ALPHA_TEST'] &&renderCutoutQueues.phases(pass.phase)) {if (isTransparent && pass.defines['USE_GRAB_PASS']) {hasGrabPass = true;}renderCutoutQueues.insertRenderPassNoCheck(ro, m, p);} else if (!isTransparent && renderOpaqueQueues.phases(pass.phase)) {renderOpaqueQueues.insertRenderPassNoCheck(ro, m, p);} else if (isTransparent && renderTransparentQueues.phases(pass.phase)) {if (isTransparent && pass.defines['USE_GRAB_PASS']) {hasGrabPass = true;}renderTransparentQueues.insertRenderPassNoCheck(ro, m, p);}

最后调用 blitFramebuffer 复制颜色缓冲区到自己创建的归属于 GrabPass 的 RenderTexture 即可。

1,创建GrabPass的RenderTextureif (this.hasGrabPass) {const colorAttachment = new gfx.ColorAttachment();colorAttachment.format = gfx.Format.RGBA8;colorAttachment.loadOp = gfx.LoadOp.CLEAR;colorAttachment.storeOp = gfx.StoreOp.STORE;const rt = new RenderTexture();rt.reset({width: this._width * sceneData.shadingScale,height: this._height * sceneData.shadingScale,passInfo: new gfx.RenderPassInfo([colorAttachment],new gfx.DepthStencilAttachment(gfx.Format.DEPTH_STENCIL))});data.grabOutputRenderTarget = rt;data.grabOutputRenderTargets.push(device.createTexture(new gfx.TextureInfo(gfx.TextureType.TEX2D,gfx.TextureUsageBit.COLOR_ATTACHMENT | gfx.TextureUsageBit.SAMPLED,gfx.Format.RGBA16F, // normals need more precisionthis._width * sceneData.shadingScale,this._height * sceneData.shadingScale)));}//拷贝颜色缓冲区if (hasGrabPass) {cmdBuff.blitFramebuffer(framebuffer,renderData.grabOutputRenderTarget.window!.framebuffer,this._renderArea,this._renderArea,gfx.Filter.POINT);}
这里调用了blitFramebuffer,但是cocos pipeline上并没有暴露出这个API,我们只需要自定义一下。因为在webgl2-commands中cocos已经实现好了。
export function WebGL2CmdFuncBlitFramebuffer (device: WebGL2Device,src: IWebGL2GPUFramebuffer,dst: IWebGL2GPUFramebuffer,srcRect: Readonly<Rect>,dstRect: Readonly<Rect>,filter: Filter,): void {...}

到这里我们已经准备好了当前渲染阶段的颜色区,那么只需要在渲染水面的时候,最后加到 albedo 上即可。

vec4 refractColorRefr = texture(grabTexture, v_screenPos.xy / v_screenPos.z + _RefractOffset * v_offsetPos.xy);vec3 refractColorRefl = SRGBToLinear(fragTextureLod(_Sky, rotationDir, 0.0).rgb);refractColorRefr.xyz = lerp(refractColorRefl.xyz, refractColorRefr.xyz, refractColorRefr.a);

这样我们就实现了水面下的渲染效果。

实时反射

由于 Cocos 在实时反射渲染阶段需要重新编译 shader 变体,这导致实时反射渲染一旦使用,整个帧率直接就奔着 4FPS 去。

因为每时每刻编译大量 shader 变体实在是太耗时了。所以我们在渲染反射探针的队列中禁用了编译 shader 变体的代码,转而采用全局参数传递的方式输出不同阶段的颜色需求。

在render-reflection-probe-queue中禁用// if (!bUseReflectPass) {//     this._patches = [];//     this._patches = this._patches.concat(subModel.patches!);//     const useRGBEPatchs: IMacroPatch[] = [//         { name: CC_USE_RGBE_OUTPUT, value: true },//     ];//     this._patches = this._patches.concat(useRGBEPatchs);//     subModel.onMacroPatchesStateChanged(this._patches);//     this._rgbeSubModelsArray.push(subModel);// }

在 forward 阶段的 chunk 文件中修改最后的输出为:

// Color output#if CC_USE_RGBE_OUTPUTcolor = packRGBE(color.rgb); // for reflection-map#elseif(cc_probeInfo.y == 1.0){color = packRGBE(color.rgb); // for reflection-map 这里的判断是我们加的,让cc_probeInfo的y分值变成阶段判断常量}else{color = CCSurfacesDebugDisplayInvalidNumber(color);#if !CC_USE_FLOAT_OUTPUT || CC_IS_TRANSPARENCY_PASScolor.rgb = HDRToLDR(color.rgb);color.rgb = LinearToSRGB(color.rgb);#endif    }#endif

最后重新修改渲染反射探针的流程为:

export class SRPReflectionProbeFlow extends ReflectionProbeFlow {public render(camera: renderer.scene.Camera): void {const pipeline = this.pipeline as ISRPRenderPipeline;pipeline.beforeReflectionProbe();super.render(camera);pipeline.afterReflectionProbe();}}public beforeReflectionProbe(): void {const globalDSManager = this.globalDSManager;const ds = this.descriptorSet;const cmdBuffer = this.commandBuffers;const globalUBO = this.pipelineUBO['_globalUBO'];globalUBO[UBOGlobal.PROBE_INFO_OFFSET + 1] = 1.0;cmdBuffer[0].updateBuffer(ds.getBuffer(UBOGlobal.BINDING), globalUBO);globalDSManager.bindBuffer(UBOGlobal.BINDING, ds.getBuffer(UBOGlobal.BINDING));globalDSManager.update();}public afterReflectionProbe(): void {const globalDSManager = this.globalDSManager;const ds = this.descriptorSet;const cmdBuffer = this.commandBuffers;const globalUBO = this.pipelineUBO['_globalUBO'];globalUBO[UBOGlobal.PROBE_INFO_OFFSET + 1] = 0.0;cmdBuffer[0].updateBuffer(ds.getBuffer(UBOGlobal.BINDING), globalUBO);globalDSManager.bindBuffer(UBOGlobal.BINDING, ds.getBuffer(UBOGlobal.BINDING));globalDSManager.update();}

经过修改后,渲染水面实时反射就可以稳定 60FPS了。当然这对游戏来说是够用了,如果要渲染大场景,还是需要 SSR/SSPR 才行。

下雨的地面涟漪

游戏中,第二章的场景是竹林雨季的风格,借用于 Cocos 强大的粒子系统我们就随手一做下雨效果就 OK了。

与下雨效果配合的,是雨滴滴落到地面的涟漪效果,这里采用的是 shader 实现。

a446deea03b81044f5aadd9b7ecf6911.gif
#if WEATHER_RAIN//ripple topvec3 _emissive = vec3(1.0) - vec3(fract(cc_time.x * rainSpeed));vec3 _emissive2 = vec3(1.0) - vec3(fract((cc_time.x *rainSpeed)+ 0.5));vec3 _mask = vec3(texture(rainRipple, v_texCoord2 * v_scale / rainRippleScala).r);vec3 _mask2 = vec3(texture(rainRipple,v_texCoord2 * v_scale / rainRippleScala + vec2(0.5,0.5)).r);float _maskColor = saturate(1.0 - distance(_mask.r - _emissive.r,0.05)/0.05) * _mask.r;float _maskColor2 = saturate(1.0 - distance(_mask2.r - _emissive2.r,0.05)/0.05) * _mask2.r;vec3 finalColor = vec3(_maskColor + _maskColor2);s.albedo.rgb = finalColor + s.albedo.rgb;#endif
a01c138ea29d3f3f0155094fe84dbfbd.png

后处理

在《InOutPath》中,为了渲染出不同场景新颖的画风,我们实现了许多后处理效果。

我们并未使用自定义管线,而是基于 Cocos 的内置管线开发了一套后处理架构。由于之前已经做过分享,这里就不再阐述,大家可以去论坛查看。

后处理的确可以非常低成本地增加游戏画面的美术表现,建议朋友们都多研究研究。

这里只说一下水下的全屏焦散。焦散处理采用的是白嫖来的全屏后处理:https://www.shadertoy.com/view/mdtyRr

效果如下: 

d8f2df37fa20b961f31b9784fabd0974.png

所有物体都处于焦散状态,效果很好,处理很高效。其他后处理没什么好讲的,大家都懂。这里就贴个图说明一下当前游戏已经使用的后处理效果有哪些。

71dc0d0e8e040ebfedd95af4c814ba33.png

风格切换

为了实现游戏中多个风格迥异的关卡氛围,我们实现了 9 个不同的场景,这些场景只是作为氛围配置表。里面包含了主方向光、雾效、环境光、阴影等各类参数。

游戏会在加载不同的风格的时候,使用对应的场景数据同步到当前场景。

this.syncAmbient(curr, next);this.syncSkybox(curr, next);if (style.fog.enabled) {this.syncFogInfo(curr!.scene.globals.fog, style.fog);} else {this.syncFog(curr, next);}this.syncShadow(curr, next);this.syncMainLight(curr, next);this.syncPostProcess(curr, style);//参考代码:private syncSkybox(curr: Scene, next: SceneAsset): void {let currSkybox = curr!.scene.globals.skybox;let nextSkybox = next!.scene?.globals.skybox;if (!nextSkybox || !currSkybox) return;currSkybox.enabled = nextSkybox.enabled;currSkybox.envLightingType = nextSkybox.envLightingType;currSkybox.rotationAngle = nextSkybox.rotationAngle;if (nextSkybox.skyboxMaterial) {let mat = new Material();mat.copy(nextSkybox.skyboxMaterial);currSkybox.skyboxMaterial = mat;}currSkybox.useHDR = nextSkybox.useHDR;currSkybox.envmap = nextSkybox.envmap;if (nextSkybox.reflectionMap) {currSkybox.reflectionMap = nextSkybox.reflectionMap;}}

场景文件除开main,其他场景都是一个空盒子当做配置用。

b9c194733ea4aea35c2888a5a6018eb7.png

STEAM打包及加密

打包

STEAM 打包的时候选择 Cocos 的 web-mobile 打包。打包后再配合 steamwork.js+electron 接入 STEAM SDK 即可。

加密

需要知道的是 steamwork.js 是包装的 rust 实现,打包后的二进制文件是 node 后缀,而 node 后缀文件是可以作为 electron 启动文件的。

所以先修改 package.json 里面的 main 入口为打包后的启动文件 plugin-win.node。

"main": "./plugin-win.node",

这里的问题是这个 plugin 是怎么来的呢?其实就是使用 rust 开发的 napi 插件,这里说一下重点。

先禁用掉监听调试入口。

let process: JsObject = global.get_named_property("process").unwrap();let argv: JsObject = process.get_named_property("argv").unwrap();let leng = argv.get_array_length().unwrap();for x in 0..leng {let arg: JsString = argv.get_element::<JsString>(x).unwrap();if arg.into_utf8()?.as_str()?.contains("--inspect")|| arg.into_utf8()?.as_str()?.contains("--remote-debugging-port"){return Err(Error::new(napi::Status::InvalidArg,"Not allow debugging this program.".to_string(),));}}

然后在 napi 插件中使用 rust 劫持下载脚本的函数。

let _ = module_prototype.define_properties(&[Property::new("_compile")?.with_method(encrypt::module_prototype_compile)]);let _ = s.get_named_property::<JsObject>("__proto__").unwrap().define_properties(&[Property::new("createScript")?.with_method(encrypt::systemjs_create_scripts)]);let _ = env.get_global().unwrap().define_properties(&[Property::new("downloadScript")?.with_method(encrypt::ccjs_download_scripts)]);

劫持这个函数后,使用 rust 重新实现。在里面进行解密并返回源码即可。

let content = decrypt(ctx.env, content.into_utf8().unwrap().as_str().unwrap());content

到这里解密就已经完成了,那么加密在什么时候进行呢?在打包 electron 应用的时候进行。

const iv = crypto.randomBytes(16);let append = false;const cipher = crypto.createCipheriv('aes-256-cbc',key,iv);cipher.setAutoPadding(true);cipher.setEncoding('base64');const _p = cipher.push;cipher.push = function (chunk, enc) {if (!append && chunk != null) {append = truereturn _p.call(this, Buffer.concat([iv, chunk]), enc);} else {return _p.call(this, chunk, enc);};};return cipher;

采用 electron-packager 进行打包,然后对 asar 包进行自定义处理。

asar: {unpack: "*.{node,dylib,dll,lib}",transform(filename) {... pipe}}

这里需要注意的是打包的时候有个关键参数需要添加,这会让电脑自动使用最佳显卡。

process.env['SHIM_MCCOMPAT'] = '0x800000001'

为什么使用Cocos

最后我们采访下开发者为什么使用 Cocos Creator 开发《InOutPath》

熟悉

你没看错就是 2 个字熟悉,对于 Cocos 的源码我们团队已经滚瓜烂熟,没道理不用。

优秀

你没看错,Cocos 目前阶段对于我们的项目需求来说,就是两个字优秀。

开源

如果你从头到尾看到这里来的话,你就知道我们基于 Cocos 改造了那些地方。

如果不开源,我想改代码那是不可能的,对于我们小团队且可以改的动代码的团队来讲,开源就是一切选择的基石。

易用性

得益于 Cocos 超现代化的架构,可以很方便的让我们随便蹂躏完成项目需求。

跨平台

可以打包任意主流平台就可以让我们的游戏增加更多的曝光,拥有更多盈利机会。

社区

Cocos 社区应该是目前国产游戏引擎中最强的交流社区,没有之一。

相比一些一潭死水的社区氛围,我更喜欢 Cocos 社区活跃的学习与交流气氛,可以捡到很多珍宝知识和前辈们的经验,只能说非常 nice。

3c0c213b254a108a34f6689c2d0ad521.png

游戏截图

最后在放几张图,都是实机录制。

677eac14d2f3e34f6179cb29eb956925.png 20336b4e2052b8953083786a1a6fcd0c.png baca4943f839738c8c42e7d0e69fda2b.png 1c458446f533bd328edb349a3ce8643d.png 64e1f70b4b616424db5d3b87af1e2988.png 405195487f4157dc61f019ccde5ce303.png

想要亲身体验《InOutPath》的朋友,点击【阅读原文】即可进入游戏主页。

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

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

相关文章

【数据结构与算法】(14)基础算法 之AVL 树相关示例 详细代码讲解

目录 3.4 红黑树概述历史红黑树特性 实现插入情况删除情况完整代码小结 3.4 红黑树 概述 历史 红黑树是一种自平衡二叉查找树&#xff0c;最早由一位名叫Rudolf Bayer的德国计算机科学家于1972年发明。然而&#xff0c;最初的树形结构不是现在的红黑树&#xff0c;而是一种称…

关于js [GDOUCTF 2023]hate eat snake

查看页面源代码 发现snake.js文件 打开js文件 第7行定义了游戏的速度this.speed this.oldSpeed speed || 10 ; 全文搜索speed&#xff0c;在第237行发现自增代码this.speed; 注释或者删除自增代码 回到游戏页面 重玩游戏&#xff0c;等待60s即可 得到flag

Swift Combine 使用 handleEvents 操作符调试管道 从入门到精通二十五

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

【微服务】mybatis typehandler使用详解

目录 一、前言 二、TypeHandler简介 2.1 什么是TypeHandler 2.1.1 TypeHandler特点 2.2 TypeHandler原理 2.3 mybatis自带的TypeHandler 三、环境准备 3.1 准备一张数据表 3.2 搭建一个springboot工程 3.2.1 基础依赖如下 3.2.2 核心配置文件 3.2.3 测试接口 四、T…

d3dcompiler_47.dll是什么,电脑出现d3dcompiler_47.dll丢失如何解决

当打开软件时提示“d3dcompiler_47.dll丢失”时&#xff0c;用户通常会看到类似于以下的错误消息&#xff1a; “无法启动此程序&#xff0c;因为计算机中丢失了d3dcompiler_47.dll。尝试重新安装该程序以解决此问题。” “找不到d3dcompiler_47.dll文件&#xff0c;因此应用…

破译一致性难题:Raft日志复制技术及成员变更问题详解

一、日志复制 Raft 算法是一种用于实现分布式系统中一致性状态机复制的共识算法。在 Raft 中&#xff0c;日志复制是保证集群数据一致性的关键机制。每个节点&#xff08;服务器&#xff09;都维护着一个日志&#xff0c;其中包含一系列的日志条目&#xff08;Log Entry&#x…

在 where子句中使用子查询(二)

目录 ANY ANY &#xff1a;功能上与 IN 是没有任何区别的 >ANY &#xff1a;比子查询返回的最小值要大 ALL >AL &#xff1a;比子查询返回的最大值要大 EXISTS() 判断 NOT EXISTS Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209…

Open3D 点云法向量计算与可视化 (25)

Open3D 点云法向量计算与可视化 (25) 一、算法原理二、算法实现三、可视化显示和长度调节一、算法原理 通常计算点云的法向量可以使用以下两种常见的方法: 最小二乘法(Least Squares Method):该方法通过拟合局部表面的平面来计算法向量。对于给定点周围的邻域,可以通过…

Peter算法小课堂—动态规划

Peter来啦&#xff0c;好久没有更新了呢 今天&#xff0c;我们来讨论讨论提高组的动态规划。 动态规划 动态规划有好多经典的题&#xff0c;有什么背包问题、正整数拆分、杨辉三角……但是&#xff0c;如果考到陌生的题&#xff0c;怎么办呢&#xff1f;比如说2000年提高组的…

apache 模式、优化、功能 与 nginx优化、应用

一、I/O模型——Input/Output模型 1.同步/异步 A程序需要调用B程序的某一个功能&#xff0c;A发送一个请求需要B完成一个任务 同步&#xff1a;B不会主动去通知A是否完成需要A自己去问 异步&#xff1a;B会主动通知A是否完成 2.阻塞/非阻塞 A发送一个请求需要B完成一个任务 …

勇宝趣学JavaScript ES6第三章(字符串的拓展)

已经写到系列教程的第三章了&#xff0c;本章节我们一起来探讨字符串的那些事。在我们的日常工作中&#xff0c;经常会用到模板字符串&#xff0c;还有一些字符串的方法&#xff0c;我们今天就来好好的品一品。 谢谢大家的点赞和收藏。 文章目录 一、字符串的方法1.1 charAt和c…

消息队列-RabbitMQ:延迟队列、rabbitmq 插件方式实现延迟队列、整合SpringBoot

十六、延迟队列 1、延迟队列概念 延时队列内部是有序的&#xff0c;最重要的特性就体现在它的延时属性上&#xff0c;延时队列中的元素是希望在指定时间到了以后或之前取出和处理&#xff0c;简单来说&#xff0c;延时队列就是用来存放需要在指定时间被处理的元素的队列。 延…

软考45-上午题-【数据库】-数据操纵语言DML

一、INSERT插入语句 向SQL的基本表中插入数据有两种方式&#xff1a; ①直接插入元组值 ②插入一个查询的结果值 1-1、直接插入元组值 【注意】&#xff1a; 列名序列是可选的&#xff0c;若是所有列都要插入数值&#xff0c;则可以不写列名序列。 示例&#xff1a; 1-2、插…

暑期宅家?计算机专业必看的8部电影!一定要安利给你们!

代码编程看上去枯燥乏味&#xff0c;但也是艺术的&#xff0c;感性的&#xff0c;计算机编程的许多概念被应用于电影中&#xff0c;其中有些非常之酷炫&#xff0c;它们甚至能帮助开发人员理解一些编程概念。 所以今天学姐来给大家推荐几部心中top级的编程人必看电影&#xff0…

matlab倒立摆小车LQR控制动画

1、内容简介 略 54-可以交流、咨询、答疑 2、内容说明 略 摆杆长度为 L&#xff0c;质量为 m 的单级倒立摆(摆杆的质心在杆的中心处)&#xff0c;小车的质量为 M。在水平方向施加控制力 u&#xff0c;相对参考系产生位移为 y。为了简化问题并且保其实质不变&#xff0c;忽…

数据结构:链表的冒泡排序

法一&#xff1a;修改指针指向 //法二 void maopao_link(link_p H){if(HNULL){printf("头节点为空\n");return;}if(link_empty(H)){printf("链表为空\n");return;}link_p tailNULL;while(H->next->next!tail){link_p pH;link_p qH->next;while(q…

抖音视频提取软件使用功能|抖音视频下载工具

我们的抖音视频提取软件是一款功能强大、易于操作的工具&#xff0c;旨在解决用户在获取抖音视频时需要逐个复制链接、下载的繁琐问题。我们的软件支持通过关键词搜索和分享链接两种方式获取抖音视频&#xff0c;方便用户快速找到自己感兴趣的内容。 主要功能模块&#xff1a;…

进程线程信号通道

4> 使用消息队列完成两个进程间相互通信 usr1代码&#xff1a; #include <myhead.h> //定义一个消息类型 struct msgbuf {long mtype;//消息类型char mtext[1024];//消息正文 }; #define MSGSIZE sizeof(struct msgbuf)-sizeof(long) int main(int argc, const char …

物奇ENC算法开关接口修改方法

物奇ENC算法开关接口修改 是否需要申请加入数字音频系统研究开发交流答疑群(课题组)&#xff1f;可加我微信hezkz17, 本群提供音频技术答疑服务&#xff0c;群赠送语音信号处理降噪算法&#xff0c;蓝牙耳机音频&#xff0c;DSP音频项目核心开发资料, 1 配置工具事件接口 2 代…

K线实战分析系列之十一:行情力量不足——平头形态

K线实战分析系列之十一&#xff1a;行情力量不足——平头形态 一、平头形态二、不同形态与平头形态的叠加三、总结平头形态 一、平头形态 前一根K线具有较长的实体&#xff0c;后一根K线的实体比较小&#xff0c;无论是多头还是空头的力量到第二根K线都被瓦解了多头上攻&#…