想直接看成果的傳送門 => https://youtu.be/4LLYvSY010Y
目標: 在光暈戰記上面播一個能玩的Bad Apple,同時播放原影片
最初的開發方向: 先預備好每個畫格的資料,以二維布冧陣列表示每一格,用箱子變黑顯示,主要有三步
-
預備二維布冧陣列
-
搞到影片 (480px*360px)
-
到光暈戰記量度我想要的輸出大小 (18格*13格)
-
用python opencv2 將影片轉成二維布冧陣列,用gzip 方式壓縮,因為那麼多畫格直接json可能會慢。上載至github page 備用
-
-
研究如何在CG以程式方式放置地圖物件
-
看大神的Alt模組是怎製作放置地圖物件動作
^用F12下載他的碼
^ 找到新增地圖物件的代號
^ 在代碼中找到他的原理1
^ 在代碼中找到他的原理2,至於怎樣去理解這種超醜的程式,我也不知道怎解釋...
-
理解好到CG試試自製一個動作去放地圖物件
import CgAction = CG.CgEventsEngine.CgAction; import TWEventsUtil = CG.TwilightWarsLib.libs.TWEventsUtil; export class PlayBadAppleAction extends CgAction{ // ... public someFunction() { const game = TWEventsUtil.getEventsGame(this.event.manager); const box1 = game.mapResource.getObjectResourceByName('box1') const obj = game.mapRenderer.buildObjectRenderer(box1, 5, 5) game.addToGroundRoot(obj) } }
-
調整地圖物件至黑色,黑木箱是黑象素,地皮是白象素
obj.children[0].tint = '#000000'
-
-
嘗試用地圖物件播放Bad Apple
-
用fetch API 去下載github page 上面的gzip file,用pakos 解壓
export class PlayBadAppleAction extends CgAction{ //... private frames public function prepare(){ fetch('https://dipsy.me/bad-apple/compressed.gzip') .then(r => r.arrayBuffer()) .then(r => pako.ungzip(r as Uint8Array, { to: 'string' })) .then(JSON.parse) .then(frames => { this.frames = frames this.play() }) } }
-
預先生成18*13的箱子陣列,用visibility 操控而不是移除再放置可以更快哦
export class PlayBadAppleAction extends CgAction{ //... private pixels = {} private preparePixels() { let game = TWEventsUtil.getEventsGame(this.event.manager); const box0 = game.mapResource.getObjectResourceByName('box1') for (let i = 0; i <= 18; i++) { for (let j = 0; j <= 13; j++) { const obj = game.mapRenderer.buildObjectRenderer(box0, i, j) obj.visible = false game.addToObjectRoot(obj) // @ts-ignore obj.children[0].tint = '#ffffff' this.pixels[`${i},${j}`] = obj } } } }
-
用setInterval 的方式以30fps 播放影片 (原影片是30fps)
export class PlayBadAppleAction extends CgAction{ //... private play() { var i = 0 var k = setInterval(() => { this.render(this.frames[i]) console.log(i) i = i + 1 if (i >= this.frames.length) { clearInterval(k) } }, 1000 / 30) } }
-
-
將影片放到光暈裡面放,兩個問題:
- 光暈和原影片及音樂不同步
- 太少象素,根本看不出那個是bad apple
-
解決不同步問題
- 決定要改變開發方向,放棄gzip 二維布冧陣列方案,改為一直播放影片,即時讀取影片的象素來生成光暈的地圖物件
- stackoverflow 找到方法 https://stackoverflow.com/questions/12130475/get-raw-pixel-data-from-html5-video
- 建立多一個canvas,每次getAnimationFrame 將video tag內的影片渲染到canvas ,再用canvas 的getPixelData 取得它是不是黑色
- 成功做到在CG同人陣上播放Bad Apple並與光暈同步
-
解決太低清問題
-
以畫面縮放方式令畫面可以顯示更多地圖格子,從而令畫面更高清
-
計畫用180*130解象度,就是將畫面縮小10倍的簡單數學
-
看大神的Alt模組是怎製作畫面縮放
-
實測會看不到原本視野範圍以外的地板,但能看到視野以外的箱子
-
決定將原本的地板弄成黑色,木箱用作白色象素
-
卡爆,縮小3倍好了
-
效果就跟youtube上面的一樣了
-
-
做成能玩的Bad Apple - 大致方向就是想避開黑色
- 用光暈內建WASD來走太慢,決定讓鼠標避開黑色
- 找不到CG中得到鼠標在地圖上位置的方法
- 自己算鼠標位置,但需要校正
- 最後新增了兩個木箱,一個在(0,0),一個在(0,1),再分別取得它們在canvas 上面的座標,從而得出光暈戰記Bad Apple 在canvas左上角的象素座標和每個地圖格子的長和寛
const box = game.mapResource.getObjectResourceByName('box1') const obj0 = game.mapRenderer.buildObjectRenderer(box, 0, 0) // 生成一個箱子的pixi sprite並放於地圖的0,0;即藍點 const obj0 = game.mapRenderer.buildObjectRenderer(box, 1, 0) // 生成一個箱子的pixi sprite並放於地圖的1,0;即綠點 const left = obj0.getGlobalPosition().x // 地圖(0,0)距離canvas(0,0)(即紅點)left那麼多象素 const delta = obj1.getGlobalPosition().x - left // 箱子的長度和寛度 game.on('mousemove', (event: PIXI.InteractionEvent) => { // 滑鼠移動時會觸發的函式 // event.data.global 是鼠標在canvas 上面的座標(相對紅點多少象素) // 下面算出鼠標在哪個地圖格子上面並記下,待渲染時計算傷害 this.prevX = Math.floor((event.data.global.x - left) / size) this.prevY = Math.floor(event.data.global.y / size) })
- 每次getAnimationFrame 可以順道檢查鼠標所在的地圖格子
this.prevX, this.prevY
是不是黑色的,從而判斷要不要扣血 - 玩兩三次調整傷害力和血量來令遊戲變得更具挑戰性
-
加入開始頁面和任務完成就完工了~
-
嗯寫這個write up花的時間比做這個遊戲還要久