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

news2024/12/26 9:28:06

编辑语: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的RenderTexture

    if (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 precision
                    this._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_OUTPUT
        color = packRGBE(color.rgb); // for reflection-map
    #else
        if(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_PASS
            color.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 top
        vec3 _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 = true
            return _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://www.coloradmin.cn/o/1472439.html

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

相关文章

嵌入式中逻辑分析仪基本操作方法

前期准备 1.一块能触摸的屏对应的主板机 2.逻辑分析仪对应的软件工具 3.对应的拓展板 4.确定拓展板的引脚分布情况 第一步&#xff1a;逻辑分析仪j基本操作 1.数据捕捉需要先进行对应软件安装,并按照需求进行配置 2.这里以A20为例:此手机使用显示驱动芯片CST148,触摸屏分辨…

SImpAl

output matrix M&#xff0c;Curriculum using w ( x t , f , h ) w(x_t, f, h) w(xt​,f,h) 辅助信息 作者未提供代码

数学建模论文、代码百度网盘链接

1.[2018中国大数据年终总决赛冠军] 金融市场板块划分与轮动规律挖掘与可视化问题 2.[2019第九届MathorCup数模二等奖] 数据驱动的城市轨道交通网络优化策略 3.[2019电工杯一等奖] 露天停车场停车位的优化设计 4.[2019数学中国网络数模一等奖] 基于机器学习的保险业数字化变革…

STM32控制数码管从0显示到99

首先 先画电路图吧&#xff01;打开proteus&#xff0c;导入相关器件&#xff0c;绘制电路图。如下&#xff1a;&#xff08;记得要保存啊&#xff01;发现模拟一遍程序就自动退出了&#xff0c;有bug&#xff0c;我是解决不了&#xff0c;所以就是要及时保存&#xff0c;自己重…

设计模式(七)装饰模式

相关文章设计模式系列 1.装饰模式简介 装饰模式介绍 装饰模式是结构型设计模式之一&#xff0c;不必改变类文件和使用继承的情况下&#xff0c;动态地扩展一个对象的功能&#xff0c;是继承的替代方案之一。它是通过创建一个包装对象&#xff0c;也就是装饰来包裹真实的对象…

使用ClickHouse进行SQL动态列选择

本文字数&#xff1a;4073&#xff1b;估计阅读时间&#xff1a;11 分钟 作者&#xff1a;Mark Needham 审校&#xff1a;庄晓东&#xff08;魏庄&#xff09; 本文在公众号【ClickHouseInc】首发 在处理包含大量列的数据集时&#xff0c;我们通常希望在其中的一部分列上做聚合…

模型评估方式

文章目录 一、有监督-分类模型1、混淆矩阵2、分类模型的精度和召回率3、ROC曲线与AUC 二、有监督-回归模型1、均方误差MSE2、 R 2 R^2 R2决定系数3、回归模型代码示例 三、无监督模型1、kmeans求解最优k值的方法&#xff1a;轮廓系数、肘部法2、GMM的最优组件个数&#xff1a;A…

基于springboot的新闻资讯系统的设计与实现

**&#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;**一 、设计说明 1.1 课题背景…

JavaAPI常用类03

目录 java.lang.Math Math类 代码 运行 Random类 代码 运行 Date类/Calendar类/ SimpleDateFormat类 Date类 代码 运行 Calendar类 代码 运行 SimpleDateFormat类 代码一 运行 常用的转换符 代码二 运行 java.math BigInteger 代码 运行 BigDecimal …

防御保护----内容安全

八.内容安全--------------------------。 IAE引擎&#xff1a; IAE引擎里面的技术&#xff1a;DFI和DPI技术--- 深度检测技术 DPI --- 深度包检测技术--- 主要针对完整的数据包&#xff08;数据包分片&#xff0c;分段需要重组&#xff09;&#xff0c;之后对 数据包的内容进行…

设计模式六:策略模式

1、策略模式 策略模式定义了一系列的算法&#xff0c;并将每一个算法封装起来&#xff0c;使每个算法可以相互替代&#xff0c;使算法本身和使用算法的客户端分割开来&#xff0c;相互独立。 策略模式的角色&#xff1a; 策略接口角色IStrategy&#xff1a;用来约束一系列具体…

Qt QWiget 实现简约美观的加载动画 第三季

&#x1f603; 第三季来啦 &#x1f603; 这是最终效果: 只有三个文件,可以直接编译运行 //main.cpp #include "LoadingAnimWidget.h" #include <QApplication> #include <QVBoxLayout> #include <QGridLayout> int main(int argc, char *argv[]…

架构设计:流式处理与实时计算

引言 随着大数据技术的不断发展&#xff0c;流式处理和实时计算在各行各业中变得越来越重要。那么什么是流式处理呢&#xff1f;我们又该怎么使用它&#xff1f;流式处理允许我们对数据流进行实时分析和处理&#xff0c;而实时计算则使我们能够以低延迟和高吞吐量处理数据。本…

axure9.0 工具使用思考

原型设计软件【AxureRP】快速原型设计工具原型设计软件【AxureRP】快速原型设计工具原型设计软件【AxureRP】快速原型设计工具原型设计软件【AxureRP】快速原型设计工具原型设计软件【AxureRP】快速原型设计工具原型设计软件【AxureRP】快速原型设计工具原型设计软件【AxureRP】…

linux中查找进程cpu使用率高的原因

查询哪些进程/线程cpu使用率高 使用 top 命令&#xff1a; 在终端中运行 top 命令&#xff0c;它会实时显示系统中正在运行的进程和线程&#xff0c;并按照 CPU 使用率进行排序。你可以按 Shift P 键按照 CPU 使用率对进程进行排序&#xff0c;或者按 Shift T 键按照线程进…

nginx基础模块配置详解

目录 一、Nginx相关配置 1、nginx配置文件 2、nginx模块 二、nginx全局配置 1、关闭版本或修改版本 1.1 关闭版本 1.2 修改版本 2、修改nginx启动的子进程数 3、cpu与worker进程绑定 4、PID路径 5、nginx进程的优先级 6、调试worker进程打开文件的个数 7、nginx服…

idea 设置启动类置底/设置folders置顶

在新建项目的时候启动类外和swagger交叉展示在包之间&#xff0c;缺少美观&#xff0c;这在一个有洁癖的程序员眼里是非常不能接受的。在网上大量检索相关的设置&#xff0c;一无所获。但是苍天犹怜&#xff0c;经过我一上午的探索&#xff0c;终于在一个犄角旮旯里面找到了这个…

【可实战】被测系统业务架构、系统架构、技术架构、数据流、业务逻辑分析

一、为什么要学习 更深的理解业务逻辑&#xff08;公司是做什么的&#xff1f;它最重要的商务决策是什么&#xff1f;它里面的数据流是怎么做的&#xff1f;有哪些业务场景&#xff1f;考验你对这家公司、对所负责业务的熟悉程度。公司背后服务器用什么软件搭建的&#xff1f;…

系统找不到xinput1_3.dll怎么办?试试这五种解决方法轻松搞定

在计算机系统运行过程中&#xff0c;当我们遭遇“找不到xinput1_3.dll”这一错误提示时&#xff0c;实际上正面临一个软件兼容性、系统组件缺失以及游戏或应用程序无法正常启动的关键问题。深入探究这一现象&#xff0c;我们会发现它可能引发一系列连带问题&#xff0c;例如某些…

蓝桥杯Learning

Part 1 递归和递推 1. 简单斐波那契数列 # 这里使用了数组进行保存 n int(input())st [0]*(47) # 注意这个地方&#xff0c;需要将数组空间设置的大一些&#xff0c;否则会数组越界 st[1] 0 st[2] 1def dfs(u):if u 1:print(st[1],end" ")if u 2:print(str(st[…