Kaplay

0x01 概述

(1)简介

  • 官网地址:https://kaplayjs.com/
  • Kaplay 是一个 JavaScript(TypeScript)游戏库,便于在 Web 制作游戏
  • 特点:
    • API 友好,易于教学
    • 提供一系列示例和资源来快速上手
    • 包含对 TypeScript 的大力支持
    • 永久免费、开源
  • 基于 Canvas 实现
    • 坐标系:坐标原点在左上角,\(x\) 轴正方向向右,\(y\) 轴正方向向下

(2)环境搭建

  • 官方提供了官方脚手架、CDN 和 Node 自定义环境三种搭建方法,以下采用基于 Vite 构建的自定义项目环境
  • 以下使用 Kaplay v3001.0.19
  1. 使用命令 npm create vite 创建项目,名称随意

    1. 选择任意框架或原生 JavaScript 环境,以下以 Vue 框架为例
    2. 为便于学习和调试,不使用 TypeScript
  2. 进入项目目录,使用命令 npm i ; npm i kaplay 安装必要依赖和 Kaplay 库

  3. 编辑 App.vue

    <script setup>
    import kaplay from "kaplay";
    
    kaplay();
    </script>
    
    <template></template>
    
    <style scoped></style>
    
    
  4. 使用命令 npm run dev 运行项目

  5. 在浏览器访问 http://localhost:5173/ 查看

(3)基本概念

  • 游戏初始化:kaplay() 是一切的开始

    <script setup>
    import kaplay from "kaplay";
    import { onMounted, ref } from "vue";
    
    /** @type {import('vue').Ref<HTMLElement | null>} */
    const container = ref(null);
    
    onMounted(() => {
      if (!container.value) return;
    
      // 游戏初始化
      const k = kaplay({
        width: container.value.clientWidth, // 场景宽度
        height: container.value.clientHeight, // 场景高度
        background: "#00deff", // 背景颜色
        scale: 1, // 缩放倍率
        canvas: container.value, // 使用指定画布
        global: false, // 关闭全局注册 Kaplay
      });
    });
    </script>
    
    <template>
      <canvas ref="container" class="fixed top-0 left-0 w-screen h-screen block" />
    </template>
    
  • 游戏对象:Kaplay 的基本单元,通过 add() 创建,其参数是包含多个组件或标签的数组

    // 创建游戏对象
    const cube = k.add([
      k.rect(50, 50), // 50x50 矩形
      k.pos(k.center()), // 放在中央
      "cube", // 标签 cube
    ]);
    console.log(cube.is("player")); // false
    console.log(cube.is("cube")); // true
    
  • 组件:构建游戏对象的基石,定义了游戏对象的行为

  • 事件:便于开发者与用户对游戏进行交互控制

    // 键盘单击事件
    k.onKeyPress("right", () => {
      cube.move(3000, 0);
    });
    
    // 每帧执行一次
    k.onUpdate(() => {
      console.log("更新了一帧");
    });
    
    k.wait(3, () => k.add([k.rect(100, 100), "rect"])); // 等待三秒后添加矩形
    // 指定标签对象被添加是时执行
    k.onAdd("rect", () => {
      console.log("添加了一个矩形");
    });
    
  • 场景:用于包裹游戏对象,通过 scene() 创建创建、go() 切换场景

    // 创建场景
    k.scene("custom_scene", () => k.add([k.rect(100, 100), "rect"]));
    // 按 C 键切换场景
    k.onKeyPress("c", () => {
      k.go("custom_scene");
    });
    

0x02 第一个游戏

(1)基础示例

