iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Modern Web

Javascript 從寫對到寫好系列 第 13

Day 13 - OOP 初探 (3) - 實戰地圖遊戲

前言

跟 FP 一樣,OOP 到目前已經第三天了,我們來點實戰吧!

今天的實戰很特別啊,基本上是工作派不上用場的程式,但因為我不知為何靈光一閃,覺得寫這種東西很好玩,所以就剛好用今天試試看吧!

當然,多少還是會用到一些 OOP 的概念進去,可以邊寫邊體會哦!

實戰 - 地圖遊戲超基礎版

印象中在我小時候那個年代,沒什麼遊戲可以玩,最經典的就能玩很久了。而像是貪食蛇小精靈(Pac-Man) 都是玩不膩的經典。

像這兩個遊戲都有個共通點,就是會有個場景,裡面會有個角色可以操縱,最單純的操作就是上下左右嘛!

但只有一個角色太無聊了,所以會有一些附加的遊玩價值:要嘛有個獎品放在場景中,吃到就可以加分;要嘛反過來,放一隻會移動的鬼在場景裡面跑,被它碰到就死掉。

今天要實作一個地圖遊戲的半成品,也就是只做到遊玩價值以前的部分XD,直接來看圖:

這張 GIF 我自己錄起來都覺得很好笑XD

