Screeps 浅谈游戏中的原型拓展

Screeps 浅谈游戏中的原型拓展_第1张图片
screeps 系列教程

简介

在游戏的教程中,我们了解到可以通过游戏给定的基本 api 如Game.creeps Game.spawns等获取游戏对象,然后设计一层层的封装,并将这些游戏对象作为参数传递给封装好的函数,进而完成我们的逻辑。

但是这么做有一点缺陷,假如我们代码封装做的比较多的话,就会出现游戏对象被一层层的传递下去,如下:

// 获取游戏对象
const creep = Game.creeps['creep1']

// 传递给任务分发函数
work(creep)

// work 函数里传递给指定的角色函数
upgrader(creep) 

// upgrader 函数里再传递给状态更新函数
updateState(creep)

…

可以看到 creep 对象被一步步的传递给不同的函数,这样的设计会增加参数的个数,不方便理解,进而增加代码的维护成本。

那么有没有一种更简洁的方式来避免这种设计呢,答案就是本文要讲的 游戏原型拓展

什么是原型?

js 中的继承是通过原型链实现的,每一个对象都有一个链接指向了另一个对象,被指向的对象就称为前者的原型

比如 screeps 中的每一个creep的原型都是Creep注意区分这两者的大小写,我们常用的.moveTo().harvest()就是在Creep原型上定义的。

而如果我们尝试调用某个对象上不存在的属性时,它就会去它的原型上去查找,如果原型对象上有的话,就会执行原型上的方法。这也就是我们可以进行原型拓展的根本。

Screeps 浅谈游戏中的原型拓展_第2张图片

简单的例子

刚才讲了一大堆概念,可能对没有系统学过 js 的同学有点难以理解。接下来就举一个简单的例子。我们在游戏代码入口处进行如下定义:

module.exports.loop = function () {
    Creep.prototype.sayHello = function () {
        console.log('hello world!')
    }
}

然后随便抓个正在干活的creep执行下面代码:

Game.creeps['creep name'].sayHello()

然后你就会发现控制台输出了hello world!。完美!我们成功的修改了Creep,让每一个creep都获得了sayHello方法。接下来我们讲解一下上面的例子:

我们在Creep.prototype上定义了sayHello,并把一个函数赋值给它。这里的Creep.prototype就是Creep原型对象,当creep上找不到sayHello方法的时候就会去prototype上寻找,正好就找到了我们刚才定义的方法。

很好,我们来更近一步,把上面的代码替换如下:

Creep.prototype.sayHello = function () {
    this.say(`我的名字是${this.name}`)
}

然后再找一个creep执行sayHello方法,就会出现如下情况:

Screeps 浅谈游戏中的原型拓展_第3张图片
image.png

creep他自己说话了!没错,在原型对象上定义的方法中,你可以使用this访问到其他所有的属性!这么一来,我们就可以将常用的方法挂载到creep来简化我们的代码结构,比如下面这样:

// 建设房间内的建筑工地
Creep.prototype.buildStructure = function () {
    const targets = this.room.find(FIND_CONSTRUCTION_SITES)
    // 找到就去建造
    if (targets.length > 0) {
        if(this.build(targets[0]) == ERR_NOT_IN_RANGE) {
            this.moveTo(targets[0])
        }
    }   
}

这样我们只需要执行creep.buildStructure()就可以让creep自己跑到建筑工地然后干活了。至于原型拓展的用处还有很多,这里不再深入,有兴趣的可以参考官方的这篇文档 《screeps modifying-prototypes》 来了解更多用法。

接下来,我们讲一下如何优雅清晰的规模化拓展原型。

更好的代码结构

和其他的逻辑代码一样,我们总不能把所有的原型拓展代码都写到主入口module.exports.loop里吧,接下来就分享一种比较清晰的代码结构。当然,如果你有自己的想法的话大可不必按照我的来做。

首先假设我们想拓展Creep原型,那么可以新建一个名为mount.creep.js的文件,其内容如下:

// 将拓展签入 Creep 原型
module.exports = function () {
    _.assign(Creep.prototype, creepExtension)
}

// 自定义的 Creep 的拓展
const creepExtension = {
    // 自定义敌人检测
    checkEnemy() { 
        // 代码实现...
    },
    // 填充所有 spawn 和 extension
    fillSpawnEngry() { 
        // 代码实现...
    },
    // 填充所有 tower
    fillTower() {
        // 代码实现...
    },
    // 其他更多自定义拓展
}

我们将所有的自定义属性都存放到creepExtension对象中,然后使用lodashassign方法将其一次性全部签入到Creep原型中。

然后我们再新建mount.js文件,这个文件引入mount.creep.js并将封装成一个单独的入口函数:

const mountCreep = require('./mount.creep')

// 挂载所有的额外属性和方法
module.exports = function () {
    if (!global.hasExtension) {
        console.log('[mount] 重新挂载拓展')
        global.hasExtension = true
        
        mountCreep()
    }
}

注意这里做了一次检查,当global.hasExtension (这个值同样也是我们自定义的) 不存在时我们就重新挂载,不然什么都不做。为什么要进行这个检查呢?因为 挂载完成之后,我们自定义的属性是具有持久性的。这也就意味着,我们只需要挂载一次就可以在之后一直使用,就不需要每个tick都挂载一遍从而造成 cpu 损耗了。

但是挂载完拓展之后并不是一劳永逸,每当我们有代码提交或者是游戏清除缓存后,我们挂载的拓展就会被清除掉。这时候就需要重新进行挂载。加上这个检查之后,一旦global.hasExtension不存在了,那就说明其他的拓展也大概率的被清掉了,这时候统一进行重新挂载就好了。

并且加上这一层封装之后,如果我们再有新的拓展,比如mount.flag.js或者mount.room.js,我们也都可以再这个文件里统一引入、统一挂载。如下:

const mountCreep = require('./mount.creep')
const mountFlag = require('./mount.flag')
const mountRoom = require('./mount.room')

module.exports = function () {
    if (!global.hasExtension) {
        console.log('[mount] 重新挂载拓展')
        global.hasExtension = true

        mountCreep()
        mountFlag()
        mountRoom()
        // 其他更多拓展...
    }
}

最后,我们再main.js中引入mount.js并执行就好了:

const mount = require('./mount')

module.exports.loop = function () {
    // 挂载所有拓展
    mount()
}

这里放一张拓展挂载的流程图来方便理解。

Screeps 浅谈游戏中的原型拓展_第4张图片
拓展挂载流程图

总结

本文简单介绍了 screeps 中如何拓展原型,只需要在对应原型的prototype属性上添加新的属性即可,添加完成后所有由该原型派生出的对象都可以调用该属性,并且在 .screeps - api 中提到的原型都可以通过这种方式进行拓展。

最后我们介绍了一种封装拓展的代码结构,做到了拓展的统一挂载,并且方便了日后的维护。想要了解更多内容欢迎访问 《Screeps 文集》!

你可能感兴趣的:(Screeps 浅谈游戏中的原型拓展)