[C/C++]天天酷跑超详细教程-中篇

  •  个人主页:北·海
  •  🎐CSDN新晋作者
  •  🎉欢迎 👍点赞✍评论⭐收藏
  • ✨收录专栏:C/C++
  • 🤝希望作者的文章能对你有所帮助,有不足的地方请在评论区留言指正,大家一起学习交流!🤗

天天酷跑,一款童年游戏,主要是进行跳跃操作,和躲避障碍物,中篇主要实现人物的下蹲,随机障碍物的生成以及优化main函数里面的sleep(30)

一. 游戏的展示效果

二.本节开发日志

上篇已更新 : 天天酷跑上篇

优化main函数里面的sleep(30);

1.利用接口getDelay()函数

2.更新窗口标志update;

3.优化来自用户点击时候的消息

实现玩家的下蹲

1利用计数器

随机障碍物的实现  

1.障碍物池子

2.随机数取类型

三.优化上篇中main函数里的sleep(30)

首先为什么要优化这个呢?

了解sleep的都知道,这个是让程序休眠30ms也就是当程序执行到这里的时候,会在这里呆30ms,因为这款游戏需要接收玩家的按键消息,所以有时候玩家在这30ms内有键盘响应的时候,由于程序休眠,无法接收到消息,会降低游戏的体验性

要解决的问题: 在按跳跃键的时候不受sleep的影响

解决方法 : 利用时间戳,定义一个计时器,当达到该计时器的设定的时间就可以打开刷新窗口的按钮,或者当跳跃时候打开该按钮,代码如下:

int main() {

	init();
	while (1) {
		keyEvent();

		timer += getDelay();
		if (timer >= 30) {
			timer = 0;
			update = true;
		}

		if (update) {
			update = false;
			BeginBatchDraw();
			//渲染背景
			updataBg();
			//实现人物的奔跑
			putimagePNG2(heroX, heroY, &imgHero[heroIndex]);
			//渲染障碍物
			updateEnemy();

			EndBatchDraw();

			fly();
		}
	}
	system("pause");
	return 0;
}

代码解释 : timer是一个全局变量,用于累加两次函数执行的时间,当达到30ms就会将update标志设置为true进行刷新界面

时间戳 : 一个用于表示特定时刻的数值,通常是一个整数或浮点数。在上述代码中,时间戳用于记录函数调用的时间点

 这样优化的话,感觉和sleep(30)的效果一样,此时还需要在跳跃的时候将其update设为true,在以后只要接受玩家键盘消息的时候都要加上

void jump() {

//跳跃只需要改变y值即可,在底层数据管理函数实现,此时只需要给出可以改数据的信号即可
	heroJump = true;
	update = true;
}

这样的话就解决了玩家按跳跃键程序休眠的问题 ,此时提高了游戏的体验性

开发之前,先回顾一下上篇中的开发流程,将开发分为两层,一个是渲染层(update)一个是数据层(fly),渲染层是将游戏的图片呈现出来,数据层是控制渲染层的,实现游戏的控制,其他的功能根据可封装为函数围绕这两层进行展开,将游戏的资源加载放在初始化函数(init)

四.实现玩家的下蹲功能

天天酷跑主要就是跳跃和下蹲,下蹲可躲避障碍物柱子,给障碍物的创建先奠定基础,实现下蹲和实现跳跃的流程大致相似,分析可知,下蹲时玩家发出的信号,那么我们就可用从用户点击函数开始开发,当玩家按下a的时候执行下蹲操作

资源必须先加载进来,因为下蹲有两张图片,所以在全局定义一个存放该图片的数组,然后再初始化中进行加载

//下蹲
IMAGE imgDown[2];
bool heroDown;//下蹲标志

void init(){
...

//人物的下蹲
loadimage(&imgDown[0], "res/d1.png");
loadimage(&imgDown[1], "res/d2.png");
heroIndex = 0;
heroDown = false;
}

由于下蹲是个动态的,所以要用到帧序列,也要将其初始化为0

用户点击 

