整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

这款中国程序员开源的游戏引擎,让你用不到100行代码就写出“愤怒的小鸟”

家好,我是一个游戏引擎技术探索者,同时也是一名做过不少前端开发工作的程序员。如果你想知道如何从编写网页到开发游戏,那你来对地方了!

今天我们聊聊如何使用 Dora SSR,一个支持 TSX 且跨平台在 native 运行的游戏引擎,助你轻松跨入游戏开发的世界。

不必担心,说到游戏引擎并不是啥高不可攀的技术,反而和我们熟悉的前端开发工具可以有惊人相似之处。


一、游戏客户端开发也可以是一种前端开发

首先,让我们解释一下什么是游戏引擎。
简单来说,游戏引擎就是一套工具和库的集合,帮助开发者构建游戏,管理图形、声音、物理计算或碰撞检测等。
对于前端开发者来说,你可以把它想象成就是一种特殊的浏览器,专门用来运行游戏。
Dora SSR 的游戏场景管理使用了类似于 HTML DOM 的树形结构,这对我们来说再熟悉不过了。
想象一下,将 div 元素换成游戏中的各种对象,CSS 动画换成游戏动画,概念也差不多,代码写法上可能也差不多,是不是觉得有点意思了?


二、从 TypeScript 到 TSX:前端技术在游戏中的应用

许多前端开发者都熟悉 TypeScript 和 React 的 JSX 语法。
在 Dora SSR 开源游戏引擎中,我们通过支持 TSX,提供了与前端开发编程模式相似的游戏开发接口。
是的你没听错,就是那个 TSX。
使用 TSX 开发游戏,意味着你可以利用已有的前端技术栈 — 组件、模块和其他现代前端技术,直接在游戏开发中复用这些概念。
而且,Dora SSR 的性能优化确保了即使是在复杂的游戏场景中,也能保持流畅的运行。


三、挑战 100 行代码以内,教你写一个 “愤怒的小鸟” like 的游戏

