首页 > 其他分享 >threejs——开发一款塔防游戏

threejs——开发一款塔防游戏

时间:2024-04-10 11:32:19浏览次数:12  
标签:coefficient 动画 threejs const 一款 塔防 money 武器 敌人

前言

完成效果

gif 图较大,耐心等待,源码见文末

为了上班摸鱼合理的玩游戏,我写了一个3d塔防游戏,其中功能包含动画、敌人运动、放置武器、升级武器、销毁武器、动态检测等功能。请动动小手,点赞收藏,这就发车~

目录结构

思维导图

具体功能和思路如下

有了这个思维导图,就可以按部就班,一步一步的实现游戏功能

技术栈

  • typescript
  • vite
  • threejs
  • astarjs

由于项目体系较大,内容覆盖较广,下面挑几个关键内容介绍一下

地图

首先要加载一个地图,地图功能包含可放置模块,不可放置模块(敌人路线,装饰元素),大概思路就是根据floorSize生成一个长和宽相等的地图,每个地图都是一个plane。

const createPlane = (texture: THREE.Texture): THREE.Mesh => {
    const geometry = new THREE.PlaneGeometry(1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide });
    let plane = new THREE.Mesh(geometry, material);
    plane.rotation.x = Math.PI * 0.5;

    plane.material.map = texture;
    plane.material.needsUpdate = true;
    castShadow(plane)
    return plane
}

生成轨迹

第一步用for循环,生成一个二维数组存放在mapUseV2中,设定一个起点const startPoint = new THREE.Vector2(0, 0)和终点const endPoint = new THREE.Vector2(floorSize - 1, floorSize - 1),用于生成敌人运动轨迹。

生成轨迹的代码

const maps = new (window as any).Graph(mapUseV2);
// 夸过阻碍的随机点位,生成怪物的路线图 
var starPosition = maps.grid[startPoint.x][startPoint.y];
var endPosition = maps.grid[endPoint.x][endPoint.y];

// 计算路线图
let trailPoints = (window as any).astar.search(maps, starPosition, endPosition, {
    closest: false
});

寻路生成后的数据结构如下

这些14个点位都是敌人的路线,所以不可以放置武器,所以要在mapUseV2中将数据置为0

mapUseV2[trailPoints[i].x][trailPoints[i].y] = 0;

这样我们就得到了一个包含敌人行动路线、装饰、可放置区域的地图。

敌人

前面定义的startPoint作为敌人的出生点。生成敌人的方法为EnemyCrouched类,初始化敌人的血条,移动速度,攻击力,和等级。

继承基类Enemy_Level,Enemy_Level定义了添加方法、运动、和动画的更新。如果要多加一种敌人类型,再继承Enemy_Level即可,(项目目前只有一种敌人类型),而Enemy_Level也继承了公共类ModelCheck,这个公共类主要功能是修改位置、旋转角度、获取包围盒等功能。武器、子弹、敌人、水晶塔都继承了这个公共类。

生成敌人

创建敌人的时候遇到一个问题,本来想着节约渲染成本,只加载一次敌人模型,再使用clone()方法,复制出多个敌人,但是敌人模型包含了骨骼模型,在克隆模型的时候,骨骼动画和敌人模型复制了,但是两者的绑定关系没有复制成功,导致骨骼动画和模型不匹配,导致动画不生效,所以每次生成敌人的时候都重新加载了一下模型。

export class EnemyCrouched extends Enemy_Level {
    coefficient: number = crouched_coefficient
    constructor(level: number) {
        super(ENEMY_CROUCHED_NAME)
        this.speed = this.coefficient * 60
        this.price = this.coefficient
        this.HP = this.coefficient * level
        this.level = level

        // 骨骼在执行clone()时候,骨骼之间的位置和动画发生错位,所以每次重新拉一次模型,很不可取。
        // 但是可以重新克隆一个骨骼动画,并重新绑定骨骼动画和新的模型之间的关系
        loadGltf(import.meta.env.VITE_ASSETS_URL + '/assets/models/snowgolem/scene.gltf').then((gltf: any) => {
            this.model = gltf.scene
            this.bindAnimite(gltf.animations)
            this.addModel()
            this.group.example = this
            this.handleModel(this.group)

            this.run()
            this.runStart()
        });

    }
}

