最新要闻
- 飞利浦推出两款44.5英寸带鱼屏:1500R大曲率、75Hz刷新率
- 行驶中小车后备箱塞人露双脚!司机称其喝醉了:遭网友谴责
- 世界播报:低头族注意了!9岁女孩长期低头颈椎老如50岁
- 领1030元大额券:骆驼户外徒步鞋179元起大促
- 世界观点:亚马逊要求员工一周到岗工作3天:公司一度像个鬼城
- 天天热资讯!埃塞俄比亚政府和“提人阵”武装达成停火协议
- 天天新消息丨义乌自己的全球支付工具!义支付发布:打通16个币种
- 这待遇简直了!美团外卖进军香港 正疯狂招骑手:月入3万+
- 消息!手机套餐费太贵 韩国三大运营商被批:是否串通涨价
- 与剧版强烈反差!《三体》动画豆瓣跌至3.9分:官方已停播
- 特斯拉最入门车型国内现身?别被骗了:“披着狼皮的羊”而已
- 女子在桃李面包中吃出带锈刀片 官方:不存在使用该刀片的生产环节
- 环球快看:手机寒气仍存 5G巨头中兴被曝优化人员 回应称正常调整
- 世界观焦点:蔚来ET9要来了?号称“蔚来版迈巴赫” 屏幕开始横过来
- 掌权八年后上汽大众换帅:贾健旭接棒
- 大嘴猴50S精梳棉男士四角裤狂促:券后每条不到10元!
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
【Unity AR】<PokemonGo> AR精灵制作学习记录
一、实验摘要
《宝可梦GO》(pokemonGo)是一款能对现实世界中出现的宝可梦进行探索捕捉、战斗以及交换的游戏。玩家可以通过智能手机在现实世界里发现宝可梦,进行抓捕和战斗。玩家作为宝可梦训练师抓到的宝可梦越多会变得越强大,从而有机会抓到更强大更稀有的宝可梦。本次开发复刻了宝可梦GO的LBS地图系统、AR捕捉系统以及仓库系统。
二、实验过程
0.环境介绍
- 3.1
- Andriod SDK 、NDK、OpenJDK均来自于3.14版本安卓模块
1.游戏内容分析
【传统部分】
- 选择阵营,参与争夺
- 宠物捕捉
- 宠物升级进化
- 宠物PK
- 道具系统:收集,购买
【创新内容】
(相关资料图)
1、LBS:
- 增加与现实世界互动
- 增加玩家代入感
- 增加社交互动
2、AR
- 增加现实世界互动
- 形式新奇
2.项目设计
- app功能
- 用户登录
- LBS基于地理的服务功能
- AR捉宠功能
- 宠物仓库功能
- 数据更新与保存
- 制作流程及标准
- 以模块的形式做成独立的项目
- LBS使用GoMap插件
- AR使用Vuforia
- 宠物仓库
- UGUI制作登陆界面
- 资源
- 角色模型
- 精灵球、食物模型
- 卡通宠物模型
- UI美术
- 粒子特效及音效
3.unity实现LBS服务(Location Based Service)
- 先配置环境:3.1
- 安卓SDK(我用的是unity2021版本自带的sdk):导入GoMap插件包
- 在demo文件夹拷贝一个POI demo的场景
- build出一个安卓apk进行测试(注意在导出之前要更改player setting中的package name)
4.获取地图运营商密钥
- 在mapbox官网注册账号
- 点击token创建新的token
- 选择默认选项即可
- 注意,一开始申请注册账号的时候需要visa信用卡,就放弃了注册,但后来给我发送了确认邮件,我验证后就注册成功了
5.GoMap的图块机制
- tile buffer
图块缓冲越大,一次申请访问的地图就越大
- Coordinates / units Ratio
单位取真实世界的米
- Zoom level
值越大,地图比例越小。放大和缩小不会更改比例尺,意味着放再大单位还是取真实世界的米
6.GoMap的坐标机制
开始游戏时,世界中心坐标将会和场景中心坐标(0,0,0)匹配,并且不会变动,人物的行走会改变人物的位置坐标,这与普通地图SDK有所不同(普通地图SDK人物不产生位移,地图画面移动)
7.设置地图玩家角色
- 替换角色模型
修改角色比例与方向,再拖到Avatar下,将角色物体拖到Avtar物体的Move Avtar脚本的Avatar Figure上
- 让模型根据移动速度产生动画的变化
- 创建Animator文件夹(在Avatar_01下),创建Animator controller作为角色模型的控制器
- 将animation文件夹中的Idle、run、walk拖入控制器
- 将any state连接(Transition)三个动画
- 在any state中添加参数:三个trigger(idle、run、walk)
- 在三个连接中添加对应的条件(condition)
- 将控制器拖到模型Boy的Animator组件的controller中
- 写脚本控制动画
创建文件夹,创建脚本BoyControl.cs
内容如下:
using System.Collections;using System.Collections.Generic;using UnityEngine;public class BoyControl : MonoBehaviour {//储存动画控制器private Animator ator;//储存角色控制器private MoveAvatar moveAvatar;// Use this for initializationvoid Start () { //获取动画控制器 ator = gameObject.GetComponent(); //获取父物体的角色控制器 moveAvatar = transform.parent.GetComponent();}// Update is called once per framevoid Update () { //如果角色控制器当前的状态是 定义的待机状态 if(moveAvatar.animationState == MoveAvatar.AvatarAnimationState.Idle) { //而且当前动画不是待机动画,就将动画控制器的动画设置为待机 if(!ator.GetCurrentAnimatorStateInfo(0).IsName("Idle"))ator.SetTrigger("Idle"); } else if(moveAvatar.animationState == MoveAvatar.AvatarAnimationState.Run) { if (!ator.GetCurrentAnimatorStateInfo(0).IsName("Run")) ator.SetTrigger("Run"); } else if(moveAvatar.animationState == MoveAvatar.AvatarAnimationState.Walk) { if (!ator.GetCurrentAnimatorStateInfo(0).IsName("Walk")) ator.SetTrigger("Walk"); }}}
注意将main camera的target设置为boy模型,保证视角的正确
8.范围内生成随机点
- 指定预制体作为事件点的载体
- 获取角色坐标信息
- 定义随机点与角色距离范围
- 在距离范围内取一个随机坐标
- 在随机坐标的位置生成随机点预制体:新建一个cube改名为EventPoint,拖到创建的prefab文件夹
- 新建脚本cs:
using System.Collections;using System.Collections.Generic;using UnityEngine;public class InsPoint : MonoBehaviour { //储存地图角色 public GameObject Ava; //储存事件点预制体 public GameObject PrePoint; //储存距离范围最小点 public float MinDis = 3f; //储存距离范围最大点 public float MaxDis = 50f; //储存当前角色当前位置 private Vector3 v3Ava; // Use this for initialization void Start () { } // Update is called once per frame void Update () { } public void InsPointFuc() { //获取角色坐标位置 v3Ava = Ava.transform.position; //从距离范围内取一个随机距离值 float _dis = Random.Range(MinDis, MaxDis); //从原点为(0,0)随机获取一个方向的向量 Vector2 _pOri = Random.insideUnitCircle; //将该方向向量标准化 Vector2 _pNor = _pOri.normalized; //计算生成随机点的位置向量 随机点x坐标 随机点y坐标 Vector3 _v3Point = new Vector3(v3Ava.x + _pNor.x * _dis, 0, v3Ava.z + _pNor.y * _dis); //生成随机点的预制体 GameObject poiMark = Instantiate(PrePoint, _v3Point, transform.rotation); }}
- 新建一个空物体作为manager,删掉之前创建的eventpoint物体
- 将随机点生成脚本挂载manager上
- 将avtar、预制体prePoint拖到manager的脚本上
- 创建canvas,创建按钮(锚点右上角),添加manager中的随机点函数到按钮中
9.在地图上生成随机事件
- 随机事件:
- 生成小精灵
- 生成精灵球
- 生成食物
- 实现手段:
- 制作三个预制体
- 在随机点的脚本中随机选取事件
- 生成对应的预制体
- 具体流程
- 导入资源包食物和精灵球,放到package文件夹下
- 将资源包中prefab的蛋糕和精灵球拖到hierarchy修改比例后放到工程的prefab文件夹作为预制体(将精灵球刚体组件删掉)
- 创建一个圆柱暂时作为宠物
- 新建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public class PointEvent : MonoBehaviour { //分别储存精灵、精灵球、食物的预制体 public GameObject Pet; public GameObject Ball; public GameObject Food; //储存偏移量 private Vector3 offset; // Use this for initialization void Start () { int _randomEvent = Random.Range(0, 3);//随机选 0 1 2 if (_randomEvent == 0) insPet(); else if (_randomEvent == 1) insBall(); else if (_randomEvent == 2) insFood(); offset = new Vector3(0, 5f, 0); } // Update is called once per frame void Update () { } private void insPet() { Instantiate(Pet, transform.position + offset, transform.rotation); } private void insFood() { Instantiate(Food, transform.position + offset, transform.rotation); } private void insBall() { GameObject _ball = Instantiate(Ball, transform.position + offset, transform.rotation); //设置精灵球的角度 _ball.transform.localEulerAngles = new Vector3(-30f, 0, 0); }}
- 将脚本挂载到eventPoint预制体上,并将蛋糕、精灵球和精灵(现在是圆柱)挂载在脚本上
- 删掉eventPoint预制体上的meshrender和cube组件(不用显示)
- 新建脚本cs控制随机生成的物体的动态效果
using System.Collections;using System.Collections.Generic;using UnityEngine;public class MoveEffect : MonoBehaviour { //设置起始弧度参数 private float radian = 0f; //弧度变化值 private float perRad = 0.03f; //位移偏移量 private float add = 0f; //储存物体生成时的真实坐标 private Vector3 posOri; // Use this for initialization void Start () { //获取初始位置的坐标信息,否则就是0,0,0 posOri = transform.position; } // Update is called once per frame void Update () { //弧度值变化 radian += perRad; //计算偏移量 add = Mathf.Sin(radian); //更改物体位置 transform.position = posOri + new Vector3(0, add, 0); //控制物体旋转(我试了下,无论是space.self还是world都是绕自己y轴旋转 怪) transform.Rotate(0, Time.deltaTime * 25f, 0, Space.World); }}
10.生成精灵
- 实现方式:
- 制作精灵预制体
- 使用resource加载预制体
编写随机生成事件
- 制作流程
- 导入精灵包
- 将精灵包文件夹中prefab->low poly -> naked中的预制体拖到hierarchy中,修改大小,改名字为01
- 在精灵文件夹中新进animator文件夹,并创建animator controller
- 将Animation文件夹中的idle basic拖到刚创建的controller中
- 将controller添加到01上
- 在根目录创建Resource文件夹,并在里面创建Pets文件夹,将01拖到pets文件夹中形成预制体
- 将场景中的01删掉
- 以相同的方式添加多个精灵
- 创建脚本cs生成宠物
using System.Collections;using System.Collections.Generic;using UnityEngine;public class InsPets : MonoBehaviour { //创建数组 存储精灵预制体 private GameObject[] pets; void Awake() { pets = Resources.LoadAll("Pets"); } // Use this for initialization void Start () { InsPet(); } // Update is called once per frame void Update () { } private void InsPet()//注意不要和脚本同名 { //随机生成一个精灵序号 int _petIndex = Random.Range(0, pets.Length); //生成精灵 Instantiate(pets[_petIndex], transform.position, transform.rotation); }}
- 将脚本拖到之前创建的精灵预制体(圆柱),删掉圆柱的mesh
11.随机事件与角色互动
- 互动事件:
- 角色碰撞到食物或者精灵球时,对应UI数字增加
- 角色碰到精灵时,出现面板询问是否捕捉
- 实现方式:
- 给事件添加碰撞器,使用触发碰撞器
- 给角色增加”Avatar“标签,并增加碰撞器
- 判断进入事件点碰撞器的物体标签是否为Avatar
- 具体流程:
- 搭建UI,在canvas新建image至左上角,命名为im_Ball,锚点设置在左上角
- 在im_Ball上添加text_Mark 作为×号和text_num作为数字,将锚点放在右侧(因为符号和数字在父物体img的右侧)
- 复制im_Ball做成food
- 创建Pics文件夹再在里面创建UI文件夹,将食物和精灵球图片拖入,将图片类型改为sprite
- 新建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class UI_Mgr_02 : MonoBehaviour { //储存精灵球和食物的数量 public Text Tx_BallNum; public Text Tx_FoodNum; //申请静态类变量,便于其他脚本直接调用该类的方法 public static UI_Mgr_02 Instance; void Awake() { Instance = this; } public void AddBallNum() { int _num = int.Parse(Tx_BallNum.text); _num++; Tx_BallNum.text = _num.ToString(); } public void AddFoodNum() { int _num = int.Parse(Tx_FoodNum.text); _num++; Tx_FoodNum.text = _num.ToString(); }}
- 创建碰撞脚本cs:
using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ball_Find : MonoBehaviour { //碰撞函数 void OnTriggerEnter(Collider other) { if(other.tag == "Avatar")//与标签为“Avatar”的物体碰撞 { UI_Mgr_02.Instance.AddBallNum(); //销毁物体 Destroy(gameObject); } }}
- 创建空物体UI_mgr并挂载上cs,将Im_Ball和Im_Food的数字text挂载到UI_Mgr_02.cs变量上(不要挂maker,不然会报错嗷)
- 更改ball的预制体,添加刚体组件并把限制constraint中的freeze全打上勾,使其不受物理的原因而移动变化,挂上ballFind脚本,将碰撞器的is trigger勾上
- 为boy物体添加”Avatar“标签和box_collider
- 食物同理
12.触发精灵捕捉面板
- 面板内容:
- 提示信息:精灵种类
- 开始捕捉
- 放弃捕捉
- 流程:
- 在canvas上添加img_catch
- 在img_catch添加三个text分别是恭喜、发现宠物和宠物名字,再添加两个按钮,分别为捕捉和放弃
- 扩充cs
//储存捕捉面板 public GameObject Im_Catch //设置捕捉面板的激活状态 public void SetIm_Catch(bool bl) { Im_Catch.SetActive(bl); };将捕捉面板物体挂载到ui管理物体的脚本上创建脚本csusing System.Collections;using System.Collections.Generic;using UnityEngine;public class Pet_Find : MonoBehaviour { void Start() { //精灵初始朝向角色 transform.LookAt(GameObject.FindGameObjectWithTag("Avatar").transform); } void OnTriggerEnter(Collider other) { if (other.tag == "Avatar")//与标签为“Avatar”的物体碰撞 { UI_Mgr_02.Instance.SetIm_Catch(true); //销毁物体 Destroy(gameObject); } }}
- 为所有精灵添加碰撞体和刚体,像食物和精灵球那样调整,挂上cs
- 为面板的放弃捕捉按钮添加im-mgr物体,并选上SetIm_Catch方法
12.LBS部分重构
- 在Resources文件夹中新建文件夹ball和foods
- 将prefab文件夹中的对应物体拖入刚新建的文件夹
- 重构cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public class PointEvent : MonoBehaviour { //分别储存精灵、精灵球、食物的预制体 private GameObject[] Pets; private GameObject[] Balls; private GameObject[] Foods; //储存偏移量,让生成的物体高一点 private Vector3 offset; // Use this for initialization void Awake() { Balls = Resources.LoadAll("Balls"); Foods = Resources.LoadAll("Foods"); Pets = Resources.LoadAll("Pets"); } void Start () { offset = new Vector3(0, 5f, 0); int _randomEvent = Random.Range(0, 3);//随机选 0 1 2 if (_randomEvent == 0) insPet(); else if (_randomEvent == 1) insBall(); else if (_randomEvent == 2) insFood(); } // Update is called once per frame void Update () { } private void insPet() { // 随机生成一个精灵序号 int _petIndex = Random.Range(0, Pets.Length); Instantiate(Pets[_petIndex], transform.position + offset, transform.rotation); } private void insFood() { int _foodIndex = Random.Range(0, Foods.Length); Instantiate(Foods[_foodIndex], transform.position + offset, transform.rotation); } private void insBall() { int _ballIndex = Random.Range(0, Foods.Length); GameObject _ball = Instantiate(Balls[_ballIndex], transform.position + offset, transform.rotation); //设置精灵球的角度 _ball.transform.localEulerAngles = new Vector3(-30f, 0, 0); }}
- 删掉cs和Pet预制体
- 使用代码为预制体增加组件,继续重构cs,修改脚本中的insBall和insFood函数即可
private void insFood() { int _foodIndex = Random.Range(0, Foods.Length); GameObject _food = Instantiate(Foods[_foodIndex], transform.position + offset, transform.rotation); //添加碰撞体组件 _food.AddComponent(); //勾选isTrigger _food.GetComponent().isTrigger = true; //设置碰撞体 _food.GetComponent().center = new Vector3(0, 0, 0); _food.GetComponent().size = new Vector3(0.33f, 0.3f, 0.33f); //添加刚体组件 _food.AddComponent(); //更改刚体组件的限制 _food.GetComponent().constraints = RigidbodyConstraints.FreezeAll; //添加moveeffect组件和find组件 _food.AddComponent(); _food.AddComponent(); } private void insBall() { int _ballIndex = Random.Range(0, Foods.Length); GameObject _ball = Instantiate(Balls[_ballIndex], transform.position + offset, transform.rotation); //设置精灵球的角度 _ball.transform.localEulerAngles = new Vector3(-30f, 0, 0); //添加球形碰撞体组件 _ball.AddComponent(); //勾选isTrigger _ball.GetComponent().isTrigger = true; //设置碰撞体大小 _ball.GetComponent().radius = 0.011f; //添加刚体组件 _ball.AddComponent(); //更改刚体组件的限制 _ball.GetComponent().constraints = RigidbodyConstraints.FreezeAll; //添加moveeffect组件和find组件 _ball.AddComponent(); _ball.AddComponent(); }
13.配置Vuforia
- 在周围随机出现精灵
- 发射精灵球
- 创建ARCamera时导入Vuforia的包
- 切换至安卓平台
- 更改playersetting中的XR settings 勾选Auforia
- 为ARcamera添加密钥
- 更改ARcamera下VuforiaConfiguraton的Device Tracker,勾选Track Device Pose
- 导出apk测试(导之前更改playersetting 里的名称还有other setting里的Android TV要取消掉)
14.规划AR模块的制作(生成精灵球)
- 功能流程
- 读取数据
- 配置精灵球
- 配置精灵
- 可以发射精灵球
- 触发精灵捕捉成功
- 流程:
- 创建cube作为ballpos,调整位置与大小并拖给ARcamera作为子物体
- 创建一个球体作为精灵球,调整大小位置,删除碰撞器组件(因为要在代码中动态添加)
- 在Resources文件夹中创建Balls文件夹,并将球体拖进去作为预制体(在合并项目时,同名文件夹也会合并)
- 创建scripts文件夹再创建AR文件夹,在里面创建cs脚本
using System.Collections;using System.Collections.Generic;using UnityEngine;public class ARBallCtrl : MonoBehaviour { //储存精灵球预制体 private GameObject[] balls; //精灵球生成位置 public Transform PosInsBall; // Use this for initialization void Start () { //加载路径在“Resources/Balls”的预制体 balls = Resources.LoadAll("Balls"); insBall(); } // Update is called once per frame void Update () { } private void insBall() { Instantiate(balls[0], PosInsBall.position, PosInsBall.rotation); //设置精灵球的父物体 为了保证发射前始终在屏幕的固定位置 _ball.transform.SetParent(PosInsBall); //为精灵球添加球体碰撞器组件 _ball.gameObject.AddComponent(); }}
- 新建脚本管理器,添加cs脚本,将BallPos拖到脚本中
15.发射精灵球
- 发射效果
- 用手指滑动发射
- 发射后精灵球运动的过程类似于真实物体抛射
- 可以碰到精灵
- 受重力影响下落
- 落在地面上
- 制作思路
- 有碰撞器
- 有刚体
- 计算手指滑动方向
- 计算手指滑动距离
- 计算手指滑动的速度
- 注意事项
- 有的滑动方向不能触发精灵球发射
- 滑动距离不够不能触发精灵球发射
- AR交互对算法的影响
- 流程
- 创建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public class ARShootBall : MonoBehaviour { //设置给精灵球的力度 public float FwdForce = 200f; //设置夹角的参照数值 public Vector3 StanTra = new Vector3(0, 1f, 0); //判断手指是否碰到精灵球位置 private bool blTouched = false; //判断精灵球是否发射 private bool blShooted = false; //手指滑动起始点 private Vector3 startPosition; //手指滑动的终点 private Vector3 endPosition; //手指滑动的距离 private float disFlick; //手指滑动的速度 private float speedFlick; //手指滑动的时间(帧数) private int timeFlick = 0; //手指滑动的偏移向量 private Vector3 offset; //记录主摄像机 private Camera cameraMain; // Use this for initialization void Start () { //赋值为主摄像机 cameraMain = Camera.main; } // Update is called once per frame void Update () { if(blTouched)//如果按在小球上,允许检测手指的滑动 { slip(); } } //重置参数 private void resetVari() { //起始位置为手指按下的位置 startPosition = Input.mousePosition; } //当鼠标/手指 按下/触摸到 脚本挂载的物体 void OnMouseDown() { if(blShooted == false)//如果没发射 { blTouched = true;//允许检测手指滑动 } } //计算手指的滑动 private void slip() { timeFlick += 25;//时间每帧增加25 if(Input.GetMouseButtonDown(0))//当手指按下的时候 { resetVari();//重置参数 } if(Input.GetMouseButton(0))//当手指一直按下的时候 { //将终点位置坐标刷新为手指位置 endPosition = Input.mousePosition; //获取手指在世界坐标上的偏移量 offset = cameraMain.transform.rotation * (endPosition - startPosition); //计算手指滑动的距离 disFlick = Vector3.Distance(endPosition, startPosition); } if(Input.GetMouseButtonUp(0))//当手指抬起来 { //计算速度 speedFlick = disFlick / timeFlick; //手指触摸设置为false blTouched = false; //时间重置 timeFlick = 0; //如果手指滑动距离超过20而且是向上滑动 if(disFlick > 20 && endPosition.y- startPosition.y > 0) { //允许发射 shootBall(); } } } //发射精灵球 private void shootBall() { //给精灵球添加刚体组件 transform.gameObject.AddComponent(); //使用局部变量储存刚体组件,便于后续操作 Rigidbody _rigBall = transform.GetComponent(); //给精灵球一个初始速度 _rigBall.velocity = offset.normalized * speedFlick; //为精灵球添加力度,方向为朝着屏幕前方(即为摄像机的前方) _rigBall.AddForce(cameraMain.transform.forward * FwdForce); //让精灵球旋转 _rigBall.AddTorque(transform.right); //设置精灵球阻力 _rigBall.drag = 0.5f; //设置为发射状态 blShooted = true; //让发射出去的球脱离父物体 transform.parent = null; //执行协程函数 StartCoroutine(LateInsBall()); } //协程函数 IEnumerator LateInsBall() { yield return new WaitForSeconds(0.2f); //延迟两秒执行 //生成新的精灵球 ARBallCtrl.Instance.InsNewBall(); }}删掉BallPos的碰撞器和渲染插件,只需要为精灵球提供位置将cs的脚本生成新精灵球函数insBall改为公有函数方便调用,并修改: //申请脚本静态类对象 public static ARBallCtrl Instance; public void InsNewBall()
- 创建材质文件夹,新建一个带颜色的材质赋给cube方便区分球和cube
16.AR场景中生成精灵
- 流程
- 升高ARcamera的y坐标至6
- 在Resources文件夹新建Pets文件夹,将cube拖入作为预制体
- 在ARcamera的周围创建多个生成点cube
- 删除cube的碰撞器和网格(因为只需要提供位置)
- 新建空物体PosMgr统一管理生成点(作为生成点的父物体)
- 新建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public class ARInsPet : MonoBehaviour { //储存生成精灵位置的物体 public Transform[] TraPos; //储存精灵 private GameObject[] pets; // Use this for initialization void Start () { pets = Resources.LoadAll("Pets"); } // Update is called once per frame void Update () { } public void InsPet() { //随机选一个生成的位置点 int _index = Random.Range(0, TraPos.Length); //获取该位置的transform Transform _tra = TraPos[_index]; //在该位置生成精灵 Instantiate(pets[0], _tra.position, _tra.rotation); }}
- 将脚本挂载到PosMgr上,并将五个位置点cube放在脚本上
17.地图模块与AR模块的整合
- 目标
- 将两个项目文件合并
- 测试调整整合后的项目场景
- 场景的跳转
- 流程
- 打开pokemonGo_map项目文件,导出包
- 复制pokemonGo_AR项目文件改名为pokemonGo_main,作为主项目文件
- 导入包
- 切换场景到map
- 测试,为防止鼠标移动到按钮时相机发生变化,新建一个ARcamera并关掉所有组件
- 切换场景到AR
- 由于在文件合并时,相同文件夹的内容也会合并,同名的预制体会覆盖之前的预制体,所以要调整Ball
- 制作捕捉精灵时的跳转函数,修改UI_Mgr脚本
using UnityEngine.SceneManagement;public void Btn_GoARScn() { SceneManager.LoadScene("AR_Scn"); }
- 要将场景添加到build setting中
- 切换至AR_Scn,创建一个脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.SceneManagement;public class ARUI_Mgr : MonoBehaviour { public void Btn_GoMapScn() { SceneManager.LoadScene("Map_Scn"); }}
18.Resource中的预制体标准
- 标准
同一文件夹中的预制体类型要相同
同一批预制体的配置信息保持一致(坐标轴位置、旋转角度、模型朝向、导出前的操作)
外部导入的资源在进入unity前就要统一标准
- 流程
- 修改ARInsPet脚本,使精灵大小合适,在生成精灵函数最后进行修改
//在该位置生成精灵 GameObject _pet = Instantiate(pets[0], _tra.position, _tra.rotation); //调整缩放比例 _pet.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f) ;
- 将BallPos 的scale全改为1
- 修改cs脚本中生成精灵球函数最后加上
//设置精灵球的大小为25倍 _ball.transform.localScale = new Vector3(25f, 25f, 25f);用代码来调整碰撞器的缩放比,并添加发射精灵球脚本 //为精灵球添加发射精灵球脚本 _ball.gameObject.AddComponent(); //设置精灵球的大小为25倍 _ball.transform.localScale = new Vector3(25f, 25f, 25f); //获取碰撞器并更改半径 _ball.GetComponent().radius = 0.01f;
19.数据的全局管理
- 流程
- 切换至map场景,将img_Ball拖入Prefeb文件夹形成预制体
- 回到AR场景,将预制体拖到场景中
- 在脚本文件夹,创建Map文件夹,把之前的脚本拖到Map中
- 再新建一个Static文件夹,用来存放全局脚本
- 新建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public static class StaticData { //精灵球初始数量 public static int BallNum = 5;}
- 修改UI_Mgr脚本
//精灵球数量 public Text Tx_BallNum; //单例 public static ARUI_Mgr Instance; void Awake() { Instance = this; } public void UpdateUIBallNum() { Tx_BallNum.text = StaticData.BallNum.ToString(); }
- 修改ARBallCtrl脚本
void Start () { //加载路径在“Resources/Balls”的预制体 balls = Resources.LoadAll("Balls"); //刷新精灵球数量 ARUI_Mgr.Instance.UpdateUIBallNum(); InsNewBall(); } public void InsNewBall() { if(StaticData.BallNum > 0) { GameObject _ball = Instantiate(balls[0], PosInsBall.position, PosInsBall.rotation); //设置精灵球的父物体 为了保证发射前始终在屏幕的固定位置 _ball.transform.SetParent(PosInsBall); //为精灵球添加球体碰撞器组件 _ball.gameObject.AddComponent(); //为精灵球添加发射精灵球脚本 _ball.gameObject.AddComponent(); //设置精灵球的大小为25倍 _ball.transform.localScale = new Vector3(25f, 25f, 25f); //获取碰撞器并更改半径 _ball.GetComponent().radius = 0.01f; } }
- 修改精灵球发射脚本ARShootBall中的发射函数,在发射结束后添加以下两行代码
//减少精灵球数量 StaticData.BallNum--; //更新UI ARUI_Mgr.Instance.UpdateUIBallNum();
- 回到地图场景
- 修改cs脚本,增加start函数,初始化场景精灵球数量,修改增加精灵球数量
void Start() { Tx_BallNum.text = StaticData.BallNum.ToString(); } public void AddBallNum() { StaticData.BallNum++; Tx_BallNum.text = StaticData.BallNum.ToString(); }
- 保存精灵的全局变量:修改cs脚本
//当前正要捕捉的精灵序号 public static int CatchingPetIndex;
- 修改Pet_Find脚本,修改void OnTriggerEnter(Collider other)函数
void OnTriggerEnter(Collider other) { if (other.tag == "Avatar")//与标签为“Avatar”的物体碰撞 { //显示捕捉面板 UI_Mgr_02.Instance.SetIm_Catch(true); //当与角色碰撞时,更改全局变量:要捕捉的精灵序号 StaticData.CatchingPetIndex = PetIndex; //销毁物体 Destroy(gameObject); } }
- 修改PointEvent脚本的生成精灵函数
private void insPet() { // 随机生成一个精灵序号 int _petIndex = Random.Range(0, Pets.Length); //储存随机生成的精灵 GameObject _pet = Instantiate(Pets[_petIndex], transform.position + offset, transform.rotation); //更新精灵的序号 _pet.GetComponent().PetIndex = _petIndex; }回到AR场景,修改ARInsPet脚本中的InsPet函数。并修改start函数 //在该位置生成要捕捉的精灵 GameObject _pet = Instantiate(pets[StaticData.CatchingPetIndex], _tra.position, _tra.rotation);void Start () { pets = Resources.LoadAll("Pets"); //生成精灵 InsPet();}
- 删掉之前生成精灵的按钮
20.精灵捕捉的处理
- 功能效果
- 精灵播放被捕捉到的动画
- 精灵消失
- 显示捕捉成功界面
- 流程
- 打开PetsAnimator
- 打开Animation_FBX文件夹,将Hurt_die动画和jump_basic动画拖到Animator中
- 连接:Any State -> hurt -> jump
- 添加一个Trigger参数Catched
- 将Any State -> hurt的连线condition添加参数Catched
- 回到AR场景,在canvas上创建一个Panel组件Pn_Catched,锚点在四角
- 在panel上添加输入框
- 新建两个按钮,一个是确认,一个是放生
- 修改Mgr脚本,添加以下内容
//储存面板 public GameObject PnCatched; public void Show_PnCatched() { PnCatched.SetActive(true); }
- 给精灵球添加Ball标签
- 修改PetFind脚本
//延迟两秒显示捕捉面板并销毁精灵 IEnumerator ShowCatchedPn() { yield return new WaitForSeconds(2f); //显示捕捉面板 ARUI_Mgr.Instance.Show_PnCatched(); //销毁精灵物体 Destroy(transform.gameObject); } //播放被捕捉的动画 private void playCatched() { transform.GetComponent().SetTrigger("Catched"); } void OnTriggerEnter(Collider other) { if (other.tag == "Avatar")//与标签为“Avatar”的物体碰撞 { //显示捕捉面板 UI_Mgr_02.Instance.SetIm_Catch(true); //当与角色碰撞时,更改全局变量:要捕捉的精灵序号 StaticData.CatchingPetIndex = PetIndex; //销毁物体 Destroy(gameObject); } if(other.tag == "Ball") { //播放动画 playCatched(); //显示面板 ShowCatchedPn(); } }
21.精灵命名
- 流程
- 在脚本文件夹中创建Pet文件夹,新建PetSave脚本
using System.Collections;using System.Collections.Generic;using UnityEngine;public class PetSave{ //记录精灵名字 private string strName = "未命名宠物"; //记录精灵模型的序号 private int petIndex = 0; //精灵名字属性 public string PetName { get { return strName; } set { strName = value; } } //精灵序号属性 public int PetIndex { get { return petIndex; } set { petIndex = value; } } //构造函数 public PetSave(string name, int index) { PetName = name; PetIndex = index; }}
- 修改全局数据脚本StaticData
//申请列表储存捕捉到的精灵 public static List PetList = new List(); /// /// 向全局数据的精灵列表中添加精灵 /// /// 精灵的属性类 public static void AddPet(PetSave petSave) { PetList.Add(petSave); }
22.精灵仓库的制作
- 流程
- 新建Scn场景
- 创建Text,修改canvas的canves Scaler中mode为Scale with the screen Size并修改分辨率为1080*1920
- 调整text大小尺寸,命名为Tx_Title并将锚点放在左上角
- 新建按钮放在右上角,返回
- 新建画布Cav_world,修改canves Scaler中mode为Scale with the screen Size并修改分辨率为1080*1920
- 在Cav_world上新建panel调整大小
- 在panel上新建text组件保存信息:宠物名称和宠物类型
- 添加image用来储存精灵图
- 再复制两个panel
- 将摄像机模式改为正交模式并调整相机大小
- 将Cav_World的canvas组件中Render Mode改为Camera,并将main camera拖到Render camera上
- 创建三个空物体储存精灵的位置
- 新建空物体Pos_Mgr管理这三个位置
- 在脚本文件夹中新建文件夹Store,创建新脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public class StoreInsPet : MonoBehaviour { //储存展示的位置 public Transform[] Pos; //储存所有精灵预制体 private GameObject[] pets; //储存展示的精灵 private GameObject[] petsShow = new GameObject[3]; void Awake() { pets = Resources.LoadAll("Pets"); } // Use this for initialization void Start () { InsPet(); } // Update is called once per frame void Update () { } public void InsPet() { // 获取精灵数量 int _petNum = StaticData.PetList.Count; //如果仓库中有精灵 if(_petNum > 0) { for(int i = 0; i < 3; i++) { //如果i大于了最后一个精灵的序号 if (i > (_petNum - 1)) return; //储存精灵信息 PetSave _petInfo = StaticData.PetList[i]; //生成精灵 Instantiate(pets[_petInfo.PetIndex], Pos[i].position, Pos[i].rotation); //获取精灵的名字 string _petNm = _petInfo.PetName; //更新精灵的名字 StoreUIMgr.Instance.UpdatePetNm(i, _petNm); } } }}
- 新建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class StoreUIMgr : MonoBehaviour { //显示精灵名字的text public Text[] PetName; //显示精灵类型的text public Text[] PetType; //单例 public static StoreUIMgr Instance; void Awake() { Instance = this; } //更新精灵的名字函数 public void UpdatePetNm(int index, string strName) { PetName[index].text = strName; }}
- 新建一个空物体Mgr,并将刚才创建的两个脚本挂载上去
- 将三个位置物体挂载到Mgr的StoreInsPets下,三个名字和类型text挂载到Mgr的StoreUIMgr
- 切换到AR场景,在ARUI_Mgr脚本的Btn_Yes()函数最后加上跳转场景的代码
//跳转到仓库界面 SceneManager.LoadScene("Store_Scn");
- 给确定按钮绑定上Mgr物体上的ARUI_Mgr脚本的Btn_Yes()函数
- 由于精灵仓库场景中ARcamera的影响使得精灵显示不正常,新建一个ARcamera,关掉组件
23.完善
场景切换设计:
- 地图场景:
- 可随时跳转到精灵仓库
- 碰到精灵时可跳转到AR场景
- AR场景:
- 随时跳转到精灵仓库
- 捕捉成功跳转到精灵仓库
- 随时跳转到地图场景
- 放弃捕捉时跳转到地图场景
- 仓库场景
- 随时跳转到地图场景
流程
- 修改StoreUIMgr脚本,添加场景切换函数
using UnityEngine.SceneManagement; //跳转到地图场景 public void Btn_ToMap() { SceneManager.LoadScene("Map_Scn"); }
- 给返回按钮添加Mgr物体,选择StoreUIMgr脚本的Btn_ToMap()函数
- 返回Map场景,复制扫描按钮,命名为精灵仓库
- 修改UI_Mgr_02脚本,添加跳转场景函数
- 回到AR场景,复制回到地图按钮,命名精灵仓库
- 修改ARUI_Mgr脚本,添加跳转到精灵仓库和放生的函数
完善
- AR场景
- 仓库场景
流程
- 修改ARInsPet脚本,将ARcamera挂载到脚本上
//储存相机位置public Transform CameraTra;
InsPet()中最后添加:
//让生成的精灵面向我们_pet.transform.LookAt(new Vector3(CameraTra.position.x, _pet.transform.position.y, CameraTra.position.z));
- 切换到精灵仓库场景,添加宠物类型显示功能,修改StoreUIMgr脚本,添加刷新精灵种类的函数
/// /// 刷新精灵类型函数 /// /// 精灵序号 /// 精灵类型名字 public void UpadatePetType(int index, string strType) { Tx_PetType[index].text = strType; }
- 再StaticData脚本中定义一个静态函数,用来根据序号返回精灵的种类
public static string GetPetName(int index) { if(index == 0) { return "不灭狂雷"; } else if(index == 1) { return "牛头酋长"; } else if (index == 2) { return "赤小兔"; } else if (index == 3) { return "沙漠皇帝"; } else if (index == 4) { return "乌迪尔"; } else if (index == 5) { return "傲之追猎者"; } else if (index == 6) { return "麻花藤"; } else if (index == 7) { return "沙漠死神"; } else { return "不屈之枪"; } }
- 修改StoreInsPet脚本,在InsPet函数最后添加刷新类型的函数
/// /// 刷新精灵类型函数 /// /// 精灵序号 /// 精灵类型名字 public void UpadatePetType(int index, string strType) { Tx_PetType[index].text = strType; }
24.美术及音效
- 新建一个登录场景Login_Scn
- 为防止Vuforia的影响,先创建一个ARcamera,关掉所有组件
- 在Pic文件夹中创建Icons、Btn、InputF、BG、Words文件夹,把之前的图标图片放到Icons中,导入美术资源,设置为sprite,记得apply
- 创建canvas,再创建一个panel作为背景
- 将导入的背景图片设置为panel的源图像,并将不透明度调为100%
- 在背景Panel上新建image,并选择导入的美术资源,设置锚点(就近原则)
- 新建inputfile,一个输入用户名,一个输入密码
- 新建一个登录按钮
- 新建几个image装饰一下
- 为登录添加跳转功能,在脚本文件夹新建Login文件夹,创建Login_UI脚本
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.SceneManagement;public class Login_UI : MonoBehaviour { public void Login() { SceneManager.LoadScene("Map_Scn"); }}
- 新建管理器物体,将脚本挂载到管理器上,为按钮添加跳转功能
- 新建一个文件夹Audios,存放音乐与音效,导入音乐包
- 创建一个空物体管理声音AuMgr(用来添加按钮的音源),在Mgr添加audio source组件,勾选loop,选择一个bgm导入
- 创建一个脚本Login_Au
using System.Collections;using System.Collections.Generic;using UnityEngine;public class Login_Au : MonoBehaviour { private AudioSource Au; //静态单例 public static Login_Au Instance; void Awake() { Instance = this; } // Use this for initialization void Start () { Au = GetComponent(); } public void Btn_Play() { Au.Play(); }}
- 切换到Map场景,换掉场景中所有按钮图片
- 为Mgr添加音源
- 新建脚本Map_Au
using System.Collections;using System.Collections.Generic;using UnityEngine;public class Map_Au : MonoBehaviour { private AudioSource Au; //静态单例 public static Map_Au Instance; void Awake() { Instance = this; } // Use this for initialization void Start() { Au = GetComponent(); } public void Btn_Play() { Au.Play(); }}using System.Collections;using System.Collections.Generic;using UnityEngine;public class Map_Au : MonoBehaviour { private AudioSource Au; //静态单例 public static Map_Au Instance; void Awake() { Instance = this; } // Use this for initialization void Start() { Au = GetComponent(); } public void Btn_Play() { Au.Play(); }}using System.Collections;using System.Collections.Generic;using UnityEngine;public class Map_Au : MonoBehaviour { private AudioSource Au; //静态单例 public static Map_Au Instance; void Awake() { Instance = this; } // Use this for initialization void Start() { Au = GetComponent(); } public void Btn_Play() { Au.Play(); }}
- 新建空物体AuMgr,挂载声音控制脚本
- 给每个按钮的响应函数内添加一条:
//播放按钮声音 Map_Au.Instance.Btn_Play();
- 切换到AR界面
- 更改UI按钮、添加BGM、按钮音效、AR场景的AU管理脚本
- 切换到精灵仓库界面,重复上面的操作
25.游戏保存功能
- 导入Easy Save包
- 切换到仓库场景,复制一个按钮为保存
- 在静态脚本文件夹创建脚本cs
using System.Collections;using System.Collections.Generic;using UnityEngine;public static class SaveAndLoad{ public static void Save() { //保存精灵球数量 ES3.Save("BallNum", StaticData.BallNum); //保存已捕捉的精灵的数量 ES3.Save("PetNum", StaticData.PetList.Count); //保存每一个已捕捉到的精灵信息 for(int i = 0; i < StaticData.PetList.Count; i++) { //储存每一个精灵的名字 ES3.Save("PetName" + i.ToString(), StaticData.PetList[i].PetName); //储存每一个精灵的序号 ES3.Save("PetIndex" + i.ToString(), StaticData.PetList[i].PetIndex); } } //加载函数 public static void Load() { if(ES3.KeyExists("BallNum") && ES3.KeyExists("PetNum")) { //读取精灵球数量 StaticData.BallNum = ES3.Load("BallNum"); //读取精灵数量 int _petNum = ES3.Load("PetNum"); //读取每个精灵的信息 for(int i = 0; i < _petNum; i++) { string _petName = ES3.Load("PetName" + i.ToString()); int _petIndex = ES3.Load("PetIndex" + i.ToString()); StaticData.AddPet(new PetSave(_petName, _petIndex)); } } }}
- 修改StoreUI_Mgr脚本,添加保存按钮的函数,并给按钮加上函数
public void Btn_Save() { Store_Au.Instance.Btn_Play(); SaveAndLoad.Save(); }将加载函数加载登录的按钮函数内(在LoginUI脚本中),登录即读取 public void Login() { Login_Au.Instance.Btn_Play(); SaveAndLoad.Load(); SceneManager.LoadScene("Map_Scn"); }
三、项目界面
登录界面:
该界面用户名密码其实并没有效果, 点击登录,会自动读取上一次的记录并跳转到地图界面
地图界面:
打开app后允许gps功能,玩家在现实世界移动,游戏世界的角色也会跟着移动,地图为学校。左上方显示精灵球和食物数量,通过点击右上角的扫描区域,可以在周围随机刷新精灵球、食物和精灵。玩家碰撞到精灵球、食物,数量会增加,碰撞到精灵弹出捕捉面板
AR战斗界面:
玩家手持手机环顾四周,寻找精灵,找到精灵后,通过滑动手指的动作来投掷精灵球,碰撞到精灵便捕捉成功,捕捉成功后显示成功面板,起名确认后跳转到仓库界面,否则跳转到地图界面
仓库界面:
玩家捕捉的精灵将显示在该界面,点击保存将会保存当前精灵,下次登录时会自动读取
四、实验中遇到的问题
- 环境配置问题
在上一次实验时,我就想导出安卓包,但出现了SDK的各种问题,先是找不到SDK,然后是版本低,不能够支持最新版本的Vuforia,尝试了很多办法。最开始解决找不到SDK时,把Unity卸载了重装,但安装又出现了问题,又看博客的单独安装,就是把各种需要的模块、包、SDK等等单独下载,再整合一起,找到SDK了,但版本又不支持,于是又重新下载了最新的Unity,但最新的Unity的安卓sdk版本依旧不支持最新版本的Auforia,于是乎,便排查出不是Unity的问题,而是安卓包和Auforia版本不匹配的问题,最后又下回了Unity2020的稳定版本,导入Vuforia后,降低Vuforia的版本后能够成功导出安卓包了。但此次实验的环境是Unity2017,因为Unity2017自带Vuforia,更方便,根本不用担心安卓包和Auforia版本不匹配的问题。
- 实验开始阶段MapBox的密钥申请问题
MapBox以往和Vuforia是一样的,只要注册登录就可以申请密钥,使用其LBS插件的功能,但今年有所变化,注册账号必须要visa等国际银行卡,但这种银行卡不是随随便便轻而易举就能办下来的,所以正在我一筹莫展之时,MapBox一小时后还是给我发来了验证邮件,没有银行卡还是注册账号成功了。
中间有时会发生一些小bug,不过无足轻重很快就能调整好,比较麻烦的问题是最后的canvas布局问题
- AR部分丢出精灵球失效
经过仔细查找发现是发射脚本ARShootBall只放的层级组件有问题,导致只能对第一个精灵球进行控制,第一个销毁后新生成的失效了无反应,最后通过将脚本挂载到精灵器预制体上顺利解决。
- UI界面问题
导到手机上测试时,发现仓库界面的精灵信息面板发生重叠,发现是分辨率的不同导致的,电脑上设置的分辨率是1080*1920,而我的手机估计是10:21的比例,所以有所不同,于是查阅博客对canvas scaler的描述,更改canvas scaler的UI Scale Mode为 Scale With Screen Size,并根据情况更改Match即可,像上面这个,宽度其实不用缩放,只需要根据手机的高度来缩放UI的高度即可,因此Match拉到1。
五、个人收获
本项目的技术难点不算多,大多学习到的是项目的管理和游戏基本功能在Unity中的实现。
- 项目管理方式,为不同模块创建不同的文件夹存放脚本、预制体、资源等内容。在场景中使用不同的Manager空物体来管理场景中的脚本事件,比如Au_Mgr管理音频的脚本事件,Mgr管理场景主要事件的脚本,UI_Mgr管理界面的事件脚本。
- 项目的合并,相同文件夹会自动合并,在开发不同模块的功能时,如AR场景和地图场景可以分开开发,开发完毕后再合并项目。
- 使用Unity的Resources文件夹加载预制体,不用再一个一个手动拖拽添加了,节省了不少人力。
- 强力插件的运用,本项目就使用了GoMap插件和Easy Save插件,使用强力的插件能够节约开发时间
- 静态脚本以及单例的运用。使用静态脚本储存全局变量,可以保存场景切换时的数据,利用静态脚本实现保存和读取的功能,本项目使用了插件,只用实现保存和读取功能能即可,在GameMaker Studio2中还需要多两步,就是保存为json文件和读取json文件,整体流程很相似。单例可以直接使用类.单例名来调用该类的函数,功能和静态脚本很相似。
关键词: 仓库界面
【Unity AR】
AR精灵制作学习记录 【天天快播报】项目开发软件架构选择指南
飞利浦推出两款44.5英寸带鱼屏:1500R大曲率、75Hz刷新率
行驶中小车后备箱塞人露双脚!司机称其喝醉了:遭网友谴责
世界播报:低头族注意了!9岁女孩长期低头颈椎老如50岁
领1030元大额券:骆驼户外徒步鞋179元起大促
世界观点:亚马逊要求员工一周到岗工作3天:公司一度像个鬼城
K8S-deployment.yaml文件详解
开心档-Git 基本操作
资讯推荐:制造业做信息化(IT管理、程序员)有前途吗?---浅谈信息系统规划
全球观点:记录--什么时候会阻塞HTML文档渲染过程?
【Oculus Interaction SDK】(十一)关于手势识别中手势设置的详细信息
天天热资讯!埃塞俄比亚政府和“提人阵”武装达成停火协议
天天新消息丨义乌自己的全球支付工具!义支付发布:打通16个币种
这待遇简直了!美团外卖进军香港 正疯狂招骑手:月入3万+
消息!手机套餐费太贵 韩国三大运营商被批:是否串通涨价
与剧版强烈反差!《三体》动画豆瓣跌至3.9分:官方已停播
特斯拉最入门车型国内现身?别被骗了:“披着狼皮的羊”而已
关注:Camunda(一)---安装和简单示例
女子在桃李面包中吃出带锈刀片 官方:不存在使用该刀片的生产环节
环球快看:手机寒气仍存 5G巨头中兴被曝优化人员 回应称正常调整
世界观焦点:蔚来ET9要来了?号称“蔚来版迈巴赫” 屏幕开始横过来
掌权八年后上汽大众换帅:贾健旭接棒
大嘴猴50S精梳棉男士四角裤狂促:券后每条不到10元!
WPF学习_day1
K8S-Service的yaml文件详解
全球时讯:【机器视觉】-什么是机器视觉?
当前报道:游戏王dm新马甲_[游戏王DM]羁绊
全球热点!张艺谋监制!北京冬奥拍成电影了:《北京2022》预告片发布
世界视讯!提前!韩国欲2028年推6G网 速度有多快?5G是高铁 6G就是飞机
全球首发5.5G 高通骁龙X75基带不再公布速率:峰值没啥意义
极氪汽车被指销售欺诈!车主集体投诉:退一赔一
世界新动态:ChatGPT火了 中国电信股价大涨50%:底层技术早有布局
世界观热点:璞华易构低代码平台,真正领跑未来的敏捷软件开发方案!
CXPACKET等待类型分析
华晨宝马第500万辆下线!东北老工业基地全力发展新能源车 沈阳比肩深圳
天天新消息丨印度3年内将成为主要电信出口国 5G堆栈“已准备就绪
光环退却 曝神车雷克萨斯ES 300H大优惠:广州35万可落地
世界新资讯:神似小红书 微信为什么要做“小绿书”?
李安:下部电影《李小龙》不会用120帧 挑战观众习惯
当前信息:【踩坑记录】SpringBoot跨域配置不生效
迈柯唯Maquet协议对接(呼吸机、麻醉机)FLOWi SERVOi
【当前独家】远程服务调用(RPC与Rest本质区别)
王予嘉图片_王予嘉
焦点快报!《消费者报告》:宝马制造的汽车最高 路虎果然名不虚传
讯息:全国110个城市建成千兆城市!运营商喊话继续加速千兆宽带:每月198 还能便宜
要闻速递:K8S-Pod的yaml文件详解
全球热推荐:ChatGPT:HuggingFace使用GPT-2模型并额外语料训练
每日观点:第一周学习--linux
环球速讯:消灭挖孔!努比亚首款屏下摄像头新机Z50 Ultra入网
每日资讯:针对富士X卡口设计:腾龙宣布开发新超广角变焦镜头
通讯!小米汽车全新谍照曝光:定位三厢、车头极长
Redis缓存穿透 && 缓存击穿 && 缓存雪崩
环球热头条丨机器学习-集成学习XGBoost
速读:科创信息:公司目前涉及少量军工业务
世界快报:二月二龙抬头来了!明晚10点龙形天象将出现在夜空 天文专家科普
1899元!小米发布智能门锁M20大屏猫眼版:续航1整年
天天热点!直到丈夫临死 我才发现30多年前闻到的“怪味”竟是大病预兆!
全球通讯!开车方便了!内地与中国澳门签署驾驶证互认换领协议:免试
世界动态:爱奇艺客服回应不再限制投屏:到期后再续订还是只有480P
微博会员可以看访客记录吗?唯美微博语录11句有哪些?
恐龙家族有哪些种类?恐龙家族有哪些成员?
网络用语龟龟是什么意思?龟龟是谁家的粉丝?
綦江怎么读?綦江旅游攻略必去景点有哪些?
诺基亚7210c上市价格是多少?诺基亚7210c手机参数
昂达平板电脑怎么样?昂达平板电脑忘记密码怎么办?
光波炉是干什么用的?光波炉的用法和功能有哪些?
猫特别爱叫是什么性格?猫特别爱叫怎么纠正?
环球热文:(数据库系统概论|王珊)第五章数据库完整性:习题
天天观焦点:数据结构刷题2023.02.20小记
Windows环境下FTP Server在局域网内的搭建
环球速讯:《微附件使用教程,公众号文章上传文件、附件超简单教程,图文教程详细易懂》
天梭机械表一天误差多少算正常?天梭机械表如何调整时间和日期?
我的世界怎么去蘑菇岛?我的世界怎么传送坐标?
微动态丨被7年老粉起诉后 爱奇艺宣布VIP会员调整:老会员高清投屏恢复
当前观察:苏州2022年新生儿爆款名字公布引热议 网友称男/女孩这些名字土爆了
天天百事通!所有新车小电瓶亏电 车主:埋在凯迪拉克锐歌这颗炸弹终于炸了
天天日报丨90后小伙以为健康 体检一身老年病 网友:一体检就害怕
全球观天下!Redmi K60被官方暴力肢解:做成了一个机甲兔女郎
【全球新视野】4次迭代,让我的 Client 优化 100倍!泄漏一个 人人可用的极品方案!
React Hooks模拟组件生命周期
python 二分查找算法
【K哥爬虫普法】百度、360八年恩怨情仇,robots 协议之战终落幕
当前简讯:对于多方安全计算,你是否也有这样的疑惑?
天天速看:央行2月20日开展2700亿元7天期逆回购操作
天天速看:比亚迪、特斯拉谁强让美国投资人吵翻:前者利润、技术、全球市场都差不少?
世界热资讯!暴雪《暗黑破坏神4》开篇动画公开:相比3代跨越式进步
13倍浓缩:日本隅田川胶囊咖啡1元/杯历史新低
世界快资讯:你被各式的电话推销骚扰过吗?说了不需要还能接到电话
当前速讯:百公里5个油 广汽本田ZR-V致在e:HEV今日上市:若卖19万买吗
手机哪些功能最费电?想不到耗电大户竟是相机 没电千万别用
每日观察!惨!特斯拉Model S疑似失速撞上消防车:司机死亡 车被压扁
每日报道:无语!女子应聘助理被要求和经理同吃住 月薪最高1.2万:本人回应目的不纯放弃
当前报道:邓超新电影跟漫威大片同天上映!观众赞《中国乒乓》有力量感 漫威剧情缺乏逻辑
红旗全新中大型SUV E202亮相:5分钟充电300公里
胎压过高对汽车有什么影响
世界关注:魅族20系列宣布搭载13根电竞级高能天线!TOP级信号质量
世界消息!男子200元买梭子蟹皮套占80元?商家回应:进货时就是这
世界焦点!读Java实战(第二版)笔记15_并行数据处理与性能
全球今亮点!“神话”破了!日本男子拉面店舔筷子后放回 还有人往旋转寿司上抹口水