好了,理论知识够多了,让我们来点实际操作吧。
来看看如何在 Dora SSR 中用 100 行以内的 TSX 代码编写一个类似 “愤怒的小鸟” 的小游戏。
当然,在开始之前还是要准备开发环境,做这个事用 Dora SSR 就很简单:我有一个安装包一装,我有一个浏览器一开,嗯,开始写代码运行吧。
安装启动参见:Dora 启动!
(https://dora-ssr.net/zh-Hans/docs/tutorial/quick-start)
不小心装成了APK包在手机上?那就在同局域网下访问,直接在手机上进行开发调试吧

1. 编写最简单游戏场景

在编写实际的代码之前,我们可以先写一个有特别功能的注释,它可以告诉 Dora SSR 的 Web IDE 在我们按下 Ctrl + S 保存文件时,自动热更新运行的代码,以实现代码运行结果的实时预览功能。
// @preview-file on
然后,我们引入必要的库和组件。
当然我们的代码编辑器也会提示辅助我们自动引入需要的模块,可以放到后面编码过程中再完成:
import { React, toNode, useRef } from 'DoraX';
import { Body, BodyMoveType, Ease, Label, Line, Scale, TypeName, Vec2, tolua } from 'Dora';

在 Dora SSR 中显示一个图片很简单,只要使用 <sprite> 标签,最后通过 toNode() 函数将标签实例化为一个游戏对象就可以了。

toNode(<sprite file='Image/logo.png' scaleX={0.2} scaleY={0.2}/>);
好的,至此你已经基本掌握大部分 Dora SSR 游戏开发的诀窍了,开始做你自己的游戏吧(认真)。

2. 编写游戏箱子组件

接下来我们在游戏中碰撞的箱子会由 Box 组件定义,它接受
numxychildren 等属性:
interface BoxProps {
num: number;
x?: number;
y?: number;
children?: any | any[];
}

const Box = (props: BoxProps) => {
const numText = props.num.toString();
return (
<body type={BodyMoveType.Dynamic} scaleX={0} scaleY={0} x={props.x} y={props.y} tag={numText}>
<rect-fixture width={100} height={100}/>
<draw-node>
<rect-shape width={100} height={100} fillColor={0x8800ffff} borderWidth={1} borderColor={0xff00ffff}/>
</draw-node>
<label fontName='sarasa-mono-sc-regular' fontSize={40}>{numText}</label>
{props.children}
</body>
);
};
我们使用仿 React 的函数组件的写法来完成我们箱子组件的定义,其中:
  • body 组件的 tag 属性:用于存储箱子的分数。
  • rect-fixture :定义了箱子的碰撞形状。
  • draw-node :用于绘制箱子的外观。
  • label :用于显示盒子的分数。

3. 创建 TSX 实例化后的对象引用

使⽤ useRef 创建两个引⽤变量进行备用,分别指向⼩⻦和分数标签:
const bird = useRef<Body.Type>();
const score = useRef<Label.Type>();

4. 创建发射线

发射线由 line 变量创建,并添加触摸(同时也是鼠标点击)的事件处理:
let start = Vec2.zero;
let delta = Vec2.zero;
const line = Line();

toNode(
<physics-world
onTapBegan={(touch) => {
start = touch.location;
line.clear();
}}
onTapMoved={(touch) => {
delta = delta.add(touch.delta);
line.set([start, start.add(delta)]);
}}
onTapEnded={() => {
if (!bird.current) return;
bird.current.velocity = delta.mul(Vec2(10, 10));
start = Vec2.zero;
delta = Vec2.zero;
line.clear();
}}
>
{/* ...在物理世界下创建其它游戏元素 ... */}
</physics-world>
);
  • onTapBegan 事件中,记录触摸开始的位置并清除发射线。
  • onTapMoved 事件中,计算触摸移动的距离并更新发射线。
  • onTapEnded 事件中,根据触摸移动的距离设置小鸟的发射速度并清除发射线。

5. 创建其它游戏元素

接下来,我们以 <physics-world> 作为游戏场景的父级标签,在它下面继续创建游戏场景中的各个元素:

5.1 地面

首先,我们使用 body 组件创建一个地面,并将其设置为静态刚体:
<body type={BodyMoveType.Static}>
<rect-fixture centerY={-200} width={2000} height={10}/>
<draw-node>
<rect-shape centerY={-200} width={2000} height={10} fillColor={0xfffbc400}/>
</draw-node>
</body>
  • type={BodyMoveType.Static}:表明这是一个静态刚体,不会受到物理模拟的影响。
  • rect-fixture:定义地面碰撞形状为一个矩形。
  • draw-node:用于绘制地面的外观。
  • rect-shape:绘制一个矩形,颜色为黄色。

5.2 箱子

接下来,我们使用之前写好的 Box 组件创建 5 个箱子,并设置不同的初始位置和分数,在创建时播放出场动画:
{
[10, 20, 30, 40, 50].map((num, i) => (
<Box num={num} x={200} y={-150 + i * 100}>
<sequence>
<delay time={i * 0.2}/>
<scale time={0.3} start={0} stop={1}/>
</sequence>
</Box>
))
}
  • map 函数:用于遍历分数数组从 10 到 50,并为每个分数创建一个需要小鸟撞击的箱子。
  • Box 组件:用于创建箱子,并传入以下属性:
    • num={num}:箱子的分数,对应数组中的数字。
    • x={200}:箱子的初始 x 轴位置,为 200。
    • y={-150 + i * 100}:箱子的初始 y 轴位置,根据创建序号递增。
  • sequence 组件:用于创建要在父节点上播放的动画序列,包含以下动画:
    • delay time={i * 0.2}:延迟播放动画,延迟时间根据创建序号递增。
    • scale time={0.3} start={0} stop={1}:缩放动画,从不显示到完全显示,耗时 0.3 秒。

5.3 小鸟

最后,我们使用 body 组件创建小鸟,并设置碰撞形状、外观和分数标签:

<body ref={bird} type={BodyMoveType.Dynamic} x={-200} y={-150} onContactStart={(other) => {
if (other.tag !== '' && score.current) {
// 累加积分
const sc = parseFloat(score.current.text) + parseFloat(other.tag);
score.current.text = sc.toString();
// 清除被撞箱子上的分数
const label = tolua.cast(other.children?.last, TypeName.Label);
if (label) label.text = '';
other.tag = '';
// 播放箱子被撞的动画
other.perform(Scale(0.2, 0.7, 1.0));
}
}}>
<disk-fixture radius={50}/>
<draw-node>
<dot-shape radius={50} color={0xffff0088}/>
</draw-node>
<label ref={score} fontName='sarasa-mono-sc-regular' fontSize={40}>0</label>
<scale time={0.4} start={0.3} stop={1.0} easing={Ease.OutBack}/>
</body>
  • ref={bird}:使用 ref 创建引用变量,方便后续操控小鸟。
  • type={BodyMoveType.Dynamic}:表明这是一个动态刚体,会受到物理模拟的影响。
  • onContactStart={(other) => {}}:小鸟的物理体接触到其它物体时触发的回调处理函数。
  • disk-fixture:定义小鸟形状为一个圆盘。
  • draw-node :用于绘制小鸟的外观。
  • label :用于显示小鸟的累积分数。
  • scale :用于播放小鸟的出场动画。

6. 完成游戏逻辑

至此,我们已经完成了小游戏的核心逻辑。你可以根据自己的想法进一步完善游戏逻辑和增加功能。
完整的 demo 代码可以见这个链接:
https://github.com/IppClub/Dora-SSR/blob/main/Assets/Script/Test/Birdy.tsx
下面是一些运行效果的截图。
拖拽屏幕发射了“愤怒的小鸟”

高超的技巧,使我一击获得了所有得分


四、简单揭秘一下

1. 是鹿还是马

事实上我们写的这段游戏代码,在 Dora SSR 引擎的能力下是可以确保在跨 Linux、Android、iOS、macOS 和 Windows 获得一致的运行结果。
但是为了运行这段代码,我们的 Dora SSR 引擎甚至都没有做 JavaScript 运行环境的支持……(你说什么?)
是的,Dora SSR 的底层技术实现其实是基于 Lua 和 WASM 虚拟机作为脚本语言运行环境的。
对 TypeScript 的支持其实是通过整合了 TypescriptToLua 这个编译器提供的。
https://github.com/TypeScriptToLua/TypeScriptToLua
TSTL 通过重新编写了 TypeScript 语言编译器的后端,将 TS 和 TSX 的代码编译为了等价运行的 Lua 代码,从而使得 TS 代码可以在 Dora 上加载运行。
在 Dora 自带的 Web IDE 的代码编辑器下,可以帮助大家做 TS 的语言检查和补全以及 Dora 内置库 API 的提示。
最终的使用体验,大家就可以不用管最后是鹿还是马,只要代码能通过了 TS 的编译检查,拉出来那都是一样的跑啦。

2. 和 React 有关系吗

这个问题的答案目前是:可以有(所以截至发文前还没有)。
React 最重要的能力是通过 Virtual DOM 和执行 Tree Diff 处理的过程来进行渲染组件和业务数据的状态同步,目前这个机制还没有在 Dora SSR 中实现,所以大家目前看到的用 TSX 编写出的类似 VDOM 的构建代码只会在运行时做一次性的游戏渲染对象的构建,往后都是底层 C++ 实现的引擎功能在负责继续处理。
也许有一天我们会为游戏 UI 的开发,提供仿 React 通过执行 Tree Diff 做状态同步的能力,或是仿 SolidJS 基于 TSX 实现其它的渲染组件状态同步的机制。
所以在这里也诚挚地邀请广大前端开发的朋友,加入我们,一起玩 Dora SSR 项目,一起研究怎么运用前端开发技术思想,为游戏开发也引入更多好用便捷的轮子吧。
最后我们的 Q 群在这里,欢迎过来玩:
512620381


作者介绍

李瑾:金融行业大数据工程师,Dora SSR 和 Yuescript 开源软件作者。

项目介绍

Dora SSR(多萝珍奇引擎)是一个用于多种设备上快速开发2D游戏的游戏引擎。
它内置易用的开发工具链,支持在手机、开源掌机等设备上直接进行游戏开发。


项目仓库

https://gitee.com/pig/Dora-SSR
https://github.com/IppClub/Dora-SSR


END

OM是Document Object Model的缩写,中文名称是文档对象模型。

DOM是处理HTML页面的标准编程接口,DOM可被JavaScript用来读取、改变HTML的内容和结构。

前端三大件指HTML、CSS、JavaScript,其中JavaScript最重要的组成部分就是DOM。

一:我们为什么要学习DOM呢?

1,DOM可以让用户对网页元素进行交互操作

比如,当我点击了一个按钮,弹出一个对话框等操作。

2,DOM可以用来做网页游戏

比如,现在比较流行的游戏,我们完全可以拿JavaScript操作DOM来实现。

3,DOM是ajax的重要基础

比如,我们通过ajax获取了一些数据,你要显示给用户,这就需要用到DOM了。

二:我们是这么讲解DOM的

1,首先我们会讲解什么是DOM,就是之前说的文档对象模型

DOM是处理HTML的标准编程接口,DOM可被JavaScript用来读取、改变HTML的内容和结构。

2,会讲解什么是DOM树

对象与对象间的层次结构。

3,会讲解什么是DOM节点

根据 W3C 的 HTML DOM 标准,HTML 文档中的所有内容都是节点:

整个文档是一个文档节点

每个 HTML 元素是元素节点

HTML 元素内的文本是文本节点

每个 HTML 属性是属性节点

注释是注释节点

4,会讲解什么是事件驱动

即做了什么操作,执行什么事件。

5,会讲解什么是2级DOM

1级DOM、2级DOM、3级DOM分别指什么?怎样实现。

6,会讲解什么是事件流

会讲解什么叫做事件冒泡,什么叫做事件捕获,根据事件流的特点能实现什么功能,及如何阻止事件的传播。

7,会讲解什么是Event

Event 对象的属性提供了有关事件的细节。

三:模拟画笔案例

在网页中,通过JavaScript操作DOM来模拟画笔功能,比如鼠标按下后,在屏幕中拖动,会形成痕迹,那么整个过程,就是在模拟画笔。

  • 认为 WebAssembly (WASM) 只用于图像处理、复杂的数学计算或者 Web 上的小小应用吗?
  • 你是否经常将 WASM 与 Web Workers 和 Service Workers 的概念混淆?
  • 你对 WASM 不感兴趣,是因为你认为现在的 Web 应用程序在未来 10 年里依旧是 JavaScript 主导?
  • 你是否想过用 JS 以外的语言做 Web 前端开发?

如果你不想细读,你可以看下我做的 demo 页面或者直接看下 go-wasm-cat-game-on-canvas-with-docker 这个项目,我会讲的简洁一些,尽量不浪费你的时间。以下是我这个项目的一些关键的代码解析。

故事开始了

我们的目标是给猫 做一个简单的小游戏:做一个小红点在手机上不停的移动,整个过程还有 HiFi 音乐 还有震动。整个项目我们会用 Golang (Go)这门语言来实现,包括 DOM 操作、逻辑还有相关的状态。

而而而而而且,由于猫咪不会使用鼠标,我们还需要给猫爪 做一些点击触摸的交互。

说一下我的理解!

把 WASM 想象成一个 通用虚拟机(UVM, Universal Virtual Machine) 或者一个沙箱,你只需编写一次任何代码,它便可以在任何地方运行。

WASM 是一个编译目标,而不是一种语言。就像你要同时针对 Windows,Mac OS 和 Linux 进行编译一样!

我不认为 WASM 会废弃 JS,你可以有其他选择而不用付出任何代价。

想象一下使用 Go,Swift,Rust,Ruby,C ++,OCaml 或者其他语言的开发人员。现在,他们可以使用自己喜欢的语言来创建交互式,联网,快速,具有脱机功能的网站和Web 应用。

你是否曾经参与过类似「一个项目是用一个代码仓库管理还是多个代码仓库管理?」问题的讨论?

好吧,不管你有没有,你现在也要想一下现在这个项目打算用一门语言实现还是多门语言实现了。

当大家可以使用相同的技术栈时,一切都会变得更加容易,尤其是团队之间的沟通。

你可以依旧使用 React 或者 Vue,但你现在开始也可以不用使用 JS 来开发了。

WASM 跟 Service Workers 还有 Web Workers 有什么区别?

Service Workers 还有 Web Workers 允许应用在后台运行,也可以做到离线运行和缓存。它们模仿线程,无法访问DOM,并且不能共享数据(仅能通过消息传递),只能在单独的上下文中运行。咦,其实我们甚至可以在其中运行 WASM 而不是 JS。对我来说,它们只提供一些具有特殊特权的抽象层,没有人说这些层必须执行 JS。

Service Workers 还有 Web Workers 是浏览器上的功能,不是 JS 的专有功能。

设置开发环境

我们将使用 WASM,Go,JS 和 Docker(这个是可选的) 来进行开发。

如果您不了解Go,但了解 JS,请 点击这里学习 Go,然后再回来继续阅读。让我们从 Go WASM Wiki 开始。

你可以使用安装在电脑本地的 go 版本,在这里我使用 Docker 的 golang:1.12-rc 镜像。只需在此处为 go 编译器设置两个 WASM 标志。在 main.go 中创建一个简单的 hello world 进行测试。

$ GOOS=js GOARCH=wasm go build -o game.wasm main.go

build_go:
 docker run --rm \
 -v `pwd`/src:/game \
 --env GOOS=js --env GOARCH=wasm \
 golang:1.12-rc \
 /bin/bash -c "go build -o /game/game.wasm /game/main.go; cp /usr/local/go/misc/wasm/wasm_exec.js /game/wasm_exec.js"

现在,让我们利用好 Go 团队提供的 wasm_exec.js 代码。代码里的全局变量 Go 对 WASM 进行了初始化操作,我们不必自己从头开始做好任何 DOM 的实现。等我们编译好 wasm 文件后,它会获取 .wasm 文件并运行我们的游戏。

总而言之,它应该看起来像这样:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <style>body{height:100%;width:100%;padding:0;margin:0;background-color:#000000;color:#FFFFFF;font-family:Arial,Helvetica,sans-serif}</style>
    <script type="text/javascript" src="./wasm_exec.js"></script>
    <script type="text/javascript">
      async function run(fileUrl) {
        try {
          const file = await fetch(fileUrl);
          const buffer = await file.arrayBuffer();
          const go = new Go();
          const { instance } = await WebAssembly.instantiate(buffer, go.importObject);
          go.run(instance);
        } catch (err) {
          console.error(err);
        }
      }
      setTimeout(() => run("./game.wasm"));
    </script>
  </head>
  <body></body>
</html>

放码过来!(当然是 Go 的码)

要渲染我们的这个小游戏,<canvas> 这个标签应该足够了。我们可以直接从 Go 代码创建 DOM 结构和元素!这个 syscall/js 文件 (包含在标准 Go 库中)为我们处理了与 DOM 交互的方法。

main() 方法

我敢打赌,你很久没见过 main() 方法了 。

package main

import (
 // https://github.com/golang/go/tree/master/src/syscall/js
 "syscall/js"
)

var (
 // js.Value 可以是任意的 JS 对象、类型或者构造函数
 window, doc, body, canvas, laserCtx, beep js.Value
 windowSize struct{ w, h float64 }
)

func main() {
 setup()
}

func setup() {
 window = js.Global()
 doc = window.Get("document")
 body = doc.Get("body")

 windowSize.h = window.Get("innerHeight").Float()
 windowSize.w = window.Get("innerWidth").Float()

 canvas = doc.Call("createElement", "canvas")
 canvas.Set("height", windowSize.h)
 canvas.Set("width", windowSize.w)
 body.Call("appendChild", canvas)

 // 这个是小红点   Canvas 对象
 laserCtx = canvas.Call("getContext", "2d")
 laserCtx.Set("fillStyle", "red")

 // http://www.iandevlin.com/blog/2012/09/html5/html5-media-and-data-uri/
 beep = window.Get("Audio").New("data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjI1LjEwMQAAAAAAAAAAAAAA/+NAwAAAAAAAAAAAAFhpbmcAAAAPAAAAAwAAA3YAlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaW8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw////////////////////////////////////////////AAAAAExhdmYAAAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAN2UrY2LgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jYMQAEvgiwl9DAAAAO1ALSi19XgYG7wIAAAJOD5R0HygIAmD5+sEHLB94gBAEP8vKAgGP/BwMf+D4Pgh/DAPg+D5//y4f///8QBhMQBgEAfB8HwfAgIAgAHAGCFAj1fYUCZyIbThYFExkefOCo8Y7JxiQ0mGVaHKwwGCtGCUkY9OCugoFQwDKqmHQiUCxRAKOh4MjJFAnTkq6QqFGavRpYUCmMxpZnGXJa0xiJcTGZb1gJjwOJDJgoUJG5QQuDAsypiumkp5TUjrOobR2liwoGBf/X1nChmipnKVtSmMNQDGitG1fT/JhR+gYdCvy36lTrxCVV8Paaz1otLndT2fZuOMp3VpatmVR3LePP/8bSQpmhQZECqWsFeJxoepX9dbfHS13/////aysppUblm//8t7p2Ez7xKD/42DE4E5z9pr/nNkRw6bhdiCAZVVSktxunhxhH//4xF+bn4//6//3jEvylMM2K9XmWSn3ah1L2MqVIjmNlJtpQux1n3ajA0ZnFSu5EpX////uGatn///////1r/pYabq0mKT//TRyTEFNRTMuOTkuNaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+MQxNIAAANIAcAAAKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg==")
}

看起来是不是很像 JS 代码?

是的,这就是与 DOM 交互所需的全部内容!现在只需要几个 get 方法还有调用函数即可。

在这一点上,我问自己:在某种程度上,我仍然在写 JS … 这怎么算是升级?因为我们还不能直接访问 DOM,所以我们必须(通过 JS)调用 DOM 来做任何事情。想象一下如何用 JSX / React 来抽象化它。

实际上,已经可以做到了,请期待我的下篇文章 。

「渲染」还有事件处理

直接使用 syscall / js 库,这个写法看起来有点像 ES5 的回调。但我们能够监听 DOM 事件,而且那些静态类型看起来很干净!

func main() {
 setup()

  // 在编译时声明渲染器
 var renderer js.Func
 // 没有错,看起来很像 JS 的回调  
 renderer = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
  updateGame()
  // 实现 60FPS 的动画
  window.Call("requestAnimationFrame", renderer)
  return nil
 })
 window.Call("requestAnimationFrame", renderer)

 // 让我们处理下 鼠标/手势 点击事件
 var mouseEventHandler js.Func = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
  updatePlayer(args[0])
  return nil
 })
 window.Call("addEventListener", "pointerdown", mouseEventHandler)
}

