《判若莱宇宙》游戏开发介绍

游戏简介

开发背景

小学起一直有玩4399造梦西游系列游戏,后来到造梦西游4,造梦西游5越做越低龄化,动画节奏,游戏核心都背离了前三个版本,也不耐玩了,再到造梦西游手游,氪金气氛太冲。而我当时也想开发一款比较复杂的游戏,就想写一个自己的“造梦西游”。

游戏内容

判若莱宇宙是我主导开发的一款基于Pixi.Js图形库的2D横版Web小游戏(并未正式上线)。游戏包含完备的等级,装备,技能,天赋系统。游戏功能主要继承事件驱动的思想通过各种事件注册和监听来实现,对于游戏角色,每个功能都有对应的控制器代码进行分工(如技能、状态、动画控制器),对于角色各种属性值,通过事件监听在必要时刻(如更换装备,某些技能的被动增益)进行角色属性的重新计算,角色状态分为简单状态(走、跑)与复杂状态(中毒、眩晕),对应不同的运作方式。

团队组成

整个游戏程序由我全部完成,美术素材80%由我完成,也感谢54dyourc(代称)为我提供了若干自己原创的美术素材。

54dyourc原创的月球关卡boss“悦兔”素材:
悦兔
游戏背景音乐由本人高中好友Daniel(代称)原创提供,限于当前在开发阶段并没有开启音效。

游戏精彩截图分享

选择角色,炫酷光效
选择角色,炫酷光效
多样装备,随时换装
多样装备,随时换装
技能搭配,炫酷连招
技能搭配,炫酷连招
双人游戏,豪华大招
双人游戏,豪华大招
羽翼技能,其人之道
羽翼技能,其人之道
因为特别喜欢LOL游戏,模仿着写了天赋系统
天赋
游戏中特化了时间概念,某些技能或天赋可以操控时间!绯红之王,删除时间
角色孤影剑客大招,魔影杀,万剑归宗
万剑归宗
用心雕刻代码,完成关卡的归来之门
完成关卡的归来之门
合成系统,打造你的专属羽翼
在这里插入图片描述

游戏实现简介

渲染引擎

游戏开发中,我曾尝试用原生dom,canvas2d去实现动画,结果总是性能达不到,严重卡顿。在若干次探索与重构后,最终确定了以pixi为核心的渲染引擎,并实现游戏逻辑与渲染分离。

游戏机制

游戏的逻辑推进由事件实现,由时间来临事件和玩家操作事件这两种基础事件推动游戏进行。在事件模块,编写了ItemEventDispatcher、ItemEvent两个类(类似于EventEmitter、Event),实现了特殊事件监听,即时注销handler,多层监听。

/**
 * 事件委托器
 * 事件在生命周期上有永久和关卡两种
 * 事件在触发条件上分为字符判定和函数判定
 * 事件在执行次数上分为每次触发和单次触发
 */
export class ItemEventDispatcher {
    /**
     * handler的索引
     */
    _index = 0

    commonHandlers = {}
    /**
     * @type [import("./eventName").EventComt]
     */
    complexHandlers = []