void keyEvent() {
	//获取玩家键盘事件
	char ch = 0;
	if (_kbhit()) {
		ch = _getch();
		if (ch == ' ') {//空格为跳跃
			jump();
		}
		else if (ch == 'a') {//a为下蹲
			down();
		}
	}
}

 当程序收到下蹲的消息时,就会执行该下蹲操作,将下蹲功能封装为一个函数

实现下蹲 

void down() {
//下蹲只需要改变y值即可, 在底层数据更新函数实现, 此时只需要给出可以改数据的信号即可
	heroDown = true;
	update = true;
    heroIndex = 0 ;
}

此时定义一个下蹲的标志,如果时true的话,就可以在数据层进行修改数据,然后打开更新界面的按钮

现在就可以在数据层实现数据的更新了

数据更新

	//实现跳跃
	if (heroJump) {//跳跃状态
		if (heroY < jumpHeightMax) {
			heroJumpOff = 4;//+ (-4)等于向上走,+4等于向下走
		}

		heroY += heroJumpOff;
		if (heroY > 345 - imgHero[0].getheight()) {
			//达到地面
			heroJump = false;
			heroJumpOff = -4;
		}
	}
	else {
		//改变人物帧序列
		heroIndex = (heroIndex + 1) % 12;
	}

这里是上篇中的人物跳跃和人物跑步,此时只需给该状态添加一个下蹲的状态即可

	else if (heroDown) {//下蹲状态
		heroIndex++;
		if (heroIndex >= 2) {
			heroDown = false;
			heroIndex = 0;

		}
	}

我只贴出了下蹲部分的代码,当heroDown为true说明执行下蹲,就改图片帧,当帧数大于等于2的时候,说明一次下蹲操作结束了,此时就可以将下蹲标志设置为false,将图片帧也得归零

这个下蹲的速度非常快,不到1s就结束了,所以没有截图到,除了速度块基本上这个下蹲操作就已经实现了,现在来优化一下这个下蹲的速度

下蹲速度优化

为什么速度会这么快呢?

因为这个下蹲的图片只有两张,循环一次的时间也就几十毫秒,所以速度很快,解决这个问题有两种方法,第一个就是,将这个下蹲过程的图片设置很多张,例如下蹲的图片有50张,将其全部渲染出来,这就加长了时间,当然也是很费内存的,第二种就是用计数器,利用空循环当达到计数器设定的值的时候在进行执行下蹲,由于下蹲第二张渲染呈现时间比第一张的长,这也显得下蹲才逼真,解决这一问题,需要定义一个数组,存储两张图片的计数,以下为代码:

else if(heroDown){
		static int count = 0;
		int dalays[2] = { 4,10 };
		count++;
		if (count >= dalays[heroIndex]) {
			count = 0;
			heroIndex++;
			if (heroIndex >= 2) {
				heroIndex = 0;
				heroDown = false;
			}
		}
	}

利用static记录循环执行的次数,dalays保存两张图片的计数,heroIndex序列帧循环两个图片,如果满足的话,就将count设为0,执行下蹲操作

渲染下蹲操作

人物跳跃的渲染在main函数里面通过一行代码实现了,但是现在人物的状态有两种,跳跃与下蹲,此时就需要封装为函数了,创建updateHero函数

在main函数里用updateHero函数代替人物奔跑的代码 

void updateHero() {
	//实现人物的奔跑
	if (!heroDown) {
		putimagePNG2(heroX, heroY, &imgHero[heroIndex]);
	}
	else {
		int y = 345 - imgDown[heroIndex].getheight();
		putimagePNG2(heroX, y, &imgDown[heroIndex]);
	}
}

如果处于非下蹲操作,由于人物都是一个高度,当处于下蹲时候,两张图片的高度不一样,所以需要利用图片序列帧计算y的坐标

此时的下蹲代码算是写完了,下面时运行结果此时就很容易的截图到该英雄的下蹲操作了,接下来实现随机障碍物

 看看上面几个小标题就是开发这个模块的流程

五.随机障碍物的实现

如何实现随机障碍物呢?

此时就会涉及到枚举的应用,障碍物的封装,障碍物的池子和随机数

枚举 : 可用提高代码的可读性,简化程序,一般一定个数的东西可定义为枚举类型,枚举里的元素,也就是整数类型

