# Smol game dev journal ###### tags: `journal`, `開工` ## TODOs of soml-game > in ncu-fresh smol-game - Other - [x] find a better file structure - [x] add ts - Asssets - [x] load manager - [x] img pack - GamePlay ~~- [ ] use matter physic~~ - [x] many kind of obstacles - [x] obstacle factory - [ ] Input Manager - [ ] Sound manager - [ ] fx - [ ] bgm - UI - [x] new Button interface - [x] popup panel - Network - [x] login - [x] score API in backend - [x] ajax score - [ ] more anti-cheat ## 第一周 ### 7/5 開始尋找一些適合的 game framework / engine。 #### game engine - normal geame engine - > they originally designed to export pc games > But you still can export webGL or webASM version - unity - unreal - godot - > maybe I will try this one - using `gScript` and `C#` - web game lib - three.js - not a good option for game develop (more like a 3D rendering engine) - phaser - looks great - web game engine - play canvas engine - looks powerful, but the free version seems limited - but it also has a no-editor version - Gdevelop - > too simple 當然我也可以直接使用 Unity ,不過我想多做一點嘗試的喵。 因此便採用了 `Phaser`。 ### 7/6 嘗試將 Phaser import 制 vue 中,一直出問題,有夠頭痛。後來發現是因為自己對 vue 不熟而導致的問題,不能再一開始就 import Phaser,因為 Phaser 會在自己被 import 時在目標元素底下加入子元素 `canvas` 作為遊戲顯示的地方,而在 vue 的初始階段當然不允許你打亂他的 rendering。 - error: `navigator not found` - [solution blog](https://blog.csdn.net/jsxg2009/article/details/115244629) - 要在 `mounted` 階段 import Phaser 不然有些東西 vue 不允許 access - structure - [solution blog](https://connectshark.github.io/Phaser3/phaserInVue.html) #### import phaser ```bash= $ npm install phaser ``` ```vue # index.vue <template> <div id='phaser-container'></div> </template> <script> export default { mounted() { import('../game').then((module)=> { // window.Phaser = new MyGame(); window.Phaser = new module.default(); }) }, beforDestroy() { window.Phaser.destroy(); } } </script> ``` ```javascript= // index.js const config = {}; class MyGame extends Phaser.Game { constructor() { super(config); } } export default MyGame; ``` --- ### 7/7 我發現,大該是因為 `javascript` 特性的關西,在不同教學中同一種功能有千百種不同寫法,而且都很丑(`this` 滿天飛) 因此如果用 ts 去找,可以找到比較好看的寫法,剩下的只要我再把它用 js 來寫就好了 ### 7/8 0708 更: 我好想寫 typescript ## 第二周 因為實在是太丑了,在加上 Phaser 原碼也是用 typescript 寫的便把專案改成 typescript 了。 可以直接裝 nuxt 專用的 typescript modules 裝起來挺簡單的。 將遊戲物件繼承 `Phaser.GameObjects.Sprite` 後基本上寫法就很接近 Unity 了,一樣有 `preUpdate()` 可以 override,有直接的物理系統可以用。 圖片的引入會有一點問題,因為圖片路徑若放在 `/assets` 中會被 Nuxt 的 webpack 處理而導致 path 會無法預測。因此無法直接用固定的 path 去 load 圖片。 - 解法 1: 把圖片放在 `static` 中 - 解法 2: `import img1Url from './img/img1.png'` - import png,webpack 會將 import 近來的東西變成它後來實際的 url ### 7/13 研究了一下 OAuth2.0 - [是時候研究一下 OAuth 2.0](https://hackmd.io/5fjSLE8zQxiM1EIV4y9a7A) ### 7/14 找了很久很久,沒有一個好的 UI 系統。嘗試 import 一些 plugin 也一直失敗,便決定自己寫一套。 首先先從 Button 下手... 然後弄的一團糟,晚點再思考如何處理。 #### container phaser 沒辦法直接給 gameobject 上子 object 但是可以透過 container 來做到 - [container button with rxJS](https://blog.ourcade.co/posts/2020/container-button-phaser-3-typescript-rxjs/) - container 內的 object 座標位置會變成相對於 container ```typescript= class Panel extens Pahser.GameObject.Container { ... } let panelA = new Panel(...); let buttonA = new Button(...); panelA.add(buttonA); ``` #### Button 我想要讓 同一種 behavior 可以加在各個 button 上,並且多出 toggle event 像是: ```typescript= let buttonA = new Button({x:100, ...}); buttonA.addBehavior(new ImgShowOnHoverBehavior(img: 'thonk')); buttonA.addBehavior(new TintOnHoverBehavior(tintColor: 0x0c8763)); buttonA.addButtonEventListener(Button.Events.ON_TOGGLE_ON, () => { turnOnMusic(); }) ``` solution code here ##### Approach 1: :heavy_multiplication_x: ```typescript= abstract class ClickAbleComponent {..} class Button extends ClickAbleComponent {...} abstract ButtonDecorator extends ClickAbleComponent {...} class ShowImgOnMouseHover extends ButtonDecorator { private img; constructor(Button button) { button.container.add(img); button.addButtonListner(Button.Events.ON_HOVER_ENTER, this.handler); } private handler() {...} } let button1 = new Button(...); ShowImgOnMouserHover(button1, imgKey, ...); ``` :thinking_face: well, not really good... 想嘗試弄成像是 decorator pattern 的感覺,但是 event 的 emit 這樣用感覺怪怪的 ##### Approach 2: Button has list of behavior :heavy_multiplication_x: ```typescript= interface IButtonBehavior {...} interface IButtonBehaviorConfig { create: (Button but, config: any): IButtonBehavior; config: any } function AddImgShowOnHover(Button but, config): IButtonBehavior{ let behavior = function() {} as IButtonBehavior; behavior.img = but.scene.add.image(...); behavior.onMouseHover = function() { this.img.setVisiable(true); } return behavior; } class Button { private container; private buttonBehaviors; constructor(behaviorConfig) { this.buttonBehaviors.push( behaviorConfig.create(this, beaviorConfig.config)); } private HandleMouseHover() { for(behavior in this.buttonBehaviors) { if (behavior.onMouseHover == function) { behavior.onMouseHover(); } } } } let but = new Button( { create: AddImgShowOnHover, config: { imgKey: 'c8763' } },{ create: AddTintOnHover, config: { tintColor: 0x0c8763 } } ); ``` 嘗試將 behavior 傳入,但是這樣好像有點冗。 code 變得很不好寫 ##### Appraoch 3 :O: 把 Appraoch 2 加上 `init(but, scene, container)` 這樣我就不用一定要在 constructor 裡做將一些元件 add 進 button.container 裡 如此一來我只需要做很多不同 ButtonBehavior,在視情況選擇幾個插入我的 button 中就好。 ```typescript= interface IButtonBehavior { init: (Button but): void; onMouseHover: (): void; ... } class ShowImgOnHover implements IButtonBehavior { private img; constructor(imgKey, pos) { this.img = new Image(imgKey, pos); } // add class members to contaien and scene init(Button but) { but.scene.addExisting(this.img); but.container.add(this.img); } // well, dont't use it outside the Button public onMouseHover() {...} } class Button { private container; private buttonBehaviors; constructor(list[IButtonBehavior]) { for (behavior in list) { behavior.init(this); this.buttonBehaviors.push(behavior); } } public addBehavior(IButtonBehavior): this { } private HandleMouseHover() { for(behavior in this.buttonBehaviors) { if (behavior.onMouseHover == function) { behavior.onMouseHover(); } } } } let but = newButton(); but.addBehavior(new ShowImgOnHover(0, 0, 'c8763')) .addBehavior(new TintOnHover(0x0c8763)) .addBehavior(new addText(0, 0, '28px Arial')); ``` this one seems OK. 實際做起來後看起來跟上面差滿多的但是總體來說差不多。 ### 7/15 嘗試讓網站 work,導入了 docker ## 第三周 逐步完整 button.ts 的內容 同時改進 角色 移動物理 加入 scene transition #### 7/21 發現一些可以對 img 做優惠的 module,叫做 `@aceforth/nuxt-optimized-images` 是把各種 image optimize 的方法集成在一起同時包裝成可以在 nuxt 中方便做設定的 module 嘗試後,確實可以增進一點點圖片載入速度 ## 第四周 ### 7/26 我想要遊戲內生成不同的 Obstacle => `ObstacleFacotry` :::spoiler 思考草稿 #### solution 1 ```typescript= class ObstacleFactory { create(currentObj: Obstacle) { currentObj.activate(true); let deadZoneAfterCurrent = currentObj.getDeadZone(this.globalData.worldSpeed).back; let nextObj = this.getRandomObstacle(); let deadZoneBeforeNext = nextObj.getDeadZone(this.globalData.worldSpeed).front; let totalWaitTime = deadZoneAfterCurrent + this.getFreeSpace() + deadZoneBeforeNext; if (this.gachaForItem()) { // create one; above current obstacle or in the freeZone let item = this.createItem(); if (this.isThisItemAboveObstacle(item, currentObj)) { } else { } } // obj1 <-obj1.deadZone.back-> <-freeZone-> <-obj2.deadZone.front-> obj2 this.scene.time.addEvent({ delay: totalWaitTime, callback: this.create args: [nextObj] }); return; } getRandomObstacle() {...} } class GameScene extends Phaser.Scene { public gobalStatus = { score: 0, speed: initSpeed, } create() { this.factory = new ObstacleFactory({ scene: this, globalData: this.globalData, }); this.factory.start(); } } ``` ```typescript= // How to use Obstacle class Obstacle extends Phaster.GameObject.... { } class ObstacleFactory { obstacleList = [ {val: obstacle1, weight: 0.1}. {....} ] itemList = [ {val: item1, weight: 0.1}. ... ] initConfig = { x: 1600, y: ground, } getRandomObstacle() { // how do I } } const createNormalObstacle = () => { return new normalObstacle1({ // some config }) } class normalObstacle1 extends Obstacle { } ``` ::: </br> 重新架構 obstacle.ts 的樣貌。將 obstacle 改成 abstract class,與 player 的交互行為由子類別來實現。 ```typescript= abstract Obstacle extends Phaser.GameObjects.Sprite { protect abstract onOverlap(self: Obstacle, player: Player): void; } Coin extends Obstacle { protect onOverlap(self, player) {...} } ``` ### 7/28 img-optimization 的東西有部份是 npm install 會做 build,產出 binary file,而這讓 docker container 內部環境罩成我不知道啥的影響導致 npm start 不起來... 所以 dev 版暫時將 img-optimization 關掉了 跟學校的人 demo ### 7/30 美術的圖片大量產出中,如果要一張一張圖片手動 import 我真的會很想死。 因此便使用 texture packer,製造 sprite sheet,可以一次 import UI 改成以 sub-scene 的方式 overlap 在 GameScene 上面,由於都是 scene 便可以用 Phaser.scene 的 method 來操作 UI 狀態 (`pause`, `stop`, `lauch`) ### 7/31 > ~~tribes of midgard 滿好玩的,不過很快就有點疲勞了。~~ ## 第五周 ### 8/2 新的美術圖如果障礙物用方形 collider 會有明顯的怪異感。看來是時候要導入 matter.js 了。 對整體做一些微調 ### 8/3 今天不想工作便來整理筆記 對於 UI panel 終於有新的想法了等等可以記起來 有 grid 應該就不用 row 與 column 了 ```graphviz digraph { node [shape="record"] com [label="{UIComponent| setPos()}"] panel [label="{Panel| layout: LayoutSetter\n addOne(component)}"] button [label="{Button| }"] butGroup [label="{ButtonGroup| }"] grid [label="{GridLayout| }"] row [label="{RowLayout| }"] lay [label="{LayoutSetter| set(Array\<UIComponent\>): UIComponent}"] panel -> com [label="implement"] panel -> lay [label="has"] button -> com grid -> lay [label="implement"] row -> lay butGroup -> panel [label="extend"] } ``` > 8/4 updated ```typescript= // imagenations let butGroup = new ButtonGroup({x, y, image, behavior, '3x1',}); butGroup.at(0).addEventHandler(event, () => {...}) butGroup.at(1).addEventHandler(event, () => {...}) let panel = new LayoutSetter('1x2', padding: 12); panel.at(0).add(img, text, ...); panel.at(1).add(butGroup); ``` ```htmlembedded= // the result will be like this <div> <div id='panel'> <img/> <text></text> </div> <div id='butGroup'> <button></button> <button></button> </div> </div> ``` ``` /UI /button ... /panel basePanel.ts ButtonGroup.ts /layoutSetter GridSetter.ts UIComponent.ts type.d.ts ``` ```typescript= interface UIComponent { margin; getWidth; // include margin getHeight; getCoor(anchor); } interface LayoutSetter { container: Phaser.container; paddingX; paddingY; addOne(UIComponent); addMany([UIComponent]); } class Panel implements UIComponent { layoutSetter; constructor(layoutSetter) {...} addOne() { this.layoutSetter.addOne(); } } class Grid implements LayoutSetter {...} // init many Button with same config and then align them with a layoutSetter class ButtonGroup extends Panel { constructor(numbersOfBut, butConfig, layOutSetter) {} } ``` 研究,參考了一下各種 UI 的寫法,最後採以類似 Unity 中 UI 的架構: UIComponent 中有 layout 相關設定可以設定子物件的排列。 不過為了配合 Phaser 的 container 設計,便多做一個 panel 物件作為成載多個子物件集合的載體 > 好想打遊戲 > apex season 10 更新了耶 ### 8/5 終於寫完新的 UI 功能了,接下來就是把遊戲中暫時用的 UI 換掉了。 好像離 deadline 有點近了,感覺要做不完了 QQ 到底為什麼把東西放到 container 內後,東西的座標會變成以中心... 解法:自己做一個 setPos 會自帶 -0.5 ### 8/6 我到底為什麼一直在寫輪子 ### 8/7 對 scene 做分檔,因該不用再多做一個 scene manager 了,直接個別做 import 等事就好。 弄好 scene 芬黨後,就直接硬幹 UI 用自己的工具寫 UI 開心 :) TODO: GameDataManager 這邊有全部 player data .item { unlocked? amount? } .highestScore .leaderboard ### 8/8 > 問題:UIButton.Button 是基於 phaser.zone 來做點擊判定而非圖片本身,因此在 button 圖片 被其他圖片蓋過去時, button 的 hitArea 仍然可以判定,因此導致 pointer click through the image > note: img.setInteractive() 可以蓋掉 phaser.zone 的 hitarea 判定 > 因此我需要在一些圖片上加上 setInteractive 來避免 pointer 穿過圖片抵達我的 button.hitArea Quiz 中的 option button 需要一個新的 layoutSetter 我想要讓 QuizUI 有健全一點的規劃,而不是全部塞在一起 Or maybe just code everything in same scene class is not that bad? ```typescript= class QuizUI: Phaser.Scene { constructor() { super(key); } create() { this.quizManager = new QuizManager; this.itemBar = new ItemBar(this.quizManager, timer, quizPanel); // get item amout from GameDataManager this.timer = new Timer(this.quizManager) this.quizPanel = new Quiz(this.quizManager) let mainPanel = lineUp(timer, quiz.question, quiz.options); lineUp(itemBar, mainPanel); } } class QuizManager() { quizScore: number; getRandomQuiz() {} correctAction() {} wrongAction() {} } class ItemBar { bar: AlignPanel; constructor() { this.itemStatus = GameDataManager.itemStatus; // disabel some button if item.amout <= 0 } private itemTimeUp() { // need timer this.timer.increseTime() } private itemGetRidOfWrongOption() { // need refresh quizPanl this.quizPanel.eliminateOneWrongOption(); } private itemScoreX2() { // need the owner of correctAtion() this.quizManager.setScoreX2(); } private itemChangeQuiz() { // need getRandomQuiz and refresh quizPanel this.quizPanel.changeQuiz(); } } ``` ### 8/11 QuizUI 越塞越丑了,但是我不想管了 差不多該來設計後端以及資料庫了 > 跟其他人的 db 有要做關聯嗎? > 算了還是不要,感覺就很麻煩 > ```typescript= // backend PlayerModel player { user: User; // reference to UserModel Maybe? studentNumber: number, // main key nickname: string, highestScore: number, itemAmount: { moreTime: number, showWorngAns: number, x2Score: number, chnageQuiz: number, }, gameCompeletd: booleam } ``` `GameDataManager` 目前越來越亂了 或許各資料個體分開有自己的 logic 會比較方便 將 `GameDataManager` 改名為 `GlobalDataManager`,其中存了各種 `xxDataManager` ### 8/12 我覺的我要完蛋了 先把 道具,高分,網路紀錄部份完成 再來是血量顯示,多種障礙物,角色跑步動畫 ```typescript= GlobalDataManager { } GameWorldDataManger { startGame() pauseGame() stopGame() gainScoreAnima gainScore updatePlayerData() { PlayerDataMager.update } ... } PlayserDataManager { get save update } asd QuizDataManager { }ao6 ``` ### 8/13 意外的大家分數很低,所以 `backend secuority` 就先不加 先做: 手機板支援(敷衍一下就好,認真做起來很麻煩,排版什麼的 再來:排行榜 -> random-pause-text -> rule-man -> set-nick-name -> backend-sec ### 8/14 休息一天 ### 8/15 先來修排行榜,我挺想加入 nick name 的 加入 wasm ### 8/16 C 的 wasm 好難弄,一直失敗 改用 rust 看看 [rust,wasm](https://dev.to/krzysztofkaczy9/webassembly-rust-typescript-project-setup-4gio) rust 成功了! Rust Goooooooood!!!! 我要加入 Rust 教了 後來證實 wasm 會影響 webpack 然後影響其他分頁,導致奇怪的 bug ### 休息一週 被 wasm 弄到心態炸裂 找不到任何有關 nuxt+wasm wasm 又好像有不同種 webpack 也有不同種 再加上還有可能有版本問題 ### 9/2 - **被要求要有「累計分數」** 有點嚇到了,不過程式改起來應該很快 把之前的 code 用 cherry pick merge 到 master 留下 wasm 的部分不 merge > (還好之前有開始養成一個 commit 只改一個東西的習慣,wasm commit 內只有 wasm 相關的修改) 如此就先把 wasm 問題放一旁,先暫時用爛爛的仿偽吧 只要一點簡單的修改就可以把現成的分數計算機制加上「累計分數」制了。 問題在於:已經玩一段時間的玩家怎麼辦 -> 寫一個腳本插到 server 裡直接掰一個「累計分數」給所有人...