但真的相信我,寫的過程中,即便畫面上沒有任何獎勵,只要你寫的場景如預期畫出來,按方向鍵可以操作角色隨便走來走去,光這樣就會很嗨了!((好啦只有我嗨

定義這個程式中有哪些物件

OOP 以物件(object)為思考主體,所以最重要的就是先定義出會有哪些物件:

  • 場景物件(Scene)
  • 角色物件(Actor)

嗯... actor 是演員,不過我們暫且用這個詞,來稱呼被我們擺在場景中的物體

Scene 類別

constructor,接收兩個參數(widthheight),用來定義出這個場景的長寬。

  • map:透過 widthheight 計算出一個二維陣列,用來存放目前場景的長相
  • actors:存放這個場景中的所有 Actor 實體物件
class Scene {
    constructor(width, height) {
        const map = new Array(width).fill('-');
        for (let i=0; i < map.length; i++) {
            map[i] = new Array(height).fill('-');
        }
        this.map = map;
        this.width = width;
        this.height = height;
        this.actors = [];
    }
}
  • register:將 Actor 實體物件放到 actors 裡面
  • unregister:將指定的 Actor 實體物件移除
class Scene {
    
    // ...

    register(actor) {
        this.actors.push(actor);
        this.map[actor.x][actor.y] = actor.sign;
        this.draw();
    }

    unregister(actor) {
        const index = this.actors.indexOf(actor);
        this.actors.splice(index, 1);
        this.map[actor.x][actor.y] = '-';
        this.draw();
    }
}
  • changeActorPosition:將 Actor 放到新的位置上
class Scene {
    
    // ...

    changeActorPosition(actor, x, y) {
        // 舊的移除
        this.map[actor.x][actor.y] = '-';
        // 新的補上
        this.map[x][y] = actor.sign;
        
        this.draw();
    }
}
  • draw:將目前的二維場景(map)畫在 console 上
class Scene {
    
    // ...

    draw() {
        let display = '';
        for (let i = 0; i < this.map.length; i++) {
            for (let j = 0; j < this.map[i].length; j++) {
                display += this.map[j][i];
            }
            display += '\n';
        }
        console.clear();
        console.log(display);
    }
}

Actor 類別

constructor,接收四個參數(scenexysign),分別代表放置的場景、x位置、y位置與代表符號。

初始化就會把自己註冊在指定的場景(Scene)中。

class Actor {
    constructor(scene, x, y, sign) {
        this.scene = scene;
        this.x = x;
        this.y = y;
        this.sign = sign.substr(0, 1);
        scene.register(this);
    }
}
  • exit:離開這個場景(Scene)
class Actor {

    // ...

    exit() {
        this.scene.unregister(this);
    }
}
  • moveTo:移動到指定的 x,y 位置
class Actor {

    // ...

    moveTo(x, y) {
        const { width, height } = this.scene
        if (x < 0 || y < 0 || x >= width || y >= height) {
          return
        }
        this.scene.changeActorPosition(this, x, y);
        this.x = x;
        this.y = y;
    }
}

畫圖囉!

把以下程式碼複製貼到 console 按下 Enter 試試看:

class Scene {
    constructor(width, height) {
        const map = new Array(width).fill('-');
        for (let i=0; i < map.length; i++) {
            map[i] = new Array(height).fill('-');
        }
        this.map = map;
        this.width = width;
        this.height = height;
        this.actors = [];
    }

    register(actor) {
        this.actors.push(actor);
        this.map[actor.x][actor.y] = actor.sign;
        this.draw();
    }

    unregister(actor) {
        const index = this.actors.indexOf(actor);
        this.actors.splice(index, 1);
        this.map[actor.x][actor.y] = '-';
        this.draw();
    }

    changeActorPosition(actor, x, y) {
        // 舊的移除
        this.map[actor.x][actor.y] = '-';
        // 新的補上
        this.map[x][y] = actor.sign;

        this.draw();
    }

    draw() {
        let display = '';
        for (let i = 0; i < this.map.length; i++) {
            for (let j = 0; j < this.map[i].length; j++) {
                display += this.map[j][i];
            }
            display += '\n';
        }
        console.clear();
        console.log(display);
    }
}

class Actor {
    constructor(scene, x, y, sign) {
        this.scene = scene;
        this.x = x;
        this.y = y;
        this.sign = sign.substr(0, 1);
        scene.register(this);
    }

    exit() {
        this.scene.unregister(this);
    }

    moveTo(x, y) {
        const { width, height } = this.scene
        if (x < 0 || y < 0 || x >= width || y >= height) {
          return
        }
        this.scene.changeActorPosition(this, x, y);
        this.x = x;
        this.y = y;
    }
}

const s = new Scene(10, 10);
const a = new Actor(s, 5, 5, 'A');
const b = new Actor(s, 2, 6, 'B');

這邊我們除了將 class 定義好,也在最下面使用 new 將類別實體化,變成三個物件。

  • s:場景物件,寬高都是 10
  • a:演員物件,以 s 為場景,位置落在 5,5,在地圖上用 A 來代表
  • b:演員物件,以 s 為場景,位置落在 2,6,在地圖上用 B 來代表

執行結果

讓角色動起來!

我想不太到什麼好的方式,所以暫時先把 keyup 事件綁在 window 物件上,因為沒辦法直接在 console 按上下左右,所以務必要先點一下網頁的畫面,才可以開始按方向鍵哦!

另外,就像第二行註解寫的,因為 Actor 可能有很多個,我暫時先抓場景內第一個 Actor 來進行上下左右的移動操作,這個完全可以自行修改哦!

window.addEventListener('keyup', e => { 
    // 預設操控第一隻角色
    const actor = s.actors[0];
    switch (e.code) {
        case 'ArrowUp':
            actor.moveTo(actor.x, actor.y-1);
            break;
        case 'ArrowDown':
            actor.moveTo(actor.x, actor.y+1);
            break;
        case 'ArrowLeft':
            actor.moveTo(actor.x-1, actor.y);
            break;
        case 'ArrowRight':
            actor.moveTo(actor.x+1, actor.y);
            break;
        default:
            break;
    }
});

最終版

可以直接整串複製貼到 console 上 Enter,接著點一下網頁本身,就可以操作上下左右囉!(但目前走到牆壁會當掉哦XD)

class Scene {
    constructor(width, height) {
        const map = new Array(width).fill('-');
        for (let i=0; i < map.length; i++) {
            map[i] = new Array(height).fill('-');
        }
        this.map = map;
        this.width = width;
        this.height = height;
        this.actors = [];
    }

    register(actor) {
        this.actors.push(actor);
        this.map[actor.x][actor.y] = actor.sign;
        this.draw();
    }

    unregister(actor) {
        const index = this.actors.indexOf(actor);
        this.actors.splice(index, 1);
        this.map[actor.x][actor.y] = '-';
        this.draw();
    }

    changeActorPosition(actor, x, y) {
        // 舊的移除
        this.map[actor.x][actor.y] = '-';
        // 新的補上
        this.map[x][y] = actor.sign;

        this.draw();
    }

    draw() {
        let display = '';
        for (let i = 0; i < this.map.length; i++) {
            for (let j = 0; j < this.map[i].length; j++) {
                display += this.map[j][i];
            }
            display += '\n';
        }
        console.clear();
        console.log(display);
    }
}

class Actor {
    constructor(scene, x, y, sign) {
        this.scene = scene;
        this.x = x;
        this.y = y;
        this.sign = sign.substr(0, 1);
        scene.register(this);
    }

    exit() {
        this.scene.unregister(this);
    }

    moveTo(x, y) {
        const { width, height } = this.scene
        if (x < 0 || y < 0 || x >= width || y >= height) {
          return
        }
        this.scene.changeActorPosition(this, x, y);
        this.x = x;
        this.y = y;
    }
}

const s = new Scene(10, 10);
const a = new Actor(s, 5, 5, 'A');
const b = new Actor(s, 2, 6, 'B');

window.addEventListener('keyup', e => { 
    // 預設操控第一隻角色
    const actor = s.actors[0];
    switch (e.code) {
        case 'ArrowUp':
            actor.moveTo(actor.x, actor.y-1);
            break;
        case 'ArrowDown':
            actor.moveTo(actor.x, actor.y+1);
            break;
        case 'ArrowLeft':
            actor.moveTo(actor.x-1, actor.y);
            break;
        case 'ArrowRight':
            actor.moveTo(actor.x+1, actor.y);
            break;
        default:
            break;
    }
});

TODO

雖然我自己覺得開發過程滿好玩的,不過當然這還是個半成品,所以 bug 如下:

  • 角色走到牆壁就當掉了
  • 角色撞到別的角色就吃掉了(lol)

如果要繼續完善它的遊戲性,可以往這幾個方向思考:

  • 使用 extends,也就是子類別的用法,去擴充 Actor,創造不同類型的角色(比如說每隔兩秒會瞬間移動的角色)
  • 增加遊玩價值,放個獎勵或鬼在場景中,加入計分系統等

結語

其實今天這個這麼跳 tone 的範例,是大概在晚上八點多才想到的XD,所以很多思考跟實作都沒有最佳化。(歡迎留言建議拜託!!)

但如果要做為一個 OOP 的小練習,我想這個範例還是可以體會到如何以物件的角度來思考,並且設計物件之間的互動,如何影響屬性等。

決定寫這個單純就是一個好玩,畢竟自己會喜歡玩前端,就是因為有很多畫面可以看,我的每一次小改動,都會讓程式創造出不同有趣的畫面,這點讓我覺得很有動力繼續寫下去!

真實世界的每一步
都是電腦眼中的
0 與 1


上一篇
Day 12 - OOP 初探 (2) - Class
下一篇
Day 14 - Asynchronous 非同步核心
系列文
Javascript 從寫對到寫好30

1 則留言

0
TD
iT邦新手 4 級 ‧ 2021-09-29 09:34:34

這讓我想起我第一次學程式的時候,就是用 C 寫類似的小遊戲,好令人懷念啊~~

先快速來個撞牆不會報錯的 update XD

class Scene {
    constructor(width, height) {
        ...
        this.width = width
        this.height = height
    }
    ...
}
class Actor {
    ...
    moveTo(x, y) {
        const { width, height } = this.scene
        if (x < 0 || y < 0 || x >= width || y >= height) {
          return
        }
        this.scene.changeActorPosition(this, x, y);
        this.x = x;
        this.y = y;
    }
}
ycchiuuuu iT邦新手 5 級 ‧ 2021-09-29 13:04:11 檢舉

TD 你真的是大家的小精靈欸~超貼心的!
我晚點再來更新上去!

我要留言

立即登入留言