首先定义枚举类型,就将障碍物设置为乌龟,狮子,四种柱子

typedef enum {
	TORTOISE,//乌龟  0
	LION,//狮子      1
	HOOK1,//柱子     2
	HOO2,
	HOO3,
	HOOK4,
	OBSTACLE_TYPE_COUNT // 总数 
}obstacle_type;

在这里用到的OBSTACLE_TYPE_COUNT 很是巧妙,枚举里的值从0开始,到了OBSTACLE_TYPE_COUNT 刚好时前面障碍物的总数,此时就将枚举定义好了,然后就可以封装结构体了

首先应该知道封装的属性都有什么,一个障碍物,他得有类型,坐标,速度,伤害,使用状态,此时我们可用再添加一个图片的帧序列,因为每个障碍物有的是动态的,都有序列,此时就可以将初始化加载图片进行优化,要用到一个大小可变的容器vector来存储,声明为二维的,每一维存储该组图片

vector的使用需要导入头文件vector  #include<vector>

代码中 obstacleImgs为定义在全局的二维数组,在初始化时候,创建个一维数组,最后再将其一维数组添加到该二维数组里

vector<vector<IMAGE>>obstacleImgs;//存放所有障碍物的各个图片

 此时所有障碍物的图片存在于二维数组obstacleImgs中了

封装结构体

typedef struct obstacle {
	int type;//类型,由于类型定义在枚举种,枚举里的变量就相当于整数类型,所以可用int代替
	int x, y;//坐标
	int imgIndex;//帧序列
	int speed;//速度
	int power;//伤害
	bool exist;//是否可用

}obstacle_t;

创建障碍物池子

也就是定义一个结构体数组,OBSTACLE_COUNT是定义的宏,池子的大小

obstacle_t obstacles[OBSTACLE_COUNT];//障碍物池子

在封装了障碍物之后,那么之前小乌龟所定义的地方都需要优化了

小乌龟的定义

创建小乌龟

fly函数中小乌龟的运动

障碍物的渲染层

 此时可用依据上面删除的部分进行开发,定义我们已经做了但是应该将池子里的exist属性进行初始化,以保证能够正确的知道哪个障碍物可用

void init(){

...
	//初始化障碍物池子
	for (int i = 0; i < OBSTACLE_COUNT; i++) {
		obstacles[i].exist = false;
	}
}

接下来需要创建小乌龟,此时应该重写creatObstacle函数,

开发思路 : 先用for循环在池子里面找到一个可用使用的障碍物,也就是exist为false的,然后再设定他的各属性

void creatObstacle() {
	int i = 0;
	//找到一个可以用的障碍物
	for (i = 0; i < OBSTACLE_COUNT; i++) {
		if (obstacles[i].exist == false) break;
	}

	obstacles[i].exist = true;
	obstacles[i].imgIndex = 0;
	obstacles[i].type = rand() % 6;//取0-5的随机数
	obstacles[i].x = WIN_WIDTH;
	obstacles[i].y = 345 + 5 - obstacleImgs[obstacles[i].type][0].getheight();
	if (obstacles[i].type == TORTOISE) {//小乌龟
		obstacles[i].power = 5;
		obstacles[i].speed = 0;
	}
	else if (obstacles[i].type == LION) {//狮子
		obstacles[i].power = 20;
		obstacles[i].speed = 5;
	}
	else if (obstacles[i].type >= HOOK1 && obstacles[i].type<= HOOK4) {//四个柱子
		obstacles[i].power = 20;
		obstacles[i].speed = 0;//静态的
		obstacles[i].y = 0;//由于柱子是在填上挂着,所以将其y设置为0
	}
}

这个初始化看着比较多,但是难度不大,就找到一个可以用的障碍物,然后将其封装的属性进行初始化,实现随机就是再枚举里面取随机数,只有狮子是跑过来的,所以要和第三层草坪背景图的速度不能保持一致,其他障碍物的速度设置为0即可实现初始化

fly中更新障碍物的数据

更新x坐标使其运动,更新图片帧序列使其处于动态

