前言
不喜欢玩游戏的技术宅不是一个好的技术宅。为了使得学习threejs不那么枯燥,遂以制作一款生存者游戏为目标,来达成学习的目的。
效果展示
源代码
仓库地址: gitee
部分逻辑参考了这位大佬: steve
体验地址: game
一些关键点
- threejs实现基本的渲染逻辑,cannon负责物理系统
- UI图标绘制用canvas实现
- 弹窗用dom元素和样式实现
- 判断设备类别分别实现wasd移动操作和摇杆操作
- 一些技能算法的实现思路
- 后续可玩操作加载glb模型和动画替代主角,添加socket通信框架实现多人游戏等。
献丑
整体目录架构
│ main.ts
│ style.css
│ typescript.svg
│ vite-env.d.ts
├─assets //资源文件目录
│ ├─audio //音频文件
│ │ └─background.mp3
│ └─textures //贴图文件
│ ├─ground //地形贴图
│ ├─items //物品贴图
│ ├─monsters //怪物贴图
│ └─skills //技能贴图
├─base //基础类
│ BodyGC.ts //物理世界回收器
│ constants.ts //常量定义
│ emitter.ts //全局通知事件
│ loader.ts //加载器
│ registry.ts //动态自定义注册类
├─core //单例核心模块
│ index.ts
├─data //数据定义config
│ config.ts
├─design //设计器
│ ├─items //物品设计逻辑实现
│ ├─monsters //怪物设计逻辑实现
│ └─skills //技能设计逻辑实现
│ BaseSkill.ts
│ BurningGround.ts
│ Fireball.ts
│ FlyingSword.ts
│ index.ts
│ laser.ts
│ LightChain.ts
│ LightningComet.ts
│ Longinus.ts
├─modules //业务模块
│ Audio.ts
│ character.ts
│ ItemManager.ts
│ LevelManager.ts
│ world.ts
└─ui //UI控制器
├─FormUI.ts
├─index.ts
└─ui.css
加载器预加载资源以及缓存
在src/core/index.ts里定义核心模块功能,预先将指定目录下的贴图等资源加载并缓存起来
private async _loadTextures() {
// 1. 拿到所有静态资源模块
const modules = import.meta.glob('/src/assets/**', { eager: true, as: 'url' });
await Promise.all(
Object.entries(modules).map(async ([fullPath, url]) => {
const fileName: any = fullPath.split('/').pop();
if (fullPath.includes('audio')) {
const audio: any = await this.loader.audio_loader.loadAsync(url);
this.maps[fileName] = audio;
} else {
const texture: any = await this.loader.texture_loader.loadAsync(url);
texture.colorSpace = SRGBColorSpace;
texture.wrapS = texture.wrapT = RepeatWrapping;
this.maps[fileName] = texture;
}
})
);
}
后续各个模块就可以通过 this.core.maps[key]来直接调用贴图。
核心模块作为单例模式可以被各个模块引用,这个模块主要是初始化threejs主对象和camera,rennder,scene等,以及cannon的world对象。
世界管理器
src/modules/world.ts实现了全局场景管理器,在这里对角色模块以及技能系统,场景系统进行初始化。并且实现对各类事件的监听。
export default class World {
//世界核心
core!: Core
//是否进入场景的flag
enterWorld: boolean = false;
character!: Character
private _width: number;
private _height: number;
ground!: THREE.Mesh
levelManager!: LevelManager
gc!: BodyGC
formUI!: FormUI
itemManager: ItemManager
audio: Audio
constructor(width: number, height: number) {
this._width = width;
this._height = height;
this.core = new Core();
this.core.$on(ON_LOAD_PROGRESS, this._handleLoadProgress.bind(this))
this.core.$on(ON_LOAD_MODEL_FINISH, this._handleLoadFinish.bind(this))
this.core.$on(ON_ENTER_APP, this._onEnterApp.bind(this))
this.core.$on(ON_CLICK_RAY_CAST, this._onClickRayCast.bind(this))
this.core.$on(ON_SHOW_TOOLTIP, this._onShowToolTip.bind(this))
this.core.$on(ON_HIDE_TOOLTIP, this._onHideToolTip.bind(this))
this.core.$on(ON_GAME_OVER, this._onGameOver.bind(this))
this.core.$on(ON_LEVEL_UP_OPEN, this._onLevelUpOpen.bind(this))
this.core.$on(ON_LEVEL_UP_CLOSE, this._onLevelUpClose.bind(this))
this.core.$on(ON_TOUCH_ITEMS, this._onTouchItems.bind(this))
this.gc = new BodyGC();
this.character = new Character();
this.levelManager = new LevelManager();
this.itemManager = new ItemManager();
this.formUI = new FormUI();
this.audio = new Audio();
this._initLight();
this._initHelper();
setTimeout(() => {
this.core.$emit(ON_LOAD_MODEL_FINISH, {});
}, 1000);
}
}
技能系统
通过装饰器工厂模式可以便捷快速的实例化技能对象。
type Class = new (...args: any[]) => any;
interface Creator {
new (...args: any[]): any;
// 额外放个体型标记,方便调试
className?: string;
}
/**
* 全局注册表
*/
const Repo = new Map<string, Class>();
/**
* 装饰器工厂:把类丢进 Repo
*/
export function register(key?: string) {
return <T extends Creator>(target: T) => {
const k = key ?? target.name;
target.className = k; // 调试用
Repo.set(k, target);
return target;
};
}
/**
* 创建实例:把 ...args 原样交给构造函数
*/
export function createInstance<T extends Class>(
name: string,
...args: ConstructorParameters<T>
): InstanceType<T> {
const C = Repo.get(name) as T | undefined;
if (!C) throw new Error(`Class "${name}" not registered`);
return new C(...args) as InstanceType<T>;
}
/**
* 查看已注册列表
*/
export function listRegistered() {
return [...Repo.keys()];
}
只需要在技能类上标注@register("Fireball"),然后调用装饰器的createInstance方法就可以实现指定技能的实例化。
@register("Fireball")
export default class Fireball extends BaseSkill {
mesh!: THREE.Object3D;
body!: CANNON.Body;
constructor(options: SkillConfig) {}
}
UI绘制
通过挂载canvas对象调用2d模式实现叠加层UI的绘制。
export default class FormUI {
private canvas;
private ctx;
private w = 400
private h = 100
private iconSize: number = 48;
private role: Character
private time: number = 0
constructor() {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d')!;
this.canvas.width = this.w
this.canvas.height = this.h
this.role = new Character();
Object.assign(this.canvas.style, {
position: 'fixed',
left: '50%',
bottom: '10px',
transform: 'translateX(-50%)',
zIndex: 15,
pointerEvents: 'none' // 让点击穿透到页面
})
document.body.appendChild(this.canvas)
}
}
//具体的绘制方法
private _draw() {
}