敌人动画

通过bindAnimite绑定动画可以将模型中提供的骨骼动画绑定到模型上并播放动画。

bindAnimite(animations: GltfModel['animations']) {
    if (this.model) {
        this.modelAnimation = new Animation(this.model, animations);

        this.modelAnimation.once(['death'])
        this.modelAnimation.play('walking');
    }
}

这里说一下Animation类,提供播放动画play、切换动画fadeToAction、显示骨骼createSkeleton,过滤一次性动画once比如跳跃,死亡,敬礼这种非循环播放的动画。 更新动画upDate,这个方法接受一个镜头参数,如果将镜头参数传进来,可以实现镜头跟踪功能。

生成武器

生成武器通过点击操作栏对应的按钮进行。

关于武器的定位

在点击十字弓按钮后,将触发鼠标的射线检测,并将检测目标设置为地板,当鼠标移动至可放置模型的位置时,显示蓝色的框,如果检测到鼠标位置不可放置模型,则辅助色块变为红色,点击无效,需重新选择位置。

初始化武器

Crossbow_level初始化十字弓,Rifle_level初始化火炮。这两个类是区分不同的武器,并在初始化的时候加载武器模型并提供武器的upDate方法,这两个类都继承他们的基类Weapon。Weapon提供目标检测、射击等功能。Weapon又继承公共类ModelCheck为武器提供包围盒,尺寸和模型矫正的功能。初始化武器时有几个属性比较重要:武器系数coefficient、等级level这两个数值影响武器的子弹发射间隔,威力,和单价(包括升级的价格)。

基于setAnimationLoop做的定时器

这里介绍一下IntervalTime

.setAnimationLoop ( callback : Function ) : undefined

callback — 每个可用帧都会调用的函数。 如果传入‘null’,所有正在进行的动画都会停止。 可用来代替requestAnimationFrame的内置函数. 对于WebXR项目,必须使用此函数。

requestAnimationFrame方法熟悉threejs的都了解,基于屏幕刷新率调用的,一般60次/秒,实现定时的原理很简单,调用一次的时候获取一次当前时间,用当前调用时的时间now减去上次存的时间lastTime,如果超过规定的循环间隔时间time调用一次回调,这时会将lastTime置为time,并进行新一轮的计时。项目中炮弹发射间隔就是这么做的。当然,你也可以在threejs以外的项目使用。

export class IntervalTime {
    lastTime = 0
    constructor() {
    }
    interval(time: number, callback: () => void) {
        let now = performance.now(); // 使用 performance.now() 获取高精度时间
        let deltaTime = now - this.lastTime;

        if (deltaTime > time) { 
            // 执行一秒内需要做的事情
            callback()
            // 重置时间
            this.lastTime = now;
        }
    }
}

子弹跟踪

武器在生成后会发射子弹,调用TransmitDiscards类。根据不同的武器生成不同的子弹,并在render中调用子弹类的upDate方法。子弹会跟踪检测敌人的位置,并找到最近的敌人进行攻击,得到最近敌人的位置和子弹发射位置,生成一条抛物线,开口向下,并让子弹沿着抛物线进行运动,直到子弹运动到终点,并利用敌人的包围盒检测子弹的位置,判断子弹是否进入敌人包围盒的范围。如果在范围内,敌人HP掉对应的血量,并销毁子弹。

 upDate() {
        // 子弹检测敌人
        EnemyGroup.traverse((enemy) => {
            if (enemy.example) {
                if (this.model) {
                    this.getBox()
                    const worldPos = new THREE.Vector3()
                    this.model.getWorldPosition(worldPos)
                    this.position = worldPos.clone();
                    enemy.example.getBox();
                    const contains = enemy.example.box3.containsPoint(this.position)
                    // 子弹撞击敌人
                    if (contains) {
                        console.log('击中敌人');
                        let hp = enemy.example.HP - this.power
                        enemy.example.HP = hp
                        enemy.example.hpDom.element.innerHTML = `HP:${Math.max(0, hp)}`
                        this.dispose()
                    }
                }

            }
        })
    }