基于官网示例

  1. 移除默认代码,在 App.vue 中保留关键代码

    <script setup></script>
    
    <template></template>
    
    <style scoped></style>
    
    
  2. 在 src 目录下创建 games 目录,其中新建 index.js 作为第一个游戏

    import kaplay from "kaplay";
    
    export const game = () => {
      const k = kaplay(); // 实例化
      k.loadBean(); // 载入角色贴图,默认提供“豆”贴图
      k.setGravity(1600); // 设置重力
    
      // 创建角色
      const player = k.add([
        sprite("bean"), // 设置贴图
        pos(center()), // 设置位置在中心
        area(), // 设置碰撞盒
        body(), // 设置物理体
      ]);
    
      // 创建场景
      k.add([
        k.rect(width(), 48), // 设置矩形并设置宽度为 48
        k.outline(4), // 设置描边
        k.area(), // 设置碰撞盒
        k.pos(0, height() - 48), // 设置位置
        k.body({ isStatic: true }), // 设置静态物理体,不受重力影响
      ]);
    
      // 创建交互事件
      k.onKeyPress("space", () => {
        // 当角色在地面上时
        if (player.isGrounded())
          player.jump(); // 角色跳起
      });
    };
    
    
  3. 在 App.vue 中导入游戏

    <script setup>
    import { game } from "./games";
    
    game();
    </script>
    
    <template></template>
    
    <style scoped></style>
    
    
  4. 刷新页面,按测试游戏

Kaplay 支持通过按 F1 键进行调试

(2)完善机制

修改 games/index.js,添加障碍物、补充积分机制、引入游戏结束

import kaplay from "kaplay";

export const game = () => {
  const k = kaplay({
    width: 800, // 场景宽度
    height: 600, // 场景高度
  });

  k.loadBean();
  k.setGravity(1600);

  const player = k.add([
    k.sprite("bean"),
    k.pos(k.center()),
    k.area(),
    k.body(),
    k.offscreen(), // 检测角色离开屏幕
  ]);
  player.onKeyPress("space", () => {
    if (player.isGrounded()) player.jump();
  });
  // 当游戏角色离开屏幕
  player.onExitScreen(() => {
    k.go("gameover"); // 进入 gameover 场景
  });
  // 创建 gameover 场景
  k.scene("gameover", () => {
    k.add([k.text("游戏结束"), k.pos(center())]);
  });

  const ground = {
      width: k.width(),
      height: 48,
    },
    outlineThick = 4;
  k.add([
    k.rect(ground.width - outlineThick / 2, ground.height),
    k.outline(outlineThick),
    k.pos(0, k.height() - ground.height),
    k.area(),
    k.body({ isStatic: true }),
  ]);

  let score = 0;
  const scoreBoard = k.add([k.text("0")]);

  const cube = {
      width: 50,
      height: 50,
    },
    speeds = [100, 300, 500];
  // 创建循环
  k.loop(
    1, // 循环间隔,单位:秒
    () => {
      // 计分板
      score++;
      scoreBoard.text = score.toString();

      // 添加障碍物
      k.add([
        k.rect(cube.width, cube.height),
        k.outline(outlineThick),
        k.pos(k.width(), k.height() - ground.height - cube.height),
        k.area(),
        k.body(),
        k.move(
          k.vec2(-1, 0), // 移动方向,此处采用二维向量表示,意义是向 x 轴负方向移动
          speeds[Math.floor(Math.random() * speeds.length)] // 移动速度
        ),
      ]);
    }
  );
};

(3)导出游戏

  1. 使用命令 npm run build 构建并导出制作的游戏
    • 此时,项目根目录下会生成 dist 目录,即导出游戏所在的目录
  2. 使用命令 npm run preview 预览导出的游戏

0x03 开发导引

  • 官方文档不支持中文,详细用法须参考原文档
  • 版本为 3001.0.19

(1)基础功能 Basics

游戏对象 Game Objects

  • 游戏对象通过 add() 创建,游戏对象支持创建子游戏对象

    const k = kaplay();
    const player = k.add(["player"]);
    const head = player.add(["head"]);
    
  • 获取游戏对象:get()

    console.log(k.get("*")); // 获取所有对象,返回对象数组
    console.log(k.get("player")); // 获取所有指定标签的子级对象
    console.log(k.get("head")); // 无法直接跨级获取更深层级的对象
    console.log(
      k.get("head", {
        recursive: true, // 开启递归以获取深层级对象
      })
    );
    
    
  • 移除游戏对象:destory()

    destory(head);
    // 或
    head.destroy();
    
    • 移除子对象

      player.destroy(head);
      
  • 动态创建游戏对象

    // 直接创建一个游戏对象
    function create(x, y) {
      return k.add([k.pos(x, y)]);
    }
    
    // 克隆一份游戏对象配置,并非直接创建
    function clone(x, y) {
      return [k.pos(x, y)];
    }
    