func updatePlayer(event js.Value) {}
func updateGame() {}

日志记录、音频播放以及「异步」执行

在 Go 中,有一个惯例是把所有的函数都写成同步的方式,由调用者决定函数的执行是否是异步的。异步运行函数非常简单,只要在前面加上 go 就行了!它使用自己的上下文创建一个线程,你仍然可以将父级上下文绑定给它,不要担心哈。

func updatePlayer(event js.Value) {
 mouseX := event.Get("clientX").Float()
 mouseY := event.Get("clientY").Float()
 
  // `go` 关键字是主要用来实现线程、异步、并行的功能
 // TODO 与 Web Workers 的区别
 // TODO 与 Service Workers 的区别
 // https://gobyexample.com/goroutines
 go log("mouseEvent", "x", mouseX, "y", mouseY)

 // 下一个关键点
 if isLaserCaught(mouseX, mouseY, gs.laserX, gs.laserY) {
  go playSound()
 }
}

// 不要以为我用了什么黑魔法,这里直接使用了 HTML5 的 API
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement#Basic_usage
func playSound() {
 beep.Call("play")
 window.Get("navigator").Call("vibrate", 300)
}

// 这里主要用了 JS 的解构赋值语法
// 这里的 `...interface{}` 有点像 TS 的 `any` 语法
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters#Description
func log(args ...interface{}) {
 window.Get("console").Call("log", args...)
}