当敌人的hp归零时,则销毁敌人并增加对应数量的金币

敌人的update方法

 upDate() {
    this.getBox()
    if (this.modelAnimation) {
        this.modelAnimation.upDate()
    }
    if (this.HP <= 0) {
        this.dispose();
        wallet.add(this.price);
    }

}

武器升级

操作栏的功能区,提供了几个属性,区分放置、升级、销毁等功能

// 存放全局变量的
export const buttonState = {
    DOWN_HERO: false, // 是否可以放置模型
    CHECK_HERO: true, // 是否可以选中模型
    HREO_TYPE: 'crossbow' as "crossbow" | "rifle",
    UPGRADE_HERO: false, // 是否升级英雄
    DISPOSE_HERO: false, // 是否销毁英雄
    SHOW_BOX3_HELPER: true, // 是否显示辅助线
    GAME_STOP: false // 暂停游戏
}

前面提过,放置武器的时候,射线检测的对象为floor组,而升级则会将射线检测的组改为武器组,通过检测到的模型去升级对应的二级和三级,当武器达到三级的时候则不可再升级,升级武器时将原有武器销毁,添加一个新的模型,目前对于武器升级只是将系数调高,如感兴趣可根据武器等级,生成多个子弹,从单一攻击改为群攻。

水晶塔

相对于水晶塔,则复用子弹的逻辑即可,将敌人视作子弹,水晶塔视作敌人,在水晶塔的upDate方法,检测敌人的位置,如果敌人的包围盒与水晶塔的包围盒相交,则水晶塔减去对应的hp,如果水晶塔的hp归零,则视为游戏失败

全局配置

金币

金币方法相对简单一些,咱们这里没vip充值,所以不必考虑一个648换算几个钻石,1个钻石换算几个金币,游戏的金币系统,只有一个加一个减,创建和升级武器减去相对应的金币,消灭敌人增加对应的金币

import { Money } from "./variable";

// 钱包实例
class Wallet {
    money: number = Money;
    moneyDom: HTMLDivElement | undefined
    constructor() {
        const dom = document.querySelector('.money') as HTMLDivElement | undefined
        if (dom) {
            this.moneyDom = dom;
            this.changeText()
        }
    }
    add(money: number) {
        this.money += money;
        this.changeText()
    }
    sub(money: number) {
        if (this.money < money) {
            return false
        } else {
            this.money -= money;
            this.changeText()
        }
        return true
    }
    changeText() {
        if (this.moneyDom) {
            this.moneyDom.innerText = this.money + '金币'
        }
    }
}
export const wallet = new Wallet()

金手指

金手指支持初始化金币数量、武器系数、地图尺寸。通过路径参数提供

const searchParams = getParams()

const getValue = (field: string, def: number): number => {
    console.log('searchParams',searchParams);
    
    const f = searchParams[field]
    let fz = f ? Number(f) : def;
    return fz
}

export const floorSize = getValue('floorSize', 8)  // 必须双数,不然后面的计算有问题

// 系数
// 十字弩系数
export const crossbow_coefficient = getValue('crossbow_coefficient', 10)

// 火炮系数
export const rifle_coefficient = getValue('rifle_coefficient', 10) * 2
// 初始化金币
export const Money: number = getValue('money', 100);

如果将路径地址后面拼上这段参数

?crossbow_coefficient=100&rifle_coefficient=100&floorSize=16&money=500,

那么你将拥有500个金币,攻击力100打底的武器,并且地图尺寸变大,敌人行动轨迹变长

这会影响游戏的平衡性,当然,我们的游戏也没做公平性和平衡性的考虑。


链接:
https://juejin.cn/post/7355745761370505231

标签:coefficient,动画,threejs,const,一款,塔防,money,武器,敌人
From: https://blog.csdn.net/2401_84190925/article/details/137589645