void init(){


...


//更新各障碍物的状态
for (int i = 0; i < OBSTACLE_COUNT; i++) {
	if (obstacles[i].exist) {
		obstacles[i].x -= (obstacles[i].speed + bgSpeed[2]);
		if (obstacles[i].x < -obstacleImgs[obstacles[i].type][0].getwidth() * 2) {
			//已经从左边跑出了屏幕
			obstacles[i].exist = false;
		}
		//更新该障碍物的帧序列
		int len = obstacleImgs[obstacles[i].type].size();
		obstacles[i].imgIndex = (obstacles[i].imgIndex + 1) % len;
		}
	}
	

}

代码解释 : 从障碍物池子里面找正字使用的障碍物,找到之后,再改变他的x坐标,bgSpeed[2]为草坪的速度,当减去他的时候,和草坪是相对速度为0,再减去该障碍物的速度,就是和草坪的相对速度,若不为0,此时就能显示出运动的状态,若为0,就和草坪相对静止

渲染障碍物

void updateEnemy() {

	for (int i = 0; i < OBSTACLE_COUNT; i++) {
		if (obstacles[i].exist) {
			putimagePNG2(obstacles[i].x, obstacles[i].y, WIN_WIDTH,
				&obstacleImgs[obstacles[i].type][obstacles[i].imgIndex]);
		}
	}

}

渲染的图片的第一维是该图片的类型,第二维是该图片的帧数,很巧妙

这样就设计完了,看看成果

我跑了半分钟,感觉这个柱子出现的频率还是太大了,因为当初随机数是对6取余的,二柱子就占了四个,所以这里可用优化

此时类型的这里就化解了,取两次随机数,让其柱子出现的几率降低

此时就能看到这几个障碍物同框了,但是碰撞这里还没有做,现在随机障碍物也实现了

六.实现英雄与障碍物的碰撞检测

从图可以看出,障碍物的碰撞检测就是在检测两个矩形是否相交,这种判断矩形相交的代码在网上开源的有很多

如果以白边的坐标来检测的话,可能会有误差,则加上偏移量,使判断更加准确,

分析 : 碰撞检测实在数据层进行的,但是这个功能可封装为函数,所以在fly函数里面定义一个checkHit函数用于检测碰撞

一下是判断是否碰撞的代码,主要是找到这四个点的坐标,加上偏移量即可

开源代码,判断矩形是否相交

//设A[x01,y01,x02,y02]  B[x11,y11,x12,y12].
bool rectIntersect(int x01, int y01, int x02, int y02,
    int x11, int y11, int x12, int y12)
{
    int zx = abs(x01 + x02 - x11 - x12);
    int x = abs(x01 - x02) + abs(x11 - x12);
    int zy = abs(y01 + y02 - y11 - y12);
    int y = abs(y01 - y02) + abs(y11 - y12);
    return  (zx <= x && zy <= y);
}

 找四个点的坐标,调用rectIntersect函数进行判断是否相交

void checkHit() {
//实现碰撞检测
	for (int i = 0; i < OBSTACLE_COUNT; i++) {
		if (obstacles[i].exist) {
			int a1x, a1y,a2x,a2y;
			int off = 30;
			if (!heroDown) {//非下蹲状态
				a1x = heroX + off;
				a1y = heroY + off;
				a2x = heroX + imgHero[heroIndex].getwidth() - off;
				a2y = heroY + imgHero[heroIndex].getheight();
			}
			else {//下蹲状态
				a1x = heroX+off;
				a1y = 345 - imgDown[heroIndex].getheight();
				a2x = heroX + imgDown[heroIndex].getwidth()-off;
				a2y = 345;
			}
			IMAGE img = obstacleImgs[obstacles->type][obstacles->imgIndex];
			int b1x = obstacles[i].x + off;
			int b1y = obstacles[i].y + off;
			int b2x = obstacles[i].x + img.getwidth() - off;
			int b2y = obstacles[i].y + img.getheight() - 10;

			if (rectIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)) {
				//相交
				heroBlood -= obstacles[i].power;
				printf("剩余血量 : %d\n", heroBlood);
				playSound("res/hit.mp3");
			}
		}
}

}

 

 此时有个bug,碰撞一次,连续掉血多次

bug原因 : 一帧一帧的检测