让游戏一直跑下去!

该代码创建一个非缓冲通道,并尝试从该通道接收数据。因为没有人向它发送任何东西,它本质上是一个永久的阻塞操作,允许我们永远运行我们的程序。

func main() {
 // https://stackoverflow.com/a/47262117
 // 创建空通道
 runGameForever := make(chan bool)

 setup()

 // 尝试从空通道接收
 // 由于没有人向它发送任何数据,它本质上是一个永久阻塞操作
  // 我们有一个 daeomon / service / background 程序
  // 在 WASM 里,我们的游戏会一直运行  
 <-runGameForever
}

更新游戏状态并移动小红点

这里没有状态管理,只有一个简单的声明类型的结构体,它不允许在内部传递任何不正确的值。

import (
 "math"
)

type gameState struct{ laserX, laserY, directionX, directionY, laserSize float64 }

var (
 // gs 处于最高范围,小于这个范围小红点   都能都能访问
 gs = gameState{laserSize: 35, directionX: 3.7, directionY: -3.7, laserX: 40, laserY: 40}
)

func updateGame() {
 // 边界判断
 if gs.laserX+gs.directionX > windowSize.w-gs.laserSize || gs.laserX+gs.directionX < gs.laserSize {
  gs.directionX = -gs.directionX
 }
 if gs.laserY+gs.directionY > windowSize.h-gs.laserSize || gs.laserY+gs.directionY < gs.laserSize {
  gs.directionY = -gs.directionY
 }

 // 移动小红点  
 gs.laserX += gs.directionX
 gs.laserY += gs.directionY
r/> // 清除画布
 laserCtx.Call("clearRect", 0, 0, windowSize.w, windowSize.h)
r/> //画一个小红点  
 laserCtx.Call("beginPath")
 laserCtx.Call("arc", gs.laserX, gs.laserY, gs.laserSize, 0, math.Pi*2, false)
 laserCtx.Call("fill")
 laserCtx.Call("closePath")
}r/>
// 判断点击的点是不是在小红点   内部
func isLaserCaught(mouseX, mouseY, laserX, laserY float64) bool {r/> // 直接这样返回是不行的r/> // return laserCtx.Call("isPointInPath", mouseX, mouseY).Bool()
> 
 // 所以这里我通过勾股定理   来实现r/> // 同时我给 laserSize 属性的值加上 15,让猫爪更容易点击   
 return (math.Pow(mouseX-laserX, 2) + math.Pow(mouseY-laserY, 2)) < math.Pow(gs.laserSize+15, 2)
}

