家好,我是一個游戲引擎技術探索者,同時也是一名做過不少前端開發工作的程序員。如果你想知道如何從編寫網頁到開發游戲,那你來對地方了!
不必擔心,說到游戲引擎并不是啥高不可攀的技術,反而和我們熟悉的前端開發工具可以有驚人相似之處。
不小心裝成了APK包在手機上?那就在同局域網下訪問,直接在手機上進行開發調試吧
// @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}/>);
Box
組件定義,它接受num
、x
、y
和 children
等屬性: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>
);
};
body
組件的 tag
屬性:用于存儲箱子的分數。rect-fixture
:定義了箱子的碰撞形狀。draw-node
:用于繪制箱子的外觀。label
:用于顯示盒子的分數。const bird = useRef<Body.Type>();
const score = useRef<Label.Type>();
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
事件中,根據觸摸移動的距離設置小鳥的發射速度并清除發射線。接下來,我們以 <physics-world>
作為游戲場景的父級標簽,在它下面繼續創建游戲場景中的各個元素:
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
:繪制一個矩形,顏色為黃色。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 秒。最后,我們使用 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
:用于播放小鳥的出場動畫。拖拽屏幕發射了“憤怒的小鳥”
高超的技巧,使我一擊獲得了所有得分
作者介紹
李瑾:金融行業大數據工程師,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來模擬畫筆功能,比如鼠標按下后,在屏幕中拖動,會形成痕跡,那么整個過程,就是在模擬畫筆。
如果你不想細讀,你可以看下我做的 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 來開發了。
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>
要渲染我們的這個小游戲,<canvas> 這個標簽應該足夠了。我們可以直接從 Go 代碼創建 DOM 結構和元素!這個 syscall/js 文件 (包含在標準 Go 庫中)為我們處理了與 DOM 交互的方法。
我敢打賭,你很久沒見過 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 上有了致命的切換能力。如果他們不愿意配合,大眾可能永遠不會知道有這個特性。
你必須得承認,我的項目已經在用了。這個項目僅僅是畫了一個全屏的畫布,這里有一些更高級的例子,它們關注于語義 Web awesome-wasm#web-frameworks-libraries。
同時,也有相當多的項目已經上了 WASM 的車了。我對 Spotify、Twitch 和 Figma 和 EWASM 更感興趣。
現在,如果你想在手機上使用以太坊錢包(Ethereum wallet),你必須從應用商店下載一個類似于 http://Status.im 的移動端錢包 App,并且信任所有商家。
如果有一個先進的 Web App,可以運行 geth (Go Ethereum 客戶端),并且能在 WebRTC 上光速同步,這會怎么樣?它可以使用 Service Worker 來更新它的 WASM 代碼并在后臺運行,可以托管在 IPFS/Dat 上。
感謝 twifkak 在 Android Chrome 上對 Go 的優化!
原標題:The world’s easiest introduction to WebAssembly
原文鏈接:The world’s easiest introduction to WebAssembly - freeCodeCamp.org - Medium
作者:Martin Olsansky (olso)
*請認真填寫需求信息,我們會在24小時內與您取得聯系。