解决方法 : 结构体中添加属性,是否碰撞,在进行对其初始化,最后在碰撞检测函数里面优化

1.添加了判断条件

2.当判断碰撞后,将其hited设置为true

 效果图:

以上就是英雄与障碍物的碰撞检测模块了

七.总结

主要学习开发思想,一些开发技巧,将语法用到实战,了解计时器,计数器,枚举,结构体在开发中的应用,灵活运用函数封装提高程序的可读性,如何改善了用户点击休眠时的问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/101597.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【08期】ArrayList常见面试题

简介 ArrayList是我们开发中非常常用的数据存储容器之一&#xff0c;其底层是数组实现的&#xff0c;我们可以在集合中存储任意类型的数据&#xff0c;ArrayList是线程不安全的&#xff0c;非常适合用于对元素进行查找&#xff0c;效率非常高。 线程安全性 对ArrayList的操作…

Linux整合seata

1、到官网下载seata&#xff0c;这里以ZIP为例 2、使用远程工具上传ZIP包 3、通过命令解压 unzip seata-server-1.7.0.zip4、修改配置文件、新建表&#xff0c;参考我之前的文章 分布式系统的多数据库&#xff0c;实现分布式事务回滚&#xff08;1.7.0 seata整合2.0.4nacos…

2023开学礼新疆理工学院图书馆藏八一新书《乡村振兴战略下传统村落文化旅游设计》许少辉新财经理工

2023开学礼新疆理工学院图书馆藏八一新书《乡村振兴战略下传统村落文化旅游设计》许少辉新财经理工

怎样来实现流量削峰方案

削峰从本质上来说就是更多地延缓用户请求&#xff0c;以及层层过滤用户的访问需求&#xff0c;遵从“最后落地到数据库的请求数要尽量少”的原则。 1.消息队列解决削峰 要对流量进行削峰&#xff0c;最容易想到的解决方案就是用消息队列来缓冲瞬时流量&#xff0c;把同步的直…

[HNCTF 2022] web 刷题记录

文章目录 [HNCTF 2022 Week1]easy_html[HNCTF 2022 Week1]easy_upload[HNCTF 2022 Week1]Interesting_http[HNCTF 2022 WEEK2]ez_SSTI[HNCTF 2022 WEEK2]ez_ssrf [HNCTF 2022 Week1]easy_html 打开题目提示cookie有线索 访问一下url 发现要求我们输入手机号&#xff0c;可是只…

又一关键系统上线,理想车云和自动驾驶系统登陆OceanBase

8 月 1 日&#xff0c;理想汽车公布 7 月交付数据&#xff0c;理想汽车 2023 年 7 月共交付新车 34,134 辆&#xff0c;同比增长 227.5%&#xff0c;并已连续两个月交付量突破三万。至此&#xff0c;理想汽车 2023 年累计交付量已经达到 173,251 辆&#xff0c;远超 2022 年全年…

【MATLAB第71期】基于MATLAB的Abcboost自适应决策树多输入单输出回归预测及多分类预测模型(更新中)

【MATLAB第71期】基于MATLAB的Abcboost自适应决策树多输入单输出回归预测及多分类预测模型&#xff08;更新中&#xff09; 一、效果展示&#xff08;多分类预测&#xff09; 二、效果展示&#xff08;回归预测&#xff09; 三、代码获取 CSDN后台私信回复“71期”即可获取下…

Python Opencv实践 - 凸包检测(ConvexHull)

import cv2 as cv import numpy as np import matplotlib.pyplot as pltimg cv.imread("../SampleImages/stars.png") plt.imshow(img[:,:,::-1])img_contour img.copy() #得到灰度图做Canny边缘检测 img_gray cv.cvtColor(img_contour, cv.COLOR_BGR2GRAY) edges…

NoSQL技术——Redis

简单介绍 Redis是当下最流行的NoSQL数据库。在Redis中&#xff0c;数据的存储格式是以键值对的方式进行存储的。在键值对的存储形式中&#xff0c;值除了是常见的字符串&#xff0c;也可以是类似于Json对象的形式&#xff0c;或者是List&#xff0c;Map等数组格式&#xff0c;…

vulnhub渗透测试靶场练习2