组件 Components

  • 组件 API:

    • agent(opt?):设置能够在瓦片地图上找到路径的代理

    • anchor():设置渲染锚点,默认左上

    • animate(opt?):设置动画

    • area():设置碰撞检测

    • body():设置物理体来响应重力

    • circle(r, opt?):设置圆形,第一个参数表示半径

    • color():设置颜色

      • 单个参数为字符串时,表示字符串对应的颜色
      • 单个参数为数组时,分别表示 rgb
      • 单个参数也可以是 Kaplay 定义的 Color 类型
    • doubleJump(n):支持多段跳

      • n:允许的跳跃次数,默认为 1
    • drawon(frameBuffer):指定对象应绘制的帧缓冲

      const canvas = k.makeCanvas(k.width(), k.height());
      k.add([k.drawon(canvas.fb)]);
      
    • fadeIn(time):设置淡入,参数为时间

    • fixed():设置游戏对象不受相机或父对象影响,一般最后渲染,适用于 UI 元素

    • follow(obj, offset?):跟随其他指定游戏对象

      • obj:通过地址指定游戏对象
      • offset:跟随偏移
    • health(hp, maxHp?):设置生命值,可选设置最大生命值

    • lifespan(time, opt?):设置定时销毁游戏对象

    • mask(type?):设置遮罩

    • move(dir, speed):设置移动

      • dir:移动方向,数字表示 \(x\) 轴方向移动,vec2 表示兼顾 \(x\)\(y\) 轴方向
      • speed:移动速度
    • named(name):命名游戏对象

    • offscreen(opt?):控制物体离开视图的行为

      • 通常使用 offscreen({ destroy: true }) 表示物体离开后销毁
    • opacity(o):设置透明度,取值范围为 \([0,1]\)

    • outline(width?, color?, opacity?, join?, miterLimit? cap?):设置描边

      • width:描边宽度(厚度)
      • color:描边颜色,默认黑色
      • opacity:透明度
      • join:连接样式
      • miterLimit:斜接限制比率
      • cap:端点样式
    • particles(popt, eopt):设置粒子

      • popt:粒子选项
      • eopt:粒子发射选项
    • pathfinder(opt):设置计算路点到目标的导航路寻器

    • patrol(opt):设置沿路点到目标的巡逻队

    • polygon(pts, opt?):设置多边形

      • pts 是二维数组 vec2[],表示点集
    • pos(x, y):设置对象位置

      • 单个参数为数字时,表示 \(x\)\(y\) 方向的坐标为同一个值
      • 单个参数为 k.vec2 时,等同于两个参数,表示分别设置位置
    • rect(w, h):设置矩形,依此表示宽高

    • rotate(a):设置旋转角度

    • scale(x, y):设置对象缩放

      • 单个参数为数字时,表示 \(x\)\(y\) 方向都缩放
      • 单个参数为 k.vec2 时,等同于两个参数,表示分别缩放
    • sentry(candidates, opt?):设置进入视野的物体做出反应的哨兵

    • serializeAnimation(obj, name):将动画序列化为普通对象

    • shader(id, uniform?):自定义着色器

    • sprite(spr, opt?):设置雪碧图

    • state(init, list?):有限状态机

      • init:初始状态
      • list:状态列表
    • stay(scenesToStay?):场景切换后保留,参数为指定场景的数组

    • tile(opt?):设置瓦片

    • timer():允许启用计时器相关方法

    • text(txt, opt?):设置文本

    • textInput(hasFocus?, maxInputLength?):设置输入交互功能

    • uvquad(w, h):设置 UV 四边形

    • z(z):设置堆叠层级

    const cube = k.add([
      k.rect(50, 50),
      k.outline(4),
      k.pos(k.center()),
      k.area(),
      k.body(),
    ]);
    
  • 组件效果器

    • areaEffector(opt):对碰撞物体施加力,适用于施加反重力水流
    • buoyancyEffector(opt):根据流体密度和浸入面积,对碰撞物体施加向上的力(与重力方向相反的力),适用于施加恒定推力
    • constantForce(opt):对物体施加恒定力,适合施加恒定推力
    • platformEffector(opt?):实现单向平台或墙壁,通常与静态物体一起使用,坚固程度取决于物体的移动方向
    • pointEffector(opt):对碰撞到的物体施加一个指向此物体原点的力,适合用于模拟磁性吸引或排斥
    • surfaceEffector(opt):对碰撞物体施加一个力,使其沿碰撞切向向量移动,适用于传送带
  • 判断是否有某个组件:has()

    console.log(cube.has("area")); // true
    console.log(cube.has("sprite")); // false
    
  • 补充组件:use()

    cube.use(k.color("#ffff00"));
    
  • 移除组件:unuse()

    cube.unuse("body");
    
  • 自定义组件

    function myComp(value) {
      let data = value;
      return {
        id: "myComp",
        require: ["pos"], // 必须和指定组件搭配
        add() {
          console.log("Add"); // 当游戏对象加入场景时触发
        },
        update() {
          console.log("Update"); // 每帧触发
        },
        draw() {
          console.log("Draw"); // 每帧的update结束后触发
        },
        destory() {
          console.log("Destroy"); // 销毁时触发
        },
        inspect() {
          console.log("Inspect"); // 设置在debug时显示的内容
          return "自定义组件";
        },
      };
    }
    

    返回的对象禁止使用箭头函数

