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 18] 制作更多的Debug工具 (1) - 连接期错误

>>:  程序精炼唯熟练尔:高阶函式 预设参数 high-order function, default parameter

Day 20:Dijkstra演算法

先前我们利用广度优先搜寻,找到图中两节点之间的最短路径,其中所谓「最短」是指「经过最少的边」。可是这...

Episode 5 - 输入与输出

如果画面太小或看不清楚,可移驾至 https://www.youtube.com/watch?v=...

Context Diagram 系统上下文图

系统上下文图 System Context Diagram (SCD) 是一种概念图的呈现,用於表达...

系统分析师养成之路-当责Accountability

因私人因素欧吉桑有一段时间没发文了,不知道有没有人期待我的新文章呢? 今天,我想跟大家分享的主题是【...

Shadow Element:条件控制元件的创建、消灭

<if> 条件控制 <if> 元素根据 test 属性中的评估值决定其下的元...