相关文章

  • Todesstern:一款针对注入漏洞识别的强大变异器引擎
    关于TodessternTodesstern是一款功能强大的变异器引擎,该工具基于纯Python开发,该工具旨在辅助广大研究人员发现和识别未知类型的注入漏洞。Todesstern翻译过来的意思是DeathStar,即死亡之星,该工具是一个变异器引擎,专注于发现和识别未知类型的注入漏洞。该工具可以从用户给......
  • 强!推荐一款API 接口自动化测试平台!
    在项目开发,迭代交付过程中开发人员,测试人员需要针对系统微服务API做调试,回归测试,性能测试。自动化测试,一个好的平台本质上需要解决API测试的5大基本问题。1.支持不同的项目,角色,技术人员多人协作2.支持定义多个不同的测试环境3.支持定义各种被测系统,API,功能,性能用例4.支持功能......
  • 我要点名一款十字线上 PVP 游戏 - 1951
    \(1900-12=1888\)。怎么rating还是这么好笑。感觉每回打cf都要破防是怎么回事?被诈骗不还是因为菜?交\(12\)发不知道自己是怎么想的。然后E也不难,但是太晚了打不动了。下次交代码之前能不能拜托先把hack测一下?占了将近一半的RE哪个不是因为没开longlong?A01字符串......
  • 永不生锈的螺丝钉!一款简洁好用的数据库表结构文档生成器
    大家好,我是Java陈序员。在企业级开发中,我们经常会有编写数据库表结构文档的需求,常常需要手写维护文档,很是繁琐。今天,给大家介绍一款数据库表结构文档生成工具。关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。项目介绍screw......
  • 编写一款2D CAD/CAM软件(五)视图
    j-view模块目录配置:JCadincludej-viewj-view.hj-viewprjCMakeLists.txtsrcj-view.cppCMakeLists.txt存在多种计算机图形学的引擎,用于将图形绘制到显卡缓存并显示出来。有些引擎使用CPU的能力,有些则充分发挥GPU的......
  • 【前端素材】优质小游戏推荐-维京战争塔防小游戏
     一、需求分析前端动画游戏页面是指在网页前端开发中,通过实现动画效果和游戏元素,创造出一个交互式、娱乐性强的页面。这类页面通常具有以下特点、功能和技术实现方式:1、功能实现:角色设计:包括主角、敌人、NPC等游戏角色的视觉形象设计。角色设计要符合游戏风格,突出个性特点......
  • 一款免费强大的运维工单管理系统 - WGCAT
    WGCAT是WGCLOUD团队开发的一款运维工单系统,该工单系统遵循轻量实用的设计原则,也是遵循该团队一贯的设计作风,恰到好处,简约时尚,部署和操作使用都很简单。除了工单流转功能,WGCAT工单系统还提供了资产管理模块,账号管理模块,以及工作笔记,可以更好的辅助我们工作。最好的是WGCAT工单......
  • 推荐一款超好用的开源SSH客户端:WindTerm
    WindTerm是一个用于DevOps的更快更好的SSH/Te.net/Serial/Shell/SFTP客户端。WindTerm目前仍然处于起步阶段,所以更多功能期待未来开发版本!如果你想要一个高性能的文本编辑器,你可以试试作者开发的免费WindEdit编辑器。功能特色支持SSH、Telnet、Tcp、Shell、Serial支持......
  • 【跨境商家福音】一款性价比高、好用的跨境选品工具
    亚马逊、速卖通、Shopee、Lazada、美客多、eBay、SHEIN、Temu、Tiktok、shopify等跨境电商平台,其用户消费喜好多样,涵盖服装、美妆、电子产品等多个品类。而店雷达作为一款基于大数据和人工智能技术的电商分析工具,为商家提供了强大的选品和数据分析功能。结合店雷达,商家能更精......
  • 推荐一款强大的开源自动化测试神器
    搞过自动化测试的小伙伴,相信都知道,在Web自动化测试中,有一款自动化测试神器工具:selenium。结合标准的WebDriverAPI来编写Python自动化脚本,可以实现解放双手,让脚本代替人工在Web浏览器上完成指定的操作。虽然selenium有完备的文档,但也需要一定的学习成本,对于一个纯小白来讲还是......