总结

事实上,WASM 仍然被认为是一个 [MVP](https://hacks.mozilla.org/2018/10/webassembly -post- MVP -future/) (MAP),你可以不用编写一行 JS,就能创建一个像这样的游戏。惊不惊讶!CanIUse 上 WASM 的支持已经是一片绿色了,没有人可以阻止你去创建基于 WASM 的网站和应用。

你可以组合所有你想要的语言,像是把 JS 转成 WASM。最后,它们都将编译成 WASM 字节码。如果你需要在他们之间分享任何东西,也没问题,因为它们可以共享原始内存。

我担心的是,在最近的新闻中,我们关注到 微软正在开发 Chromium 浏览器 还有 Firefox市场份额低于9%。这使谷歌在 WASM 上有了致命的切换能力。如果他们不愿意配合,大众可能永远不会知道有这个特性。

现在都有谁在用 WASM?

你必须得承认,我的项目已经在用了。这个项目仅仅是画了一个全屏的画布,这里有一些更高级的例子,它们关注于语义 Web awesome-wasm#web-frameworks-libraries

同时,也有相当多的项目已经上了 WASM 的车了。我对 Spotify、Twitch 和 FigmaEWASM 更感兴趣。

Web3 时代的 WASM

现在,如果你想在手机上使用以太坊钱包(Ethereum wallet),你必须从应用商店下载一个类似于 http://Status.im 的移动端钱包 App,并且信任所有商家。

如果有一个先进的 Web App,可以运行 geth (Go Ethereum 客户端),并且能在 WebRTC 上光速同步,这会怎么样?它可以使用 Service Worker 来更新它的 WASM 代码并在后台运行,可以托管在 IPFS/Dat 上。

一些有用的关于 WASM 的文章、资源还有学习资料

  • WebAssembly is more than the web
  • WebAssembly and Go: A look at the future 还有 HN comments
  • Mozilla HacksHacker News 发布的文章
  • WebAssembly architecture for Goawesome-wasm , awesome-wasm-langs , gowasm-experiments , WasmWeekly , WasmRocks , SPA with C++ , better DOM bindings for Go

感谢 twifkak 在 Android Chrome 上对 Go 的优化!

原标题:The world’s easiest introduction to WebAssembly
原文链接:The world’s easiest introduction to WebAssembly - freeCodeCamp.org - Medium
作者:Martin Olsansky (olso)