游戏简介
开发背景
小学起一直有玩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请求,减缓服务器压力,优化用户体验。