靶场介绍 靶场名&#xff1a;easy_cloudantivirus 靶场地址&#xff1a;https://www.vulnhub.com/entry/boredhackerblog-cloud-av,453 环境搭建 依旧使用VM VirtualBox搭建靶场&#xff0c;攻击机使用的是VMware中的kali&#xff0c;需要将VMware虚拟机kali和virtualbox靶机…

CSS中如何实现元素的旋转和缩放效果?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 元素的旋转和缩放效果⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏…

Ceph构件及组件分析

Ceph存储架构 Ceph 存储集群由几个不同的daemon组成&#xff0c;每个daemon负责Ceph 的一个独特功能并。每个守护进程是彼此独立的。 下面将简要介绍每个Ceph组件的功能&#xff1a; RADOS&#xff08;Reliable Autonomic Distributed Object Store, RADOS&#xff09; RADOS…

Win10搭建VisualSvn Server

Win10搭建VisualSvn Server 目录 Win10搭建VisualSvn Server一、下载VisualSvn Server安装包二、安装VisualSvn Server三、配置和使用VisualSVN Server四、添加用户及权限设定方法五、创建目录及配置权限 1、服务端&#xff1a;有集成了Subversion和Apache、安装使用非常简单且…

匠心新品:大彩科技超薄7寸WIFI线控器发布,热泵、温控器、智能家电首选!

一、产品介绍 此次发布一款7寸高清全新外壳产品&#xff0c;让HMI人机界面家族再添一新成员。该产品相比其他外壳有以下5个大改动&#xff1a; 1 表面玻璃盖板使用2.5D立体结构&#xff1b; 2 液晶盖板采用一体黑设计&#xff0c;且液晶屏与触摸板是全贴合结构&#xff1b; …

什么是jvm

一、初识JVM&#xff08;虚拟机&#xff09; JVM是Java Virtual Machine&#xff08;Java虚拟机&#xff09;的缩写&#xff0c;JVM是一种用于计算设备的规范&#xff0c;它是一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 引入Jav…

Fedora Linux Flatpak 八月推荐应用

导读本文介绍了 Flathub 中可用的项目以及安装说明。 Flathub 是获取和分发适用于所有 Linux 的应用的地方。它由 Flatpak 提供支持&#xff0c;允许 Flathub 应用在几乎任何 Linux 发行版上运行。 请阅读 “Flatpak 入门”。要启用 Flathub 作为你的 Flatpak 提供商&#xff…

list对象中如何根据对象中某个属性去重使用Java8流实现

list对象中如何根据对象中某个属性去重使用Java8流实现? 在 Java 8 的流操作中&#xff0c;可以使用 distinct 方法来对一个对象流进行去重&#xff0c;但是默认情况下它会使用对象的 equals() 方法来判断重复。如果你希望根据对象的某个属性进行去重&#xff0c;则可以使用 …

Linux centos7 bash编程(循环与条件判断)

在编程训练中&#xff0c;循环结构与条件判断十分重要。 根据条件为真为假确定是否执行循环。 有时&#xff0c;根据条件的真假结果&#xff0c;决定执行哪些语句&#xff0c;这就是分支语句。 为了训练分支语句与循环语句&#xff0c;我们设计一个案例&#xff1a; 求一组…

DevOps理念:开发与运维的融合

在现代软件开发领域&#xff0c;DevOps 不仅仅是一个流行的词汇&#xff0c;更是一种文化、一种哲学和一种方法论。DevOps 的核心理念是通过开发和运维之间的紧密合作&#xff0c;实现快速交付、高质量和持续创新。本文将深入探讨 DevOps 文化的重要性、原则以及如何在团队中实…

3D模型轻量化引擎HOOPS平台助力3D开发实现大模型轻量化渲染、3D模型格式转换!

一、包含的软件开发工具包 HOOPS Web平台帮助开发人员构建基于Web的工程应用程序&#xff0c;提供高级3D Web可视化、准确快速的CAD数据访问和3D数据发布。 HOOPS Web平台包括三个集成软件开发工具包 (SDK)&#xff1a; &#xff08;1&#xff09;Web端3D可视化引擎 HOOPSCom…