    /**
     * @return {import("./eventName").EventComt }
     * @param {import("./eventName").EventName | Function | ItemEvent} e 
     * @param {(this:ItemEventDispatcher,e:ItemEvent)=>void} handler 
     */
    on(e, handler = undefined, always = false) {
        if (handler) {
            if (typeof e != 'function') {
                return this.addCommonHandler(e, handler, always);
            } else {
                return this.addComplexHandler(e, handler, always);
            };
        } else {
            let event = (e instanceof ItemEvent) ? e : new ItemEvent(e);
            this.emit(event);
        }
    }
    /**
     * @return {import("./eventName").EventComt }
     * @param {import("./eventName").EventName | Function} e
     * @param {(this:ItemEventDispatcher,e:ItemEvent)=>void} handler 
     * @param {Boolean} always
     */
    once(e, handler, always = false) {
        if (typeof e != 'function') {
            return this.addCommonHandler(e, e => handler(e) || true, always);
        } else {
            return this.addComplexHandler(e, e => handler(e) || true, always);
        };
    }
    /**
     * @return {import("./eventName").EventComt }
     * @param {import("./eventName").EventName} type
     * @param {(this:ItemEventDispatcher,e:ItemEvent)=>void} handler 
     */
    addCommonHandler(type, handler, always = false) {
        if (!this.commonHandlers[type]) {
            this.commonHandlers[type] = [];
        }
        let comt = {
            index: this._index++,
            always: always,
            type: type,
            handler: handler,
            once: false
        };
        this.commonHandlers[type].push(comt);
        return comt;
    }
    /**
     * @return {import("./eventName").EventComt }
     */
    addComplexHandler(checker, handler, always = false) {
        let comt = {
            index: this._index++,
            always: always,
            handler: handler,
            checker: checker
        };
        this.complexHandlers.push(comt);
        return comt;
    }
    _emitingEvents = []
    /**
     * 触发一个事件
     * @param {ItemEvent} event 
     */
    emit(event) {
        this._emitingEvents.unshift(event);
        if (this.commonHandlers[event.type]) {
            let handlers = this.commonHandlers[event.type];
            let i = -1;
            for (let handler of handlers) {
                i++;
                if (!handler) continue;
                if (handler.handler(event)) {
                    handlers[i] = undefined;
                }
            }
            handlers = handlers.filter(handler => handler);
        };
        let complexCache = this.complexHandlers;
        let j = -1;
        for (let handler of complexCache) {
            j++;
            if (!handler) continue;
            if (handler.handler(event)) {
                handlers[j] = undefined;
            }
        }
        complexCache = complexCache.filter(handler => handler);
        this._emitingEvents.shift();
    }
    /**
     * 清除所有非永久的handler
     */
    refreshHandler() {
        for (let en in this.commonHandlers) {
            this.commonHandlers[en] = this.commonHandlers[en].filter(comt => comt).filter(comt => comt.always);
        }
        this.complexHandlers = this.complexHandlers.filter(comt => comt.always);
    }
    /**
     * 清除所有handler
     */
    refreshAbsolute() {
        this.commonHandlers = {}
        this.complexHandlers = [];
    }
    /**
     * @param {import("./eventName").EventComt} _comt 
     */
    removeHandler(_comt) {
        if (_comt.checker) {
            this.complexHandlers = this.complexHandlers.filter(comt => comt.index != _comt.index);
        } else {
            // this.commonHandlers[_comt.type] = this.commonHandlers[_comt.type].filter(comt => comt.index != _comt.index);
            this.commonHandlers[_comt.type].splice(this.commonHandlers[_comt.type].indexOf(_comt), 1);
        }
    }
    /**
     * 
     * @param {import("./eventName").EventName} type 
     */
    removeCommonHandler(index, type = null) {
        if (type) {
            this.commonHandlers[type] = this.commonHandlers[type].filter(comt => comt.index != index);
        } else {
            for (let en in this.commonHandlers) {
                this.commonHandlers[en] = this.commonHandlers[en].filter(comt => comt.index != index);
            }
        }
    }
    removeComplexHandler(index) {
        this.complexHandlers = this.complexHandlers.filter(comt => comt.index != index);
    }
}
export class ItemEvent {
    /**
     * @type {import("./eventName").EventName}
     */
    type
    value
    from
    /**
     * 
     * @param {import("./eventName").EventName} type 
     */
    constructor(type, value, from) {
        this.type = type;
        this.value = value;
        this.from = from;
    }
}

独特的时间概念

游戏中每个具有时间的对象(如角色)都有一个timespeed属性,描述它的上级(世界对象)经过多少单位时间才能迎接来自身的一个单位时间。可以通过改变该属性来改变某一单位的时间流速,同时每个单位也有onTimer方法实现自身一个单位时间来临时要处理的逻辑,通过直接多次调用该方法实现某个单位的时间跳跃。
加速时间的技能

技能设计

孤影剑客的技能以自身残影为核心,创造许多残影之后使用大招魔影杀来造成爆炸伤害。
在这里插入图片描述
在这里插入图片描述
星界游侠大招需要蓄力八秒才能达到最大伤害,搭配技能时间仓库可以更快完成大招
在这里插入图片描述
时间加速的残影
蓄力大招
星界游侠无双状态也不能免受控制,所以为其添加了专属解控技能“净化”
技能净化描述

怪物AI

通过类似行为树的形式每隔一定周期(例如5帧)为怪物施加指令。其中部分指令依赖于随机数,故怪物的行动方式大体上可预测,但细节上不是一成不变的,使游戏更具趣味性。

联机实现

目前联机功能还在开发阶段,在测试链接里没有开放
该功能实现了联机存档,两个玩家通过各自的设备同时在一个存档玩同一关卡,类似于《饥荒》的联机模式。联机主要通过WebSocket协议,帧同步实现。

移动端支持

该功能开发尚未完全
初步实现了触摸屏操作形式的虚拟游戏手柄,还有屏幕比例适配的问题需要解决
移动端手柄

未来改进计划

  • 实现逻辑与渲染完全的分离,将逻辑帧与渲染帧独立
  • 移动端适配:为了移动端更好的游戏体验,将游戏界面固定大小改为适应大多数手机设备宽高比例的形式
  • 完成基于帧同步的联机功能
  • 实现资源动态加载,在进入关卡前加载此关卡需要的资源,加速游戏首页加载速度
  • 集成TexturePacker工具,将图片资源打包为雪碧图,减少游戏加载时的http请求,减缓服务器压力,优化用户体验。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值