标签 Tags

  • 添加标签:tag()

    const cube = k.add([]);
    cube.tag("cube");
    cube.tag(["block", "rect"]);
    
  • 判断是否有某个标签:is()

    cube.is("cube");
    cube.is("player");
    
  • 获取所有标签:tags

    cube.tags
    
  • 移除指定标签:untag()

    cube.untag("rect");
    

事件 Events

  • 输入 API:

    • charInputted():自上一个帧以来输入的字符列表
    • 按钮绑定与虚拟触发
      • getButton(btn):从按钮名称获取输入绑定
      • setButton(btn, def):为按钮名称设置输入绑定
      • pressButton(btn):虚拟按下按钮
      • releaseButton(btn):虚拟释放按钮
    • isKeyDown(k?:是否有任何键被按下
    • isMouseDown(btn?):是否有鼠标键被按下
    • isTouchScreen():当前设备是否为可触摸设备
    • mousePos():获取当前鼠标位置
    • mouseDeltaPos():自上一帧鼠标的移动量
    • 按钮触发:
      • onButtonDown(btn?, action):对指定的按钮绑定按住事件,action 参数为按下的按钮
      • onButtonPress(key, action):对指定的按钮绑定单击事件,action 参数为按下的按钮
      • onButtonRelease(key, action):对指定的按钮绑定离开事件,action 参数为按下的按钮
    • onCharInput(action):字符输入事件,action 参数为输入的字符
    • onClick(tag, action):对指定标签的模型绑定点击事件,action 参数为被点击的游戏对象
    • 键盘触发:
      • onKeyDown(key, action):对指定的键绑定按住事件,action 参数为按下的键
      • onKeyPress(key, action):对指定的键绑定单击事件,action 参数为按下的键
      • onKeyPressRepeat(key, action):对指定的键绑定长按事件,action 参数为按下的键
      • onKeyRelease(key, action):对指定的键绑定离开事件,action 参数为按下的键
    • 鼠标触发:
      • onMouseDown(btn?, action):按住鼠标事件,可以对指定鼠标键进行绑定,action 参数为触发事件的鼠标键
      • onMouseMove(action):移动鼠标事件,action 参数为位置和变化量
      • onMousePress(action):点击鼠标事件,action 参数为触发事件的鼠标键
      • onMouseRelease(btn?, action):离开鼠标事件,可以对指定鼠标键进行绑定,action 参数为触发事件的鼠标键
    • onScroll(action):滚动事件
    • 触摸事件:
      • onTouchEnd(action):类 onMouseRelease 的触摸结束事件
      • onTouchMove(action):类 onMouseMove 的触摸移动事件
      • onTouchStart(action):类 onMouseDown 的触摸开始事件
  • 事件 API:

    • cancel():通过返回 Symbol 来取消事件
    • on(event, tag, action(obj, args)):对指定标签 tag 游戏对象绑定事件 eventaction 是事件对应的方法
    • onAdd(tag, action):注册一个当指定标签的游戏对象被添加时运行的事件
    • onCleanup(action):在调用 quit() 时运行的清理函数
    • onCollide(t1, t2, action(a, b, col?)):注册一个当两个具有特定标签的游戏对象发生碰撞时运行一次的事件
      • t1t2 都要有 area() 组件
      • ab 都是触发该事件的游戏对象
    • onCollideUpdate(t1, t2, action(a, b, col?))
    • onDestroy(tag, action):注册一个当指定标签游戏对象被销毁的事件
    • onDraw(tag, action):对指定标签的所有游戏对象注册一个每帧运行的事件
      • draw 类事件在 update 类事件之后
    • onError(action):注册自定义错误处理程序,可用于显示自定义错误页面
    • onFixedUpdate(action):注册一个以固定帧率运行的事件
    • 显隐相关:
      • onHide(action):注册一个当标签页隐藏的事件
      • onShow(action):注册一个当标签页显示的事件
    • 悬停相关:
      • onHover(tag, action(a)):注册一个当指定标签的游戏对象被悬停时运行一次的事件
        • tag 要有 area() 组件
      • onHoverUpdate(tag, action(a)):注册一个当指定标签的游戏对象被悬停的每帧运行的事件
      • onHoverEnd(tag, action(a)):注册一个当指定标签的游戏对象悬停结束时运行一次的事件
    • 载入相关:
      • onLoad(action):注册一个当所有资产(assets)结束加载的事件
      • onLoadError(action):注册一个在所有其他资产加载完成后,对于每个加载失败的资产只运行一次的事件
      • onLoading(action):注册一个在资源初始加载时每帧运行的事件,可用于绘制自定义加载界面
    • onSceneLeave(action(newScene)):注册一个当前场景结束的事件
    • onResize(action):注册在画布大小变化时运行的事件
    • 标签相关:
      • onTag(action):注册一个当一对象获得标签时运行的事件
      • onUntag(action):注册一个当一对象失去标签时运行的事件
      • onUnuse():注册一个当一对象停止使用组件的事件
      • onUse(action):注册一个当一对象开始使用组件的事件
    • onUpdate(tag, action):对指定标签的所有游戏对象注册一个每帧运行的事件
      • update 类事件在 draw 类事件之前
    • trigger(event, tag, args):触发指定标签 tag 游戏对象的事件 event,并传递指定参数 args
  • 应用级事件运行时不需要附加到游戏对象上,一般与游戏屏幕、输入等有关,如:onLoadonKeyPress

  • 根级事件运行在根游戏对象的生命周期的期间,如:onUpdateonDraw

  • 游戏对象级事件运行在单个游戏对象生命周期中的特定时刻,如:onAddonDestroy

    • 在根级事件上进行扩展
    • 部分事件需要游戏对象具备相应的组件,如 health 组件时游戏对象具备健康值属性从而可用 onHurt 事件
  • 自定义事件需要使用 on 注册,trigger 触发

    const player = k.add([]);
    k.on("customEvent", "player", () => {}); // 自定义事件
    onKeyPress(() => player.trigger("customEvent")); // 触发事件
    

场景 Scenes

  • 创建场景:scene()

  • 切换场景:go()

    • 场景间传递数据

      const s1 = k.scene("s1", (value) => {
        console.log(value);
      });
      k.go("s1", "SRIGT");
      

      传递多个参数

      const s2 = k.scene("s2", ({ name, job }) => {
        console.log(`${name}-${job}`);
      });
      k.go("s2", { name: "SRIGT", job: "Programmer" });
      

雪碧图 Sprites

  • 载入雪碧图:loadSprite()

    k.loadSprite("name", "path/to/sprite.png");
    

    载入雪碧图集

    k.loadSprite("name", "path/to/spritesheet.png", {
      sliceX: 2, // x 轴方向上雪碧图个数
      sliceY: 2, // y 轴方向上雪碧图个数
      anims: {
        a: { from: 0, to: 3, loop: false },
        b: { from: 4, to: 4 },
      },
    });
    

    雪碧图集的顺序是:左上角为起点(0),先从左向右,再从上向下

    01
    23
  • 通过组件使用雪碧图:sprite()

    const player = k.add([k.sprite("name"), {
      frame: 1,
      flipX: true, // 在 x 轴翻转雪碧图
      flipY: true,
      anim: "a", // 使用名称为 a 的动画配置
    }]);
    
  • 执行雪碧图动画:play()

    const player = k.add([k.sprite("name")]);
    player.play();
    

声音 Sounds

  • 载入声音:loadSound()

    k.loadSound("name", "/path/to/sound.mp3", {
      volume: 0.5, // 设置音量
      speed: 1, // 播放倍速
      loop: true, // 是否循环
    });
    
  • 播放声音:play()

    const sound = k.play("name");
    
  • 暂停声音:pause()

    sound.pause();
    

(2)进阶功能 Advanced

优化 Optimization

  1. 静态图像或简单的形状无需创建游戏对象,可以使用 onDraw 搭配 drawSprite 实现

    k.onDraw(() => {
      k.drawSprite({
        sprite: "bean",
        pos: k.vec2(0, 0),
      });
    });
    
  2. 对于需要大量创建或销毁的游戏对象,可以通过以下方法优化:

    1. 采用粒子系统
    2. 采用 onDraw 搭配 drawSprite 绘制
    3. 文本采用 textdrawText 绘制
    4. 声明一个对象来存储游戏状态数据
  3. 对于一次性游戏对象(比如子弹)需要及时清理,如 k.offscreen({ destroy: true })

  4. 对于目前不在视口的游戏对象需要及时隐藏和暂停,如 k.offscreen({ hide: true, pause: true })

  5. 避免将 Kaplay 注册到全局命名空间,需要在初始化 Kaplay 时,设置属性 global: false

  6. 压缩游戏资产以节约载入时间

    1. 字体文件 .ttf.otf 压缩为 .woff2
    2. 声音文件 .wav 压缩为 .ogg.mp3
  7. 尽量使用游戏对象本地组件,从而避免当游戏对象暂停或销毁后,其绑定在全局的相关功能继续运行

    1. 计时器 timer
    2. 输入处理器 input-handler

动画 Animation

  • 一般动画在 onUpdate 方法中,结合 dt 方法计算时间差,通过 lerp 进行插值动画,如:

    const start = k.vec2(0, 0),
      end = k.vec2(200, 200);
    const cube = k.add([k.rect(50, 50), k.outline(4), k.pos(start), { time: 0 }]);
    cube.onUpdate(() => {
      cube.time += k.dt();
      const t = (cube.time % 3) / 3;
      cube.pos = k.lerp(start, end, t);
    });
    
  • 补间动画 tween(from, to, duration, setValue, easeFunc?) 适用于局部对象的属性,如果使用补间动画的对象在动画过程中被销毁,则可能会导致应用崩溃

    const start = k.vec2(0, 0),
      end = k.vec2(200, 200);
    const cube = k.add([k.rect(50, 50), k.outline(4), k.pos(start)]);
    k.tween(start, end, 3, (pos) => (cube.pos = pos));
    
  • 动画组件 animate(opt?) 功能更丰富、体积更轻量,能够对多个属性实现多个插值的动画效果

    const start = k.vec2(0, 0),
      via = k.vec2(50, 0),
      end = k.vec2(200, 200);
    const cube = k.add([k.rect(50, 50), k.outline(4), k.pos(start), k.animate()]);
    cube.animate("pos", [start, via, end], {
      duration: 3,
      direction: "forward",
    });
    

    其他效果还支持:

    1. 修改各个关键帧之间所需时间(百分比),可以避免因距离不一致导致速度变化

      cube.animate("pos", [start, via, end], {
        duration: 3,
        timing: [0, 1 / 5, 1],
      });
      
    2. direction: "ping-pong" 时,可以设置循环次数,来回一次计算为循环两次

      cube.animate("pos", [start, via, end], {
        duration: 3,
        timing: [0, 1 / 5, 1],
        direction: "ping-pong",
        loops: 4,
      });
      
    3. 可以设置插值方法

      cube.animate("pos", [start, via, end], {
        duration: 3,
        interpolation: "spline",
      });
      
    4. 可以设置方法停止 unanimate() 与重置 seek(0)

画布 Canvas

  • Kaplay 的画布基本上是一种帧缓冲纹理(a framebuffer texture),当画布处于活动状态时,所有内容都会绘制到画布帧缓冲纹理上
  • 可以使用 makeCanvas(width, height) 创建画布,通过 draw(action) 方法在创建的画布中绘制
  • 创建后的画布可以通过 bind() 绑定或通过 unbind() 解绑
  • 创建后的画布也可以作为组件,将使用该组件的游戏对象的所有子对象被绘制到这个画布上
  • 可以使用 drawCanvas()drawUVQuad() 绘制画布
  • 画布绘制结果可以通过 toImageData()toDataURL() 导出保存
  • 最后可以通过 free() 方法释放

粒子 Particles

  • 粒子系统由两部分构成:粒子发射器和粒子本身
  • 在游戏对象中使用 particles() 组件创建粒子系统
  • 可以使用 getSprite().data 获取纹理数据来生成粒子
  • 使用 emit(n) 来发射粒子,其中 n 是粒子数量
k.loadSprite("bean");

// 获取纹理数据来生成粒子
const loadedSpriteData = k.getSprite("bean").data;

// 创建粒子发射器
const particleEmitter = k.add([
  k.pos(k.center()),
  k.particles(
    {
      max: 20, // 从当前发射器一次性生成粒子的最大数量
      lifeTime: [2, 5], // 粒子存活时长
      speed: [50, 100], // 粒子移速
      acceleration: [k.vec2(0), k.vec2(0, -10)], // 粒子生命周期内加速度
      damping: [0, 0.5], // 粒子生命周期内阻尼
      angle: [0, 360], // 每个粒子可旋转角度
      angularVelocity: [0, 100], // 每个粒子旋转速度
      scales: [1.0, 0.5, 0.0], // 粒子生命周期内缩放大小
      colors: [k.RED, k.GREEN, k.BLUE], // 粒子生命周期内颜色
      opacities: [1.0, 0.0], // 粒子生命周期内透明度
      texture: loadedSpriteData.tex, // 粒子纹理
      quads: loadedSpriteData.frames, // 指定发射器使用的雪碧图帧
    },
    {
      shape: new k.Rect(k.vec2(0), 32, 32), // 粒子发射的起始区域(不能为空)
      lifetime: 5, // 发射器存活时长
      rate: 5, // 粒子发射速率
      direction: 0, // 粒子运动方向
      spread: 45, // 粒子运动方向偏差
    }
  ),
]);

// 运行时发射 5 个粒子
particleEmitter.emit(5);

// 每 1 秒发射 15 个粒子
k.wait(1, () => particleEmitter.emit(15));

// 发射器生命周期结束时触发
particleEmitter.onEnd(() => k.destroy(particleEmitter));

路寻 Pathfinding

  • 路寻是指寻找两个地点之间的路径

  • 可以通过内置的 level 结合 agent 组件和 tile 组件来使用路寻

    • level 需要通过 addLevel() 方法创建
    • 通过调用 spawn() 方法在指定位置生成游戏对象,其中游戏对象的 agent 组件是具有路寻能力的对象,包括移速、对角线移动能力等属性
    • tile 组件在 addLevel() 方法中创建,用于定义地图中的瓦片含义以便于路寻
    const level = k.addLevel(
      // 创建地图
      [
        "                          $",
        "                          $",
        "           $$         =   $",
        "  %      ====         =   $",
        "                      =    ",
        "       ^^      = >    =   &",
        "===========================",
      ],
      // 配置瓦片
      {
        tileWidth: 32, // 瓦片宽度
        tileHeight: 32, // 瓦片高度
    
        // 设置瓦片对应游戏对象
        tiles: {
          "=": () => [k.sprite("floor"), k.area(), k.body({ isStatic: true })], // 地板
          $: () => [k.sprite("coin"), k.area(), k.pos(0, -9)], // 金币
          "^": () => [k.sprite("spike"), k.area(), "danger"], // 尖刺
        },
      }
    );
    
    k.loadBean();
    const bean = level.spawn(
      [
        k.sprite("bean"),
        k.anchor("center"),
        k.pos(32, 32),
        k.tile(),
        k.agent({ speed: 64, allowDiagonals: false }),
        "bean",
      ],
      k.vec2(1, 1)
    );
    
  • 也可以自定义路寻

    1. 实例化 NavMesh 类并调用其 addPolygon() 方法创建地图
    2. 在场景中添加 navigation 导航组件,其中上述地图需要导入到 graph 属性
    3. 此时可以使用 navigateTo() 方法自动规划路径到指定位置
    4. 可以使用 patrol 组件创建巡逻队,并将上述路径提供给“巡逻队”游戏对象的 waypoints 属性
    5. 可以使用 sentry 组件创建哨兵,以感知视野并采取行动

物理 Physics

  • Kaplay 的物理效果需要依赖 area 组件和 body 组件

    • area 组件可以将游戏对象看作碰撞形状,如果结合 body 组件,则该游戏对象为可碰撞实体
    • 默认情况下 area 组件会创建与绘制形状相同的区域并传递给雪碧图
    • 对于设置了 body 组件的游戏对象,如果也通过 setGravity() 方法设置了重力,则该游戏对象会受重力影响,除非设置其 isStatic 属性为 true
    • 实体的速度可以通过修改冲量 applyImpulse() 和力 addForce() 发生改变,其作用域 area 的质心
  • Kaplay 提供了效果器来施加力以模拟现实情况,包括区域效果器 areaEffector()

    参考 0x03 章(1)节组件中的效果器相关 API

图片 Picture

  • 图片可以跳过很多绘制流程以优化渲染效果,当图片处于活动状态时,顶点数据会被发送到图片上,此时当图片被绘制时,只需要进行渲染步骤
  • 需要使用 beginPicture 方法搭配 endPicture 方法进行绘制
  • 绘制图片需要在 onDraw 中使用 drawPicture
  • 可以使用 appendToPictrue 追加图片
  • 绘制完成后需要使用 free 方法释放该图片
k.loadSprite("bean");
k.onLoad(() => {
  k.beginPicture(new k.Picture());
  for (let i = 0; i < 16; i++) {
    for (let j = 0; j < 16; j++) {
      drawSprite({
        pos: vec2(64 + i * 32, 64 + j * 32),
        sprite: "bean",
      });
    }
  }
  const picture = k.endPicture();

  k.onDraw(() => {
    k.drawPicture(picture, {
      pos: k.vec2(400, 0),
      angle: 45,
      scale: k.vec2(0.5),
    });
  });

  picture.free();
});

着色器 Shaders

  • Kaplay 中,顶点着色器只能改变顶点位置,默认顶点着色器会接收顶点位置、UV 坐标、颜色参数,并返回更新后的位置:

    vec4 vert(vec2 pos, vec2 uv, vec4 color) {
      return def_vert();
    }
    

    顶点着色器默认会返回图元的原始顶点,如果需要更改后的顶点,则需要将其视为四维向量 \((x, y, z, w)\),其中 \(x\)\(y\) 是顶点位置,\(z=0\\w=1\)

  • Kaplay 中,片段着色器可以影响颜色,默认片段着色器会接受顶点位置、UV 坐标、颜色、二维贴图参数:

    vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {
      return def_frag();
    }
    

    片段着色器默认将基础颜色与纹理颜色混合,如果需要返回修改后的颜色,则需要将其设为四维向量 \((r,g,b,a)\),其中各个参数均为 \([0,1]\) 之间的浮点数,比如引入灰度、并引入纹理透明度:

    vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {
      vec4 tcolor = texture2D(tex, uv);
      float gray = dot(tcolor.rgb, vec3(0.299, 0.587, 0.114));
      return vec4(color.rgb * gray, tcolor.a);
    }
    
  • 加载着色器方法包括直接载入代码 loadShader() 或导入代码文件 loadShaderURL()

    k.loadShader("shaderName", VERT_STRING, FRAG_STRING);
    k.loadShaderURL("shaderName", VERT_PATH, FRAG_PATH);
    

    加载着色器时支持传递动态参数,假定上述 FRAG_STRING 中存在以下变量:

    uniform float u_time;
    

    则可以在使用该着色器时传递动态参数

    k.add([
      k.shader("shaderName", () => {
        return { u_time: k.time() };
      }),
    ]);
    

    也可以在绘制雪碧图时使用

    k.drawSprite({
      shader: "shaderName",
      uniforms: {
        u_time: k.time(),
      },
    });
    
  • 全屏后期处理着色器可以使用 usePostEffect() 方法

  • 对于高斯模糊等需要多次渲染才能生效的情况,可以通过在创建的帧缓冲区中绘制后,使用该帧缓冲区的纹理绘制来实现

posted @ 2025-12-05 17:43  SRIGT  阅读(3)  评论(0)    收藏  举报