Game Frameworks

维基百科:游戏引擎

  • unity 3d:最流行的游戏框架
  • unreal engine:虚幻引擎,真实感超强
  • cocos creator,cocos 2d:国产游戏框架
  • godot:类似unity,基于OpenGL实现的游戏引擎,支持gdscript+csharp脚本。开源免费,非常开放。是一个非常值得自学的游戏引擎。
  • scratch:https://scratch.mit.edu/ 小孩子的游戏工具
  • CryENGINE:The most powerful real-time development platform for achieving the highest quality experience.只支持windows开发。
  • Havok Physics
  • Game Bryo:Gamebryo技术已经被业界领先的开发者广泛用于开发一流的游戏产品。这款成熟的引擎提供了完整的工具,灵活的工作流程,快速的原型开发能力和高性能的实时运行表现,以此简化了游戏的开发。
  • Source Engine
  • painterengine: 国产,纯C实现。https://www.painterengine.com/example.html。 底层依赖OpenGL,GLUT(OpenGL的工具库)等库。 Github:https://github.com/matrixcascade/PainterEngine/tree/master/core
  • IRRLICHT:一个用C++编写的开源实时3D引擎。https://irrlicht.sourceforge.io/
  • ebitengine:使用go语言开发的2d游戏引擎:https://ebiten-zh.vercel.app/
  • stride:https://stride3d.net/ ,stride是一个开源C#游戏引擎。支持VR,有一个编辑器。也叫Xenko。Stride是一个用于真实感渲染和VR的开源C#游戏引擎。该引擎是高度模块化的,旨在为游戏开发者提供更大的开发灵活性。Stride带有一个编辑器,可以让你直观地创建和管理游戏或应用程序的内容。同时也支持Visual Studio编辑,支持Visual Studio本地运行。
  • Amazon Lumberyard
  • Haxe
  • HaxeFlixel
  • Lumberyard: 一个免费的游戏引擎,由亚马逊开发,支持3D游戏开发。
  • Armory3D: 一个免费、开源的游戏引擎,支持3D游戏开发,使用Blender进行编辑和构建。

低代码游戏引擎

  • RPG maker:不需要编程
  • Game Maker:不需要编程
  • stencyl:https://www.stencyl.com/ 无需编程

HTML5游戏引擎

HTML5引擎是最丰富的,各种框架层出不穷。其中PIXI.js、Three.js、babylon.js三者最强。
Mozila关于HTML5游戏介绍:https://developer.mozilla.org/zh-CN/docs/Games http://html5gameengine.com/

  • egret:也叫白鹭引擎,egret wing,从vscode改过来的编辑器。
  • play canvas:https://playcanvas.com/ 一个基于WebGL的游戏引擎,可以用于创建高性能的3D游戏和应用程序。
  • createjs:https://createjs.com/preloadjs
  • defold:https://defold.com/ ,The ultimate game engine for web and mobile
  • konva:html5的canvas库
  • Pixi.js:html5的canvas库,最好用的2d图像渲染。html5的2D游戏引擎可以分为两类:用PIXI的和不用PIXI的。
  • three.js:3d图像渲染
  • babylon.js:webxr游戏引擎,一个用于创建3D游戏和应用程序的开源框架,支持WebGL和WebVR。
  • GDevelop:一款无需编程就能开发游戏的2D游戏引擎。
  • MelonJS: 一个用于创建2D游戏的开源框架,支持HTML5 Canvas和WebGL
  • layabox
  • ImpactJS: 一个用于创建2D游戏的商业框架,支持HTML5 Canvas和WebGL。
  • eva.js:阿里出品
  • phaser:https://phaser.io/ phaser是一个功能完备的游戏引擎,底层使用PIXI来执行所有的呈现人物。
  • createjs:一个由Adobe赞助的开源JavaScript库,用于创建HTML5游戏和交互式应用程序。
  • sar:字节跳动出品的web游戏工具。

游戏引擎两巨头

Unity和Unreal是使用最为广泛的游戏引擎,代表了当今游戏产业的最先进生产力。web端因为各种受限,虽然小游戏多如繁星,但是很难像Unity和Unreal那样不断产生风靡全球、制作精良的游戏大作。

webxr游戏引擎两巨头

Threejs和babylonjs是webxr中最流行的两个游戏框架。

字节小游戏支持的引擎

  • cocos
  • 白鹭egret
  • laya

这三个是前端技术开发游戏的三个主要引擎。

游戏引擎的功能

  • 物理:刚体、碰撞、连接、关节
  • AI
  • 渲染
  • 网络
  • 声音
  • 动画

unity和unreal和cocos2d

游戏引擎的诞生并没有一个明确具体的时间点,往往要经历很长时间的摸索阶段才能正式与开发者见面。
unity 2006年,脚本支持csharp、js
unreal 二十世纪90年代,脚本仅支持cpp
cocos2d 2008年兴起于阿根廷。2011年,中国推出cocos creator。

三维建模

3ds Max、Maya、Blender

为什么不要使用Unity?

  1. Unity对开发者非常不友好,收费很高。
  2. Unity过于臃肿。

为什么要用Unity?

  1. Unity是目前最流行的游戏引擎。

unity和unreal的选择

  • C#比C++容易学习,开发效率高
  • unity对2d支持更好
  • unreal视觉效果虽然更好,但是只有3A游戏才对视觉效果有高追求

什么是游戏引擎?

游戏引擎就是开发脚手架集合,便于快速制作游戏。
游戏分为很多种类型,有一些特殊类型的游戏引擎,例如文字冒险类的游戏引擎,棋类游戏引擎等。
像cocos、unreal等很多著名游戏引擎的诞生都是源于某个具体的游戏,游戏制作者在制作过程中积累了一定的经验,把这些经验沉淀下来、做得通用一点就会变成游戏引擎。

为什么要了解游戏分类

  1. 游戏太多了,游戏就像书、电影一样,多得数不胜数,需要分类才能降低一些数量规模
  2. 分类一般都会按照某种feature进行分类,了解一下游戏分类能够了解游戏的feature
  3. 了解分类之后有利于设计游戏,像元素周期表一样组合一些feature就能够自然而然地确定游戏的主要需求。

以是否需要联网分类

按照游戏是否需要网络可以分为

  • 单机游戏
  • 联机游戏

以人数分类

按照游戏的人数分类,可以分为:

  • 单人游戏:推箱子、俄罗斯方块等
  • 双人游戏:象棋、五子棋等
  • 多人游戏:三国杀、英雄联盟、斗地主、麻将等

以内容分类

按照游戏内容进行分类

  • 消除类游戏(三消,三个相连便消除)
  • 塔防游戏
  • ACT (ACTion)(动作):玩家控制游戏人物用各种武器消灭敌人以过关的游戏,不追求故事情节,如熟悉的《超级玛丽》、可爱的《星之卡比》、华丽的《波斯王子》等等。电脑上的动作游戏大多脱胎于早期的街机游戏和动作游戏如《魂斗罗》、《三国志》等,设计主旨是面向普通玩家,以纯粹的娱乐休闲为目的,一般有少部分简单的解谜成份,操作简单,易于上手,紧张刺激,属于"大众化"游戏,也是较受欢迎的游戏种类。
  • ARPG (Action Role-Playing Game)(动作角色扮演)
  • AVG (AdVenture Game)(冒险游戏):由玩家控制游戏人物进行虚拟冒险的游戏。与RPG不同的是,AVG的特色是故事情节往往是以完成一个任务或解开某些迷题的形式出现的,而且在游戏过程中刻意强调谜题的重要性,使之成为一种专门考验玩家大脑的"活动"。AVG也可再细分为动作类和解迷类两种,动作类AVG可以包含一些格斗或射击成分如《生化危机》系列、《古墓丽影》系列、《恐龙危机》等;而解迷类AVG则纯粹依靠解谜拉动剧情的发展,难度系数较大,代表是超经典的《神秘岛》系列。
  • AAVG (Action AdVenture Game)(动作冒险)
  • FTG (FighTing Game)(格斗游戏)
  • RPG (Role-Playing Game)(角色扮演)
  • RTS (Real-Time Strategy)(即时战略)
  • TBS (Turn-Based Strategy)(回合制战略游戏)
  • MMO(Massively Multiplayer Online),多人在线游戏
  • MMORPG (Massively Multiplayer Online Role-Playing Game)(大型多人在线角色扮演游戏)
  • MUD (Multi-User Dungeon/Dimension/Domain)(多人即时虚拟世界)
  • SLG (SimuLation Game)(模拟游戏)
  • SRPG (Simulation Role-Playing Game)(模拟角色扮演)
  • STG (ShooTing Game)(射击)
  • FPS (First-Person Shooter)(第一人称射击游戏)
  • IF (Interactive fiction)(文字冒险)
  • TPS (Third-Person Shooter)(第三人称射击游戏)
  • SPG (SPorts Game)(运动)
  • TBG (Tabletop Board Game)(牌桌游戏)
  • PZG (PuZzle Game)(益智解谜)
  • RCG (RaCing Game)(赛车游戏)
  • VR (虚拟现实)
  • AR (增强现实)
  • MR (混合实境)
  • MOBA (Multiplayer Online Battle Arena)(多人在线战术擂台)
  • MUG (Music Game)(音乐游戏)
  • ETC(其他)
  • QTE(快速反应事件)
  • TCG(Trading card game)卡牌的诞生是在玩家之间交易产生的,但是在电脑游戏诞生后,这种方式就被改变了,从玩家之间的交易变成了玩家和游戏开发商之间的交易,也就慢慢的变成了CCG(Collectible card game)。

以制作成本分类

  • 独立游戏
    个人开发者实现的游戏作品,开发周期短,投入低
  • 3A游戏
    简单来说就是游戏的开发成本高,开发周期长,资源质量高的意思。这个说法只是在一般情况下的常用说法,关于“3A”的说法还有很多,但大概都绕不开这个意思。但是对于开发成本,耗时和质量并没有明确的标准,所以“3A”这个说法只停留在概念层次,并不像其他领域有着官方的评定标准。

以游戏视角分类

  • 2D游戏
  • 3D游戏

2d游戏按照游戏视角分类

  • Top-down,俯视一个平面世界,例如推箱子
  • Side-on:侧视图,游戏以卷轴的形式从左边往右边走,例如超级玛丽、flappyBird等
  • Isometric and 2.5d:看似是3d,实则是2d的游戏。例如翻砖块。

2d游戏按照艺术风格分类

  • Minimalist:极简抽象风格,平坦的色彩、简洁的线条和高可读性,例如移动益智游戏。
  • Pixel art:像素化图形
  • Illustrative:卡通、风格化或写实的艺术风格
  • Pre-rendered 3D:使用3D工具创作2d作品。

按照游戏模型分类

  • 高模:非常逼真的场景
  • 低模:卡通、简陋
  • 体素:Voxel,整个3D世界不是用三角面片绘制的,而是用一个一个的小立方体搭建而成。代表作《我的世界》,这其实就是3D中的像素艺术。

按照主动和被动

  • 主动型游戏,实时游戏,例如贪吃蛇、俄罗斯方块。游戏需要一个主循环,即便玩家没有动作,游戏状态也在不停地发生变化。
  • 被动型游戏,非实时游戏,例如棋类游戏,电脑的反应总是基于用户的输入。游戏不需要主循环,直接使用事件回调驱动即可。

按照关卡设置

  • 随机游戏,计算机生成游戏,例如扫雷、数独
  • 固定关卡游戏,例如推箱子

按照摄像机的位置

  • 第一人称
  • 第三人称

按题材(内容)进行分类

  • 二次元:日漫题材或受日系文化影响动漫IP改编的游戏
  • 泛二次元 :知名日本动漫人物(海贼、火影、龙珠、死神等)
  • 国漫 :中国漫画改编,或具有中国漫画特色
  • 女性向 :泛指以女性为受众的游戏
  • 官斗 :以官场为主题的游戏(女性向宫廷游戏不包含在内)
  • 武侠: 以古装武侠人物为主角,带有江湖、东方、古风等特征,以东方武侠江湖为主题的游戏
  • 玄幻 :带有修仙、玄幻、山海异兽等特征,以玄幻为主题,强调修仙,或以中国玄幻、修仙等小说改编的游戏
  • 科幻 :以科技风格为主题,包含宇宙、未来城市、未来科技等元素的游戏
  • 传奇 :盛大传奇游戏的衍生游戏,或以传奇为主题的游戏
  • 奇迹 :奇迹MU游戏的衍生游戏,或以奇迹为主题的游戏
  • 魔幻 :常见西方魔幻为主题的游戏
  • 三国 :以三国为主题/历史背景衍生的游戏
  • 西游 :以西游记为主题/历史背景衍生的游戏
  • 现代战争 :以现代化军事战争为主题的游戏
  • 都市 :以都市/城市为背景的游戏,包含美食类游戏
  • 丧尸 :出现丧尸、僵尸、生化等惊悚类为主要元素的游戏人物
  • 盗墓 :以盗墓为主题的游戏
  • 捕鱼 :以捕鱼、捞鱼、捕捉海洋生物的游戏
  • 校园 :场景发生在学校,包含校园生活、考试等元素的游戏
  • 农场 :出现农场的建筑,有田地,稻仓,畜牧等元素的游戏
  • 末世 :以毁灭性灾难后生存为主题的游戏
  • 棋牌: 出现了扑克牌等桌游元素,斗地主,麻将等字眼
  • 休闲类游戏 :没有明显题材,并且是休闲类游戏
  • 恐怖 :以营造恐怖氛围为题材的游戏(丧尸/盗墓/末世题材不包含在内)
  • IP授权改编 :由IP授权改编的游戏,注意:游戏版本提审前,需要在后台上传IP授权资质并过审,才能适用该标签
  • 其他题材 :难以划分到上述个主题中的游戏(包含侦探推理、西部等暂无分类的题材)

按游戏画风进行分类

  • 休闲类游戏(无画风)
    无主要人物场景,几乎没有美术风格,玩法非常简单的游戏
  • 可爱卡通
    造型符合可爱、Q版、萌系等元素,采用的颜色鲜艳,形状一般比较简单,与现实环境存在差距
  • 写实卡通
    整体属于卡通风格,但游戏人物场景偏写实,与现实环境相近
  • 风格化卡通
    整体属于卡通风格,与现实环境差距较大。非可爱、非写实的大多可归为风格化
  • 3渲2手绘写实
    实际是二维画面,通过技术渲染,类似于三D场景
  • 日韩写实
    游戏人物偏日韩(亚洲人),但注重细节刻画
  • 欧美写实
    游戏人物偏欧美(欧洲人),但注重细节刻画
  • 像素风
    游戏场景人物通过像素方格进行堆砌,有颗粒感
  • 中国风
    具有中国传统元素造型、水墨画等风格
  • 日韩卡通
    游戏人物偏日韩,画风细腻、脱离现实,唯美,时尚,装饰感,干净整洁,不注意细节刻画
  • 美漫
    游戏人物画风硬朗豪放、粗线条,艺术夸张,人物偏写实,体积感厚重感,不注意细节刻画
  • 其他
    以上游戏风格均不包含,且不属于无画风游戏

游戏种类

电子游戏分类

原文链接

类别分类指南
更新时间:2022-08-31 21:08:57

休闲

一级标签

二级标签

三级标签

标签释义参考

休闲

模拟

模拟经营

玩家以有限资源建造、扩张或管理虚拟社区或项目的模拟游戏

生活模拟

生活模拟指对生活及各种生命体的模拟游戏,该类型游戏可能通过围绕个人及其关系或围绕一种生态系统展开

体育模拟

模拟经营对象为各种体育运动队的模拟游戏

模拟养成

以养成为游戏核心体验,以培育者的角度来进行某种生活的模拟游戏

真实模拟

还原真实世界各种行为的模拟游戏

换装

模拟人物换装/化妆的一类游戏

休闲放置

放置养成。玩家通过挂机获得资源完成各种模拟任务的游戏类型

冒险

文字冒险

以文字输入输出为主的一种冒险游戏,此类型游戏以软件模拟情境,令玩家使用文字指令控制角色,以影响周边的情境

图形冒险

使用图形,将环境传达给玩家的冒险游戏

视觉小说

基于文字故事,以静态图片辅助叙事的

互动小说

游戏

互动电影

通过使用动画或真人镜头的全动态视频进行叙事冒险的游戏类型

益智

三消

玩家通过交换、点击、连线等形式操纵方块使其消失的游戏类型

三消+

以三消玩法为主线并叠加其他类型玩法的一系列游戏

泡泡射手

玩家发射不同颜色泡泡以使其消除的游戏类型

文字

猜字、猜成语等游戏

问答测验

向玩家展示各种类型问题并由玩家回答完成测验的一系列游戏

物理过关

玩家利用物理规律解决难题过关的一系列游戏

合成

玩家合并相同类型目标,得到更高级产物以满足不同游戏目标的游戏类型

桌游

移植线下桌面游戏玩法的一系列游戏

其他益智

其他类型益智游戏

休闲竞技

IO

以操作规则简单、多人在线对抗、死后即刻复活等为特点,由于在命名时多以“.io”结尾,久而久之也被人称为io类游戏

体育对抗

具有体育元素的竞技游戏

其他休闲竞技

其他类型操作较轻,对局时间较短的对抗竞技游戏

儿童

涂色绘画

涂色绘画为核心玩法的一系列游戏

教育

以教育为主要目的的一系列游戏,如教授识字、编程等知识

LBS

+

LBS

+

利用定位技术并将用户位置应用到游戏规则中的一类游戏

超休闲

超休闲

泛指上手门槛极低的一类游戏,以上类别不覆盖,均选超休闲

特殊功能

脸控

通过智能手机的前置摄像头进行面部动作捕捉,靠面部表情或动作达成游戏目标

AR

利用增强现实技术,实施捕捉屏幕中的场景/平台的位置,并做出即时反应的游戏

声控

通过识别玩家声音达成游戏目标

体感

通过识别用户肢体动作变化达成游戏目标

功能

内部功能性产品专用标签

中重度

一级标签

二级标签

三级标签

标签定义

中重度

RPG

即时制

MMO

用户操控游戏角色进行即时对战来推动剧情或升级,可与其他众多玩家在统一世界中互动的一系列游戏

回合制

MMO

用户操控游戏角色进行回合制对战来推动剧情或升级,可与其他众多玩家在统一世界中互动的一系列游戏

MMO

+

基于MMO玩法的变体形式

ARPG(非卡牌)

用户操纵游戏角色通过动作性战斗,推动角色升级、冒险闯关的一系列游戏。战斗场景的动作性处理及角色成长或关卡探索的乐趣构成了游戏核心体验

卡牌

RPG

经典卡牌

RPG

核心体验以卡牌养成+角色扮演体验为主的一系列游戏

卡牌

RPG

+

以卡牌养成+角色扮演体验为主并叠加多种游戏玩法的一系列游戏

放置

RPG

以角色扮演体验为主,并通过挂机实现资源获取、角色成长的一系列游戏

SRPG(非卡牌)

指带有战术性(通常为战棋类)的角色扮演游戏

Roguelike

RPG

以随机生成关卡的地牢、回合制战斗、基于磁贴的图像和角色永久死亡(即一次游戏内无法无限制复活)为特点的一些列角色扮演游戏

其他

RPG

以角色扮演为核心用户体验,不包含卡牌及玩家互动社交元素的一系列游戏

动作

3D ACT

强调玩家的反应能力和手眼的配合,对玩家操作实时反馈,剧情简单,不以角色养成或游戏剧情为核心乐趣的一类游戏

格斗

玩家分为多个两个或多个阵营相互作战,在有限区域内,使用格斗技巧击败对手来获取胜利的一类游戏

动作冒险

融合动作游戏及冒险游戏特点,玩家通过与其他角色战斗,解谜、闯关,完成特定目标的一系列游戏

平台动作

玩家使用各种方式在悬浮平台上进行移动和穿过各种障碍以达到目标的一系列游戏

双摇杆射击

需使用手指操纵两种控制方式的游戏类型,其中一个摇杆负责控制游戏角色的移动,另一个摇杆的作用则是执行射击任务

横版射击

人物徒步战斗(如具有跳跃能力)的射击游戏,可使用侧向滚动,垂直滚动或等距视点,并且可能具有多向移动功能

弹幕射击

玩家操控一位角色对来袭的大量敌人进行攻击,并在过程中升级、强化自我的能力,以最终打败敌人为目标的的一系列游戏

SPG

玩家通过控制游戏角色,完成各类体育运动动作的游戏

跑酷

玩家控制游戏角色,穿越各类障碍,无明显目的的向前无限移动的一系列游戏

音舞

玩家伴随音乐根据指令控制游戏角色完成各种舞蹈动作的一系列游戏

策略

SLG

玩家控制国家资源,军队行进等要素,进行地图上的战略或战术设计以达成某种目标的游戏

4X

包含探索、扩张、征服、剿灭敌人元素的游戏

塔防

通过建造塔楼等障碍物,阻挡敌人入侵的一系列游戏

自走棋

玩家在不同种类的棋子之间挑选组合,并布置在棋盘上,与对手进行无需后续操作输入的自动战斗,最后尚有棋子存活的一方取得战斗胜利的一类游戏

对战卡牌

玩家使用卡牌及其技能与其他玩家合作或对抗以达成特定目的的游戏类型

竞技

FPS

以玩家的第一人称视角为主视角进行的射击类游戏

TPS

包含玩家整个身姿的第三视角进行的射击游戏

MOBA

玩家控制团队中一角色,与对立团队玩家竞技的一类游戏。玩家被分为两队,通常每个玩家只能控制其中一队中的一名角色,以打垮对方队伍的阵地建筑为胜利条件

RTS

谋定过程即时,包含资源采集、基地建造、技术发展等元素,玩家可以对各单位进行独立控制的战略游戏

大逃杀

玩家参与多人混战,并通过探索、收集得到成长,最终淘汰至最后一人的一系列游戏

非对称竞技

敌我双方拥有资源从功能上错位,交互方式、受制规则、最终目标也完全不同的竞技游戏

竞速

以第一人物或者第三人物参与速度的竞争的游戏类型

其他竞技

其他类型的竞技游戏

沙盒

狭义沙盒

给予玩家充分自由进行各种创造及任务完成的一系列游戏

生存建造

玩家通过高自由度的建造及各种任务完成达到生存目的的一系列游戏

棋牌

*注意,中式棋牌中本平台仅开放接入棋类。

一级标签

二级标签

三级标签

标签定义

棋牌

中式棋牌*

麻将

移植各类传统麻将玩法的游戏

牌类

移植各类传统中国牌类玩法的游戏

棋类

移植各类传统中国棋类玩法的游戏

多合一棋牌

融合多种类型中式棋牌的一类游戏

其他中式棋牌

其他类型中式棋牌游戏

美式棋牌

扑克

移植各类线下扑克玩法的游戏

游戏引擎用于制作游戏,游戏如果需要联网,那么游戏就需要一些服务,这些服务包括房间、匹配、排行榜、语音、账号等。

微软Playfab

也叫Azure Playfab,Azure是微软的云服务。Playfab是微软提供的游戏后端服务,包括玩家管理、虚拟货币、统计分析等功能。

房间、匹配、网络、排行榜,这都是游戏功能的标配。

Playfab文档

PlayFab 数据是一组用于数据分析、存储、处理和导出的工具。 这些功能包括游戏数据、玩家数据、角色数据、组数据,以及数据管理和预配功能,如实体、内容分发网络和 Webhooks。

微软GDK

Game Develop Kit。
GDK:微软的游戏开发工具包,其实playfab sdk是GDK的一部分

Unity Services

特点:Unity官方支持,功能丰富。

Photon Engine:光子引擎

由 Exit Games 提供的实时多人游戏引擎和后端服务,支持多种平台和语言。 把多人游戏变得简单。

  • Photon Cloud
  • Photon Fusion
  • Photon Voice

https://www.photonengine.com/

GameEye

Gameye B.V. 提供的游戏服务器托管和部署服务,支持多种游戏引擎和平台。

Firebase

非常流行的游戏存储服务。 由 Google 提供的移动应用开发平台,包括实时数据库、云存储、认证、推送通知等功能,也可以用于游戏开发。

  • Cloud Firestore:Cloud Firestore 是一款 NoSQL 文档数据库,它使您可以在全球范围内轻松存储、同步和查询您的移动应用及 Web 应用的数据。类似MongoDB
  • Cloud Storage:存储图片和视频,类似大文件存储,s3等。
  • Firebase Realtime Database:是一种托管在云端的NoSQL数据库,可以实时存储并同步用户数据。

Oculus SDK

PICO SDK

GameLift

  1. 由亚马逊 Web 服务(AWS)提供的游戏服务器托管和部署服务,支持多种游戏引擎和平台。 https://aws.amazon.com/cn/gamelift/ https://www.amazonaws.cn/gamelift/?nc1=h_ls

BigWorld

BigWorld是一款商业化游戏引擎,是一个大型多人在线游戏 (MMOG) 开发商提供成熟的中间件平台,这一中间件平台正迅速成为行业标准。

Nakama

由 Heroic Labs 提供的开源游戏服务器,支持多人游戏、实时通信、社交功能等。

Normcore

多人游戏,只针对Unity。
与Photon是竞品。

Epic Online Services

Unreal的多人游戏服务,支持语音聊天、匹配、端到端连接等服务。

Amazon GameSparks

由 Amazon 提供的游戏后端服务,包括玩家管理、虚拟货币、统计分析等功能。

开源多人游戏库

除了多人游戏服务,也有一些开源多人游戏库。

KBengine

https://github.com/kbengine 一款开源的 MMOG 游戏服务端引擎, 仅 Python 脚本即可简单高效的完成任何游戏逻辑 (支持热更新), 使用配套客户端插件能够快速与 (Unity3D、UE4、OGRE、HTML5、等等) 结合形成一个完整的客户端。

引擎使用 C++ 编写,开发者无需重复的实现游戏服务端通用的底层技术, 将精力真正集中到游戏开发层面上来,稳定可靠并且快速的打造各种网络游戏

libgdx

Libgdx是一个用Java语言开发的跨平台游戏框架。目前Libgx支持的平台包括Windows, Linux, Mac OS X, Android, iOS 和 HTML5。

github:https://github.com/libgdx/libgdx

pomelo

pomelo网易开源的一个nodejs轻量级游戏服务器框架,与以往单进程的游戏框架不同, 它是高性能、高可伸缩、分布式多进程的游戏服务器框架,并且使用很简单。
github:https://github.com/NetEase/pomelo

Origin

origin 是一个由 Go 语言(golang)编写的分布式开源游戏服务器引擎。origin适用于各类游戏服务器的开发,包括 H5(HTML5)游戏服务器。
https://github.com/duanhf2012/origin

游戏行业

游戏版号的作用

游戏版号是相关监管部门同意游戏出版运营的批准文件,简单来说,没有游戏版号的游戏便无法上线运行,也无法开通买断制或内购制等变现渠道。
游戏有版号,网站有网站号,开店有营业执照。任何公开的资源都有监管部门发放的ID。

  • 只有中国才有游戏版号。
  • 开始时间2016年5月。
  • 由广电总局审核和发布游戏版号。

游戏版号的用途: 1、保护原创作者的合法权益。 2、行业监管,对游戏的内容与质量进行审查 3、宏观调控市场的游戏发行总量

根据相应法律法规,新闻出版行政部门没收违法发行的出版物和违法所得,并处违法所得2倍以上10倍以下的罚款;没有违法所得的,处3000元以上10000元以下的罚款;情节严重的,由原发证部门责令停业整顿或者吊销许可证。

健康游戏忠告

自2003年9月1日起,所有电子出版物出版单位在新出版的电子游戏出版物中、互联网游戏出版机构在出版的互联网游戏出版物中,应设置必要的程序,在游戏开始前,必须在画面的显著位置全文登载《健康游戏忠告》。2003年9月1日前已出版发行的电子游戏出版物,在重版时必须登载《健康游戏忠告》。各地新闻出版局应对此项工作认真监管,凡未按通知要求登载《健康游戏忠告》的游戏出版物,一律停止出版、运营和销售。

抵制不良游戏,拒绝盗版游戏。
注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。
合理安排时间,享受健康生活。

版号注意事项

一、国产网络游戏内容发生实质性变动的,网络游戏运营企业应当自变更之日起30日内向国务院文化行政部门进行备案。网络游戏内容的实质性变动是指在网络游戏故事背景、情节语言、地名设置、任务设计、经济系统、交易系统、生产建设系统、社交系统、对抗功能、角色形象、声音效果、地图道具、动作呈现、团队系统等方面发生显著变化。

二、棋牌游戏报备时,除需提供上述材料之外,还需提交所有运营企业填写的《棋牌游戏产品情况登记表》以及4份可以遍历游戏全部场景和功能的账号和密码。

没有游戏版号会怎样?

  • 只能公测、内测
  • 不许提供充值端口,禁止商业变现

参考资料

游戏版号制度真的适合中国吗

android常用应用商店

  1. GooglePlay
  2. ApkPure
  3. 酷安
  4. 豌豆荚
  5. ApkMirror
  6. Aptoide
  7. 兔兔助手
  8. 手机乐园
  9. Taptap
  10. 小米应用商店
  11. 腾讯应用宝
  12. 华为应用商店
  13. F-Droid:程序员的应用商店
  14. 百度应用助手

游戏应用商店

三大主流游戏商店:Steam、Uplay、Epic

  1. Steam:为国内外知名的大型游戏平台,平台发行超过50000款游戏,在世界范围内是首屈一指的。在Steam上不仅可以购买、下载、上传游戏,还能看到玩家对一款游戏最真实的评价。旗下大作有:《皇牌空战7》、《看门狗2》、《Dota》、《GTA5》等。
  2. Uplay:Uplay游戏平台是一款一站式游戏服务平台,支持一键下载、安装、更新,游戏资源极其丰富。Uplay游戏平台可以购买下载各种单机游戏、网络游戏,还具有一定的社区功能。旗下大作有《雷曼》、《刺客信条》、《全境封锁》、《幽灵行动》等。
  3. Epic:虚幻引擎,https://store.epicgames.com/zh-CN/ 作为Steam的直接对标竞品,Epic Games游戏平台专注于与来自全球的游戏开发商和发行商合作。在该平台每两周可以获取一款免费游戏游玩,各种精品游戏大作不停地送。旗下大作有《战争机器》、《CS:GO》、《堡垒之夜》、《子弹风暴》等。
  4. weGame:https://www.wegame.com.cn/store

全球十大游戏公司2021年总收入为1260亿美元。

概念

  • 独占游戏:只在一个平台上发行的游戏。
  • 国行和非国行:国内发行和非国内发行,二者硬件配置完全一致,但是游戏服务不一样。国行可以享受官方维修服务;(理论上)只能登陆国内服务器,由于国内游戏审核较为严格,可以直接购买的游戏较少,很多游戏需要购买非国行的实体游戏(光盘或卡带);非国行只有店保(商家将设备寄送到对应的地区接受官方保修服务,如日版寄回日本,买家承担来回邮费);可以在国服外跨区登录,所有在平台上线的游戏都可以购买、玩耍。

游戏设备

有一些游戏需要单独的设备,而另一些游戏则基于PC、手机等设备。

  • 主机:游戏机,三大游戏主机厂商是微软xbox、索尼、任天堂。这三家是游戏产业翘楚,能产出能投资能收购,有自己的平台,这就意味着高度的独立性与主导性。VR游戏大厂:Oculus、PICO。
  • PC:个人电脑,可执行程序
  • 页游:个人电脑网页游戏
  • 手游:手机游戏

三大游戏主机的特点:

  • 微软xbox:Xbox Series系列最独特的能力在于有钱。在雄厚的资金支持下,微软本世代主力机型的性能比索尼更强,独占游戏阵容开始呈现反败为胜的迹象(尤其在收购B社之后),XGPU游戏库大肆收买人心,Xbox品牌通过主机、PC、云游戏四处出击。
  • 索尼ps(playStattion):PS5系列最大的优势在于老玩家群体非常庞大。得益于PS4时代强大的独占游戏阵容,以及过个时代以来相对“亲民”的政策,索尼积累了庞大的忠实用户。这也导致PS5发售之初的关注度、销量遥遥领先于XSX。
  • 任天堂switch:本世代任天堂成功将主机与掌机融为一体,不仅在家可以玩,也可以外出跟任何玩家想玩的人一起玩。在碎片化娱乐越来越严重的当下,Switch比其他主机更能充分利用每一份空闲时间。Switch目前有三款:Switch Lite、Switch长续航版、Switch OLED(10月上市)

switch的各个版本:

  • Switch Lite:俗称掌机版或者迷你版,最大的特点是尺寸偏小,主要作为掌上游戏机使用。两侧手柄不可拆卸(Joy-con手柄附带的红外相机、HD震动等功能也一并阉割掉),不支持桌面模式和主机模式,续航时间3-7小时。
  • Switch长续航版:目前的主力机型,在初版的基础上,通过更换芯片和闪存实现续航提升,理论上可以达到4.5-9小时。最大的特点是支持掌机模式、桌面模式、电视(主机)模式(1080P信号输出),不仅可以在家里玩,也可以外出跟任何你想玩的人一起玩。
  • Switch OLED:屏幕升级为7英寸OLED屏幕,边框变窄,画面显示更加明亮、清晰,颜色也更加丰富。支架更大、更宽,可以实现不同的支撑角度。升级了扬声器,声音效果更加立体。底座增加网线接口,边缘做了磨圆处理,相对不容易刮花屏幕。机身存储升级为64G。

PS5的各个版本:

PS5分为两个版本:光驱版(标准版)和数字版,主要的区别在于有没有光驱。两款产品的核心性能、配置完全一样。
标准版由于搭载光驱,整体尺寸更大一点,电源功率更高一点(350W,数字版340W),重量更大一点(4.5公斤,数字版3.9公斤)

光驱版能够兼容不同地区的游戏光盘。即使不做备份,也可以玩到非国行游戏。用光驱版玩实体游戏,通关后可以卖二手回血,也可以直接租游戏光盘来玩,比购买数字版更省钱。

实体游戏光盘附带的收藏、纪念、交易价值,无法被数字版游戏替代。喜欢收藏、购买游戏的玩家,实体游戏光盘+光驱版游戏机是最佳选择。光驱版支持播放4K Ultra HD Blu-ray蓝光影碟,在游戏机吃灰的情况下,可以发挥一些光碟播放器的作用。

数字版的优势在于机身体积更小,更加轻巧,美观,占用的空间也相对较小。由于只能购买数字版游戏,不需要考虑光盘的存放问题。缺点在于少数极端情况,比如被ban账号、ban机或者遭遇关店,会导致无法购买新游戏和DLC的问题。

XBox

Xbox分为高性能版的Xbox Series X(XSX),以及主打轻巧便携的Xbox Series S(XSS)。

XSX与XSS的CPU处理能力、游戏加载速度方面基本一致。GPU方面,XSS缩水严重,52CU砍到20CU,频率也被压低。由于散热压力减小,XSS采用了传统的水平风道热管散热器,外观更加扁平、轻巧(XSX采用的是均热板垂直风道,并配有光驱,尺寸更大)

XSS由于主攻1440p@60FPS,内存由16GB减至10GB,吞吐率也阉割了一倍多。512G的机身内部存储,安装系统后实际可用空间在364GB左右(XSX可用802GB),如果不做扩容或者外置存储,相对来说空间会显得比较紧张。

市值和营收的关系

市值和营收是两个不同概念。
市值,是指一家上市公司的发行股份按市场价格计算出来的股票总价值,其计算方法为每股股票的市场价格乘以发行总股数。整个股市上所有上市公司的市值总和,即为股票总市值。
营收即营业收入,指公司在销售商品和提供劳务及让渡资产使用权等日常活动中所形成的经济利益的总流入,包括主营业务收入和其他业务收入。
净利润,净利润等于营收减去成本。有些行业成本高,营收虽然多,但是净利润不高。有些行业成本很低,但是利润率高,例如烟草行业。

市值大约是营收的1.5倍,算是合理区间。如果想收购一家公司,那么需要支付这家公司未来很长一段时间的营收,所以市值大于营收。

游戏厂商排名

  1. 微软,有Xbox和贝斯赛达(Bethesda),核心IP主要有光环(343)、极限竞速/地平线(Turn 10/Playground)、我的世界(Mojang)、上古卷轴(Bethesda)、辐射(Bethesda/Obsidian)等。当然,微软的主营业务是Office和云服务,搞Xbox也是为了拓展云服务的场景。
  2. 索尼,有PlayStation(简称PS),第一方核心IP主要有战神(圣莫妮卡)、最后生还者(顽皮狗)、神秘海域(顽皮狗)、血源诅咒(FS/蓝点)等,近年来索尼的开放世界作品也越来越有育碧罐头味儿了。索尼第一大业务虽然是游戏,但比重并不很高。除了卖游戏机外,索尼还卖保险、卖手机、卖相机、卖音响、卖电视、卖金砖、卖耳机、卖唱片、卖传感器、拍电影......
  3. 任天堂(Switch),旗下拥有主机NS,独占作品比较多,偏卡通风,代表作《超级马里奥》、《口袋妖怪》、《星之卡比》等。第一方马里奥、宝可梦、塞尔达传说三大IP无人能敌,本世代主机销量第一,游戏公司市值第一。
  4. 动视暴雪(Activision Blizzard,$500亿):美国大厂,核心业务是美国3亿鼠标的枪战梦想(使命召唤)、全球玩家都巴不得倒闭的暴雪(魔兽世界、炉石传说、守望先锋)和糖果消消乐,但人家就是市值最高的第三方。
  5. EA($372亿)美国艺电:美国大厂,一个毁掉无数工作室的资本集团,核心IP是FIFA/NFL(球)、战地/Apex(枪)、极品飞车(车),以及模拟人生(Maxis)、质量效应(Bioware)、植物大战僵尸(PopCap)等。EA这几年连续吃瘪,玩家拍手称快。世界游戏厂商巨头之一,旗下拥有诸多在世界都有广泛知名度的产品,代表作有《战地》、《极品飞车》、《模拟人生》、《FIFA》、《APEX》等。EA的旗下产品几乎覆盖了所有的游戏类型,且通过收购等方式将诸多经典游戏划归旗下,如《质量效应》、《命令与征服》、《镜之边缘》等。
  6. Take-Two($190亿):游戏作品多为策略和回合制游戏,包括《无主之地》、《文明》、《NBA 2K》等。美国大厂,分为2K(NBA 2K、文明、生化奇兵、无主之地等)和R星(GTA、荒野大镖客等)两部分。TakeTwo也不是什么善茬,毁了Irrational(核心人物肯列文)、弄残了Firaxis(核心人物席德梅尔),R星(核心人物豪瑟兄弟)也前途未卜。
  7. 万代南梦宫(BNEI,$164亿):其实这家赚钱更多的是靠卖玩具(比如高达)和完整的漫改游戏产业链(比如龙珠、高达、火影、少战、海贼王、刀剑神域、数码宝贝、假面骑士等等等等),自有的游戏IP有传说系列、皇牌空战、铁拳等,也是黑魂系列、小小梦魇 等的发行商。
  8. 科乐美(Konami,$64亿):一个任何玩家听到都会竖中指的公司,旗下有魂斗罗、恶魔城、寂静岭、合金装备、实况足球、心跳回忆等无数金闪闪的大IP,折腾到现在毁的毁、烂的烂、糊弄的糊弄、不放过任何一次恶心玩家的机会,烂钱恰的飞起。但是,这货就是靠着健身房、赌博机和手游活得潇洒滋润。据传目前科乐美的IP将移交索尼开发。
  9. 光荣特库摩(Koei Tecmo,$62亿):代表作品仁王、三国志、无双系列、炼金工房 等,除了仁王以外要么粉丝向、要么不正经(/滑稽)、要么直接无了(信长、忍龙、太阁)。但是人家愣是靠着授权手游(没错,就是你在知乎广告上天天看到的矮大紧带盐的三国志战略版)和代工游戏(塞尔达无双 、P5s)把利润和股价给支棱起来了。
  10. SQUARE ENIX($61亿)(史克威尔艾尼克斯):日本游戏厂商,作品包括《最终幻想》、《奇异人生》等。SE上个世纪踏平欧美、如日中天,一度是日式RPG的代名词,除了最终幻想、勇者斗恶龙、王国之心、星之海洋等大IP之外,还有超时空之轮 这样的经典神作。然而,二十年之后,堂堂SE靠着冷饭、移植、手游、IP授权恰饭,真有你的SE。2009年SE收购Edios,古墓丽影、杀出重围、正当防卫等IP尽入囊中,也改编了一些漫威游戏。同时SE也是尼尔系列、八方旅人、奇异人生、热血无赖等作品的发行商。
  11. Embracer Gurop($110亿):欧洲大厂,老THQ的秽土转生,旗下有九个运营集团,单机游戏IP主要有THQNG的暗黑血统、赏金奇兵、海绵宝宝,深银的地铁系列、黑道圣徒,咖啡渣的Runner、模拟山羊,Saber的僵尸世界大战 以及无主之地系列的开发商Gearbox等。
  12. 育碧(Ubisoft,$57亿):欧洲大厂,欧洲最大游戏厂商,在加拿大、法国、中国等地均设立了公司,育碧旗下也拥有较多影响力强的产品,包括《孤岛惊魂》、《刺客信条》、《彩虹六号》、《幽灵行动》、《看门狗》等。育碧罐头大厂,主要流水线有刺客信条系列、孤岛惊魂系列、看门狗系列、汤姆克兰西系列等。
  13. CDPR($50亿):欧洲大厂,代表作巫师系列。以前是波兰蠢驴,现在是纯纯的波兰蠢驴。
  14. 卡普空(Capcom,$47亿):日本,代表作生化危机系列、怪物猎人系列、鬼泣系列、逆转裁判系列
  15. 世嘉飒美(Sega Sammy,$36.7亿):日本,代表作如龙系列 、全面战争系列、真女神转生/女神异闻录系列,旗下很多作品甚为经典,包括《死亡之屋》、《全面战争》、《足球经理》等。
  16. 华纳:主要是做电影的,游戏作品不多,但是母公司有钱,所以也出过《蝙蝠侠》、《中土世界》这样围绕电影的游戏作品。
  17. SNK:旗下拥有《合金弹头》、《拳皇》等十分经典的游戏。
  18. NEXON:韩国游戏公司,擅长网游,包括《地下城与勇士》、《跑跑卡丁车》、《冒险岛》、《反恐精英online》等。
  19. 腾讯游戏:目前中国规模最大的游戏公司,在国内拥有极高的用户基础,产品包括《穿越火线》、《和平精英》、《王者荣耀》、《英雄联盟》等。
  20. 网易游戏:在国内拥有很高知名度与影响力的游戏厂商,产品包括《逆水寒》、《永劫无间》、《梦幻西游》等,网易也是动视暴雪在中国的运营商。
  21. 米哈游:《原神》
  22. FunPlus
  23. 三七互娱
  24. 莉莉丝:《剑与远征》
  25. 龙创悦动
  26. 友塔游戏
  27. Topwar Studio
  28. 沐瞳科技
  29. 乐元素
  30. 有爱互娱
  31. 悠星网络
  32. 4399
  33. 爱奇艺
  34. 字节跳动:朝夕光年《花亦山心之月》
  35. 杭州游卡公司(YokaGames):《三国杀》
  36. 灵犀互娱:《三国志·战略版》

Steam必玩十大游戏

10、格斗挑战( Brawlhalla ) )。

简单来说,《格斗挑战》是小巧版《任天堂大乱斗》,玩家可以控制角色在2D平台上对战,通过使用掉落的武器道具、个人技能等夺取胜利。 游戏支持排球比赛、休闲多人格斗等多种模式,共有46名可操控角色。

游戏中具有可爱的卡通画风、多样的攻击道具、不同角色的不同属性等特点,目前Steam的好评率高达92%,相当值得一玩,但遗憾的是不支持中文。

9、军团要塞2 (队堡2 ) ) )。

这是v公司有名的多人FPS,拥有很多游戏模式、地图、装备以及风格各异的英雄角色。 旗帜、控制点、购物车、体育场、小山之王等机型一应俱全,玩家社区自己制作了很多有趣的其他机型。

这个游戏在Steam上的评价率高达94%,至今每天有5万人左右同时玩这个游戏,还支持简体中文。 美丽是2007年发布的旧游戏,在某些方面可能有点过时。

8、战争雷霆( War Thunder ) )。

这是一场超大规模的军事运营商战争MMO,拥有来自许多不同时代的百中战争机器和近10个国家。 玩家可以通过升级不断获得新的飞机、坦克和战舰,最多可以和32名网友一起进行真实的战争。

这个游戏Steam的好评率高达89%,支持中文。

7、枪火游侠( Paladins )。

《枪火游侠》与《守望先锋》非常相似,但是是免费的,也有自己的。 可以独自加入卡组系统,强化属性,修正技能,使英雄接近玩家的风格。 游戏本身有40多个英雄角色,但通过定制可以玩更多。

《枪火游侠》在Steam上的评价率达到了91%,但Steam版没有对国内开放,国内玩家可以在Wegame平台上玩《枪火游侠》。

6、刀塔霸业( Dota Underlords ) ) )。

这是今年新发布的V公司《Dota 2》自行围棋游戏。 上线后火爆无比。 虽然现在也提前体验过,但是已经吸引了很多玩家。 Steam的评价率也达到了85%。 自行车手也是现在最流行的模式之一,你知道没有必要再介绍了吧。

当然除了《刀塔霸业》、《英雄联盟》等游戏,还有自己的自行棋竞品。 但是,在Steam上快乐的免费游戏中,《刀塔霸业》当然不能错过。

5、上帝的大灾难( SMITE ) )。

这是一款大型的MOBA游戏,而且可以很容易地向没怎么接触过MOBA的玩家下手。 另外,这不是俯视视点的点击游戏,而是第三人称视点。 玩家可以扮演美杜莎、雷神托尔、丘比特等超过90人的神话中的存在,进行各种模式的战斗。 同时游戏还有每两周免费更新一次。

《神之浩劫》 Steam好评率现在达到了89%,支持中文。

4、反恐精英:全球攻势( CS: GO ) ) ) )。

v公司的明星竞技性FPS是1999年原创CS的经典续作,包括全新的地图、人物、武器、全新的游戏模式,至今仍是Steam上最受欢迎的游戏之一。 除了家喻户晓的拆弹和救人游戏外,“吃鸡”游戏等新模式也相继推出; 另外,玩家自己还可以制作魔改版私服,体验各种各样的内容。

《CSGO》在Steam上的评价率为89%,支持简体中文。 但我想很多感兴趣的玩家早就玩这个游戏了。

3、星球大战( Warframe ) )。

著名的第三人称科幻合作射击游戏虽然有非常复杂的系统机制和故事情节,但其有趣的玩法也足以吸引玩家。 一旦沉迷其中,就能体验到无比丰富的游戏内容。 做了几百个小时也摸不着头脑。 而且,所有内容都可以完全免费玩。 操纵机甲打天下不太爽。

《星际战甲》在Steam上的好评率高达96%,支持中文,目前“好评”。

2、Dota 2

《Dota 2》是Steam的顶级至尊,虽然MOBA游戏没有当年那么火爆,但“瘦死骆驼比马大”,至今仍几乎在每日在线玩家数量的高峰中排名第一。 所有的更新和英雄也是免费的。

《Dota 2》 Steam好评率为85%,支持中文字幕和配音。

1、驱逐之路/流亡暗道( Path of Exile ) ) )。

这是一款超强的ARPG网络游戏,独特的内容重玩价值也很高,角色升级和战利品系统也很好。 制造商在那之后的更新非常充实。 拥有浓厚的《暗黑破坏神》风格,购买系统也很良心。

《流放之路》在Steam上的评价率达到了96%,但不对国家区开放,国内玩家可以在Wegame平台上玩这部作品。

IGN评选的史上最伟大的100个游戏

100 无主之地2

99 神界:原罪2

98 最终幻想7

97 刺客信条:黑旗

96 猴岛小英雄2:勒卡克的复仇

95 火爆狂飙3

94 辐射2

93 英雄联盟

92 吃豆人3

91 集合啦!动物森友会

90 神偷2:金属时代

89 模拟城市2000

88 深入(2016)

87 泰坦陨落2

86 托尼霍克职业滑板2

85 怪物猎人:世界

84 生化危机2重制版

83 网络奇兵2

82 真人快打11

81 女神异闻录5皇家版

80 黑暗之魂

79 堡垒之夜

78 神鬼寓言2

77 007黄金眼

76 任天堂明星大乱斗:特别版

75 洞穴探险2

74 奥伯拉·丁的回归

73 DOTA2

72 马里奥赛车8豪华版

71 大金刚

70 模拟人生3

69 细胞分裂:混沌理论

68 超级马力欧世界:耀西之岛

67 寂静岭2

66 侠盗猎车手:圣安地列斯

65 幽浮2

64 控制

63 使命召唤4:现代战争

62 古墓丽影:崛起

61 蝙蝠侠:阿卡姆之城

60 耻辱2

59 见证者

58 风之旅人

57 神秘海域2:纵横四海

56 守望先锋

55 Apex英雄

54 空洞骑士

53 吃豆小姐

52 反恐精英1.6

51 求生之路2

50 地球冒险

49 暗黑破坏神2

48 星际争霸

47 魔兽世界

46 星球大战:旧共和国武士

45 辐射:新维加斯

44 最终幻想6

43 口袋妖怪:黄 这个系列不仅其本身是电子游戏设计与制作的典范,在商业、文化、行业历史等方面有着言语无法描述的巨大影响力,更是我作为一个游戏玩家的入门作。

42 银河战士Prime

41 上古卷轴5:天际

40 生化危机4

39 旺达与巨像

38 最后的生还者2

37 荒野大镖客

36 合金装备5:幻痛

35 文明4

34 塞尔达传说:时之笛

33 我的世界

32 光环:战斗进化

31 半衰期

30 最终幻想14

29 毁灭战士

28 俄罗斯方块

27 合金装备索利德3:食蛇者

26 半衰期:艾利克斯

25 战神

24 时空之轮

23 传送门

22 街头霸王2

21 超级马力欧兄弟

20 传说之下

19 血源:诅咒

18 生化奇兵

17 最后的生还者

16 巫师3:狂猎

15 光环2

14 恶魔城:月下夜想曲

13 哈迪斯

12 侠盗猎车手5

11 超级马力欧兄弟3

10 极乐迪斯科

9 半衰期2

8 荒野大镖客2

7 超级马力欧64

6 质量效应2

5 超级银河战士

4 塞尔达传说:众神的三角力量

3 传送门2

2 超级马力欧世界

1 塞尔达传说:旷野之息

知乎上的一个回答

很有意思的是,前两天我刚在朋友圈推荐了一下我认为能称得上神作的三款游戏:Minecraft、GTA5、塞尔达传说荒野之息 。

其实,评判标准不同,可以有很大的差别。所以我评价这三款游戏只是很简单的,按照我玩过过程中说过的“卧槽”来评价的。当然更客观的理由也是有的:他们在出品的那一年都可以说是惊艳四方、冠绝群雄,GTA和塞尔达系列本就有极高的口碑,但GTA5和荒野之息又把系列拔到了一个新高度(并让我怀疑续作可能就会是下坡路了),MC更是在游戏史上留下重要一笔,以那么小的游戏大小,和java这种这么有历史的语言,创造出了一个无限壮丽的世界,实在是让同行汗颜。

所以要更往深了讲,这三者之间MC还能更胜一筹。把现有的东西随意组合,产生新的化学效应并且成功,不容易,比如传送门。创造一个几乎是新的东西并且成功,更不容易,比如文明。创造一个几乎是新的东西并且影响了整个游戏界、多种不同类型的游戏,难于登天,可遇不可求。就仿佛黑泽明影响了后世的电影、爱手艺影响了西方魔幻作品。

达到这一高度的,只有马里奥兄弟 、Minecraft。

因为题主只要说一个,从我个人的角度,我最后综合投票给了Minecraft。

再扩展一点,不谈对于整个游戏界,只谈在某一领域。

很多答主提到了魔兽世界,尽管它不是第一个MMORPG,但它无疑是最成功的。游戏原创、但最为人所熟知的虚构世界,大概就是魔兽(注意是游戏原创,不是小说或其它作品原创)。运营了那么多年,在这片土地上诞生的传奇故事,其实远比游戏剧情丰富多彩。它在出现的那个时候,和GTA5、荒野之息带给我的震撼差不了多少,并且在此后深深吸引了我8年。但我推荐神作的时候并没有写上它,也是因为网游 总是让我们这些老东西觉得是半成品,故事永远都讲不完,自然就没有一个完整的故事那么让人“舒服”了。(讲真我一直想做一个“有结局”的网游,就像刀剑神域 里一样,看有没有这缘分了吧)

EVE,这是一个完全由玩家自己创造历史的网游,也是最早的一批跨平台网游。这种独树一帜让它前无古人后无来者,详细的不多赘述。

怪物猎人 ,共斗游戏中最成功的一个。

生化危机,当年我和我妈在家里第一台奔四电脑上看到的“回眸一笑”,至今是我个人游戏史重要的一个片段。游戏本身也深深影响了同类游戏。

半条命 /CS,开创者与成功的继任者。

德军总部 3D,第一款3DFPS游戏。

DOTA,如果这不算单独的一款游戏,那就DOTA2。MOBA类游戏一定意义上的开创者和最成功一个。

星际争霸,让电子竞技进入正式、高速发展到伟大作品,游戏本身的平衡性和竞技性也是游戏史上的翘楚。

文明,早期的模拟类游戏中最成功的一个,不仅影响了4X游戏,还促进了模拟类游戏的发展。

宝石迷阵 ,3消游戏始祖(可能存在争议)。

当然,除了上述,可能还有一些没提到的,这些游戏的特点都是,影响了一个游戏领域(文明实际上最多算一个半,4X只是更细分之后出现的游戏分类 ),或者是某一领域的开创者。但他们跳不出自己的领域,因此也就没有被我提升到和马里奥、MInecraft一样的高度。

当然,有一些是可以再议的,比如德军总部3D,和我没提到的俄罗斯方块 ,这两个似乎是可以归结到上述“开天辟地且影响整个游戏圈”的范畴的。

还有一些,则更难分,比如龙与地下城 ,桌游自然也是广义上的游戏,而且龙与地下城也绝对满足上述所有条件。但我想题主主要问的还是电子游戏,所以这也就不算在内了。

  1. 广告
  2. 道具、皮肤、关卡、地图内容
  3. 买断制收费,一次性购买 一次性买断整个游戏,在游戏中不添加任何额外的内购收费项目。这种商业模式类似于收人头税的方式,是一种较为传统的游戏盈利模式。在STEAM和主机等平台的游戏大多以此商业模式为主。游戏设计上,设计师只需全力设计和改善游戏可玩性即可,不需进行圈钱挖坑和内购功能方面的设计。
  4. 打赏
  5. 订阅:包月制收费
  6. 赌博机制收费

游戏网站

  1. https://poki.cn/ 有各种流行的小游戏,游戏数量高达两万多。
  2. https://store.steampowered.com/ steam游戏
  3. https://poki.com/ poki全球版
  4. https://zh.pog.com/ Play Online Games 13万小游戏
  5. https://www.yikm.net/ 街机游戏
  6. https://www.freeonlinegames.com/
  7. http://www.7k7k.com/
  8. https://www.4399.com/
  9. https://xyx.hao123.com/ hao123小游戏
  10. https://dos.zczc.cz/ 在线dox游戏
  11. https://lagged.com/
  12. https://www.crazygames.com/
  13. https://tw.game-game.com/tags/395/
  14. http://bestgames.com/
  15. https://neave.com/
  16. https://www.gameflare.com/
  17. https://nazo.one-story.cn/ :One-Story,网页闯关游戏,有点类似CTF
  18. https://zh.y8.com/
  19. https://www.pogo.com/
  20. https://www.miniclip.com/games
  21. https://www.addictinggames.com/
  22. https://www.ea.com/ea-studios/popcap
  23. https://www.zynga.com/
  24. https://weavesilk.com/
  25. http://slither.io/ 在线贪吃蛇
  26. https://lines.frvr.com/ 在线连连看
  27. https://yandex.com/games/?utm_source=yandex&utm_medium=com&utm_campaign=morda yandex搜索引擎中的游戏
  28. https://www.codingame.com/multiplayer 需要编码的游戏

2023年,地球上人口已经达到80亿。
人类有许多种文化形式:

  1. 影视:电影、电视剧、短视频
  2. 歌曲、音乐作品
  3. 美术作品:雕塑、绘画
  4. 游戏
  5. 文学作品:小说、诗歌

人类创造文化作品的速度是十分惊人的,每天都有大量的歌曲、电影诞生,即便是每天看一个也永远看不完。人对文化作品的消费速度远远小于文化作品的产生速度。

  1. https://picrew.me/ 捏脸游戏
  2. https://thisissand.com/ 撒沙子
  3. https://2017.makemepulse.com/ 一些物理特效展示

谜雨(Puzzle Rain)

《谜雨(Puzzle Rain)》是一款借助WebVR技术制作的VR网页游戏,游戏借助带有美妙音乐的方块组合出美妙的和弦。玩家需要激活所有这些方块并把它们放置在正确的位置,为荒凉的大地注入新鲜雨水,使一切重归美好。

密室逃脱

这是一款线下改编成线上的VR游戏,除了平时偶尔到线下的密室逃脱俱乐部中体验逻辑和烧脑,现再在网页上也能体验找寻线索的乐趣。

画家(Painter)

一款即时喷涂绘画VR游戏,拥有多达30种以上的刷子,画笔颜色、笔触以及大小的调节都极具功能性,玩家可以通过直接分享URL的方式,将作品进行分享。在这里,你可以随心所欲的画,记住别把屏幕画没了就行!

VR乒乓

这款游戏设计有单机和联机的版本,开始游戏后直接进入了一个乒乓球场地,每成功击球计一分,看看你能够连续打中几拍,就是你最后的得分。游戏的模式很简单,戴上VR眼镜用头部控制的方式,可以很有效地治疗颈椎病,和体感逼真度较高的体验不同,不过VR网页版乒乓至少是一种新颖的模式,还是值得体验一番。

爆炸(A-blast)

这是一款单纯的VR网页版波浪射击游戏,玩家使用双激光炮来射击飞行的敌人,以获得高分。不要因为敌人可爱就不忍心下手,毕竟他们可不会手下留情。还有就是眼神好不好很关键~

声波伞

你要把自己当成一把伞,静静的倾听从天而降的各种玩具、乒乓球......然后,然后就会升华了~

周六之夜(Saturday Night)

A-Saturday Night 是一款有趣的WebVR体验,玩家可以在周末的晚上的尽情放松自己,随着音乐起舞。游戏内置有15种角色,在每次的音乐开始之前,玩家有15秒的时间来适应音乐的节奏。玩家的舞步将会被记录下来,当然,这也是通过复制URL的方式直接分享。

甜甜圈

类似于贪吃蛇的VR网页游戏,你将成为一个甜甜圈,用你的围巾包围饥饿的敌人。经典的玩法,甜美的画风,感觉很好~

网易云游戏

https://cg.163.com/#/recommend

腾讯游戏

https://game.qq.com/web202207/introduce.html

微软游戏

https://www.microsoft.com/zh-cn/store/games/windows

XBox游戏

https://www.xbox.com/zh-CN/games/xbox-one

谷歌游戏

https://play.google.com/store/apps/details?id=com.google.android.play.games&hl=zh&gl=US

字节跳动:朝夕光年nvverse

https://www.nvsgames.cn/ 主要产品:花亦山心之月、武林闲侠、晶核

游戏设计

300大作战的战斗场景人物技能:

  1. 光柱伤害:直线
  2. 快速位移
  3. 扇形伤害
  4. 球形伤害
  5. 冰冻
  6. 火球

塔防游戏保卫萝卜的伤害类型: 7. 可以位移的伤害 8. 近距离伤害,远距离伤害 9. 范围伤害,单个伤害

板与球

1962年,麻省理工大学(MIT)电脑·图像专业的 Steve Russell 在大学的研究室里用显像管作为游戏的场域制作出世界上第一款电子游戏《Space War》。从此打开了实时游戏的大门。 1972年,Nolan Bushnell 成立“雅达利公司”,发售大获成功的传奇游戏《Pong》。在此后的数年里,获得相应成功的游戏全部是对“挡板与球”这一游戏形式的变体。球体与立方体形状的刚体之间通过碰撞事件推动游戏的发展与走向。例如 1976 年发行的《打砖块》,1978 年诞生的《太空侵略者》,1983年发行的《太空战机》,1985年诞生的《超级玛丽》将“挡板与球”的游戏形式推向了一个新的高度(把马里奥、敌人看作是球,可踩踏的地面、箱子是砖块)。 假如我们回到那个百花齐放的上世纪七八十年代,我们能让板与球擦出怎样的火花呢?

派对游戏

派对游戏是2人或2人以上玩家参与的多人游戏,可以以多人桌游的形式呈现,也可以在一台游戏设备上通过热座的方式轮流控制自己在游戏中扮演或指挥的人物或国家行动,也可以通过多台游戏设备通过网络通信的方式游玩。通常用于破冰、社交、增进感情(或相反)。

团建游戏

  • 不能有人中途死掉,以免让人感觉缺乏参与感;或者,当有人死掉之后,游戏也应该更快速地结束。
  • 一定要促进交流,语言表达发挥重要作用
  • 三国杀、狼人杀模式,才身份是一个思路

游戏公式

体裁*题材=作品 例如,题材为花朵,体裁有诗歌、散文、戏剧、小说等 100中体裁*100种题材等于10000件作品。 游戏的类型:塔防、棋牌、三消等,游戏的题材:象棋 题材和体裁可以进行各种杂交,例如塔防+棋牌 象棋,卒子骑马,象骑马 http://localhost:8080/Post.html?article=10205448

乐器演奏

人可以弹钢琴 根据手的位置决定音调,根据按键决定行为。 正N面体,使用Unity实现随机旋转的正N面体。 使用Unity实现Android端的音乐播放器,播放我的歌曲。

五行棋

金木水火土,每一轮选择一个棋子,它可以进行生、走。 生:在四联通区域内产生一个新的元素,金生水、水生木等。生成元素的位置只能为空。 走:连通区域内的任何一个元素可以去往该连通区域的四联通位置,移动的时候类似跳棋。去往的位置必须是它所克的元素或者空白。

胜利与失败并存

天之道,损有余以补不足;人之道,损不足以奉有余。
马太效应随处可见,越是胜利,便越是节节胜利。 象棋中,最后很容易形成一边倒的态势,导致失败者觉得索然无味。
好玩的游戏应该像盖高塔,越来越高,离成功越近,同时也随时可能覆灭。

激励人们玩游戏的思路就是让人们感到存在感、优越感。

排行榜

徽章和等级

县长、草民 水金地火木土天海冥

经济:金币和钻石

原文链接

《富春山居图 》这电影我没看过,说实话在上映前无意瞟到海报的时候我就没有去看它的打算,然后我就渐渐从很多人嘴里听说它究竟多烂多烂,再然后我今天一刷新闻,嘿,这货上映三天票房过三亿了。

其实对于这一切我丝毫不感到意外,如果说我曾经是个一身戾气,觉得自己很了不起并什么都看不惯的小愤青的话,现在的我可能更多地尝试少说多想,慢慢用自己的方式去理解这个我一度非常看不懂的世界,为了能很好地来表述清楚我的观点,我先从我自己的故事说起。

我现在是一个游戏策划,说得通俗点,就是做游戏的,我从小到大玩过许许多多的游戏,从单机到网游,从小霸王到PSP,我可以算是一个游戏达人。如果问我为什么要去做游戏策划,说得冠冕堂皇一些,我想做一款自己的梦想中的游戏,希望它能被很多人玩到并喜欢,能成为像《口袋妖怪》,《生化危机》,《魔兽世界》之类经典的作品。之前我去公司实习的时候,有一个策划班培训,有一天老师布置的作业题目叫《我的游戏梦

》,我把自己想要做的网络游戏蓝图写了进去,包括要取消网游中“等级”这个设定,玩家不再需要打怪升级,而是用另外的一种收集“魂”的方式来代替它,取消玩家间“职业”的限制,你究竟是法师战士还是奶妈取决于你“魂”的搭配等等,总之写得天花乱坠,我自我感觉良好地认为这实在是史无前例的创意,因为目前市面上还没有一款游戏这么做过。

没想到第二天我反而被老师给说了一顿,倒不是那种批评的言辞,而更像是泼凉水,意思是取消“等级”在网游中并不现实,我的“魂收集率”和“等级”在本质上并没有多大的区别,与其拐弯抹角那么麻烦,还不如“等级”来得让人容易接受。然后我就觉得挺不爽的,心想这完全是把一款伟大的游戏扼杀在摇篮里了啊,但我也没有再说些什么。之后老师让我们体验一款网页游戏

,并让我们每个人写一份体验报告,由于这游戏的确是非常无聊,这原本是一款ARPG游戏,玩家的一个个技能都应该是用手点的,但是这游戏在副本居然挂机就能通关,点下鼠标左键后一个键也不用摁就啪啪啪打完了,于是我昏昏欲睡地挂了一下午机,就四十级了,我之后在体验报告里把这游戏批得体无完肤,说这游戏实在不能再垃圾了。之后不负众望的是,第二天老师当着全班面批了我,他问我知道这游戏一个月能赚多少钱么,我摇摇头,他说:“三千万”。

当时的感觉应该还是蛮震惊的,应该会像很多人听说《富春山居图》三天票房过亿的感受一样,我只知道自己默默思考了很多天,其中有迷茫有自我怀疑,更多的是一种失望,心想这个世界究竟是怎么了呢,我一个堂堂游戏达人都觉得烂的游戏,竟然一个月能赚三千万,究竟是我疯了还是这个世界疯了呢。

但冷静下来仔细想想,我渐渐让自己回归一个低姿态,我开始抛开自己的偏执认真去审视很多事情背后隐藏的东西,这让我逐渐有了些茅塞顿开的感觉。我想到网页游戏这东西,会去花时间甚至花钱玩的人绝对都不是学生,而更多的是那些有钱没什么时间的上班族甚至老板,他们每天都很忙,能有空玩玩游戏只能是下班回家后,以及上班时的空余时间。试想一下,他们有可能在上班时候,或者累了一天下班以后,戴着耳机开着YY疯狂地敲着键盘大喊“T顶上奶妈奶好”刷一两个小时副本么,答案是否定的。因为他们想要的不过是一个点开就能玩,花点钱就能比别人牛逼,挂挂机就能升级的游戏,而这些人是游戏中的大R,也就是花钱最多的人民币玩家,他们为游戏公司输出了最多的利益。

所以只是因为你所处的圈子不一样,你的品位和别人不在一个档次上,让你更加容易被一种与生俱来的优越感蒙蔽了双眼,仿佛这个世界应该朝着你想象中的方式来运行。事实上我后来调查了一下,在游戏市场中我这样的所谓“游戏达人”所占的比例是微乎其微的,而像我这样挑剔的游戏玩家也并不在游戏公司的市场定位里,因为我们不仅不那么轻易地掏钱,还喜欢挑三拣四骂骂咧咧,把人家口碑弄得很差。

所以我很快就能理解为什么优酷的广告里会出现类似“油腻的师姐 ”的广告了,你觉得“秒杀BOSS,怒刷装备”是件很愚蠢的事情,你发誓你打死也不会去玩这样的游戏,但其实你没看见,有的人就是会被“老婆不在家的时候玩的游戏”这样的小弹窗骗进去,会花钱在一个点开就能玩,点红叉叉就能关掉的游戏上,我特意查过相关资料,发现37wan

现在拥有1.5亿的注册用户,并且它有超过十款页游月入千万。

我之所以铺垫这么多来说这件事情,其实是想告诉大家,这个世界并不是你想象中的那么简单,你看到的永远只是表象。试想一下为什么《最炫民族风》,《江南style》能火,为什么满大街的小店里放的都是听起来一模一样的网络歌曲,你走过路过总会露出一抹不屑的微笑,心想这些东西真是太没品位了,土得要命,为什么没什么人唱摇滚搞民谣,写歌的都是傻逼么。你上网看小说,都是穿越回明朝做娘娘,霸道的总裁爱上我,中国再也没有《红楼梦》,《围城》这样的作品了么,写文章的人都是傻逼么。你去电影院看电影,尼玛鬼片拍得让人想笑,喜剧片各种无厘头情节,然后你觉得你心中的大卫林奇

,昆丁,诺兰受到了侮辱,拍电影的导演都是傻逼么。于是你觉得愤怒,觉得这个世界不可理喻,如果让你来当作曲家,作家,导演,你的作品肯定能比这些人强。

然而实际上没有人是傻逼,你以为科班出身的导演没看过《穆赫兰道 》《低俗小说》,会写歌的人没听过枪花涅槃radiohead么,事实上教我们策划的老师也是忠实的《魔兽世界》玩家,还在课上兴致勃勃地给我们讲当年联盟新年来奥格瑞玛

屠城的故事呢。如果说他们会做出类似的作品来,肯定不是因为他们的品位没你高,只是因为他们比你更懂市场。他们并不是在做艺术品,而是商品,前后者的区别在于,前者靠口碑,后者则是赤裸裸的看利益,我不相信《富春山居图》的导演分不清什么是好片什么是烂片,但他很显然知道拍什么电影能赚钱。

就从电影的角度来说,我反复和朋友建议过,单凭豆瓣和时光网 的评分,不能够说明任何问题,因为如果假设评分是一个随机抽样调查

模型的话,这个样本显然过于局限性。中国市场绝大部分的观影者并不会玩豆瓣,就像我父母辈这样的60后,还有许多非网民,即使一部电影在豆瓣百分之九十的人都给五星,也不能说明这电影就是被大多数人喜欢的,例如之前口碑很好的国产电影《钢的琴》,在豆瓣上超过10万人评价,给出8.2的高分,但是这部电影的票房只有区区400万(非常讽刺的一件事情是,假如看过电影的这10万多人一人花40去电影院看这部电影,它的票房就已经超过400万了),可见一部电影的口碑是非常具有迷惑性的,毕竟并不是所有人都这么“高雅”“有品位”,试想一下究竟会有多少人周末花钱走进电影院去看一部几乎没有大牌明星,剧情看起来也不太“猎奇”或“刺激”的片子呢,你不可以拍胸脯说你就会去看一部这样的电影,因为你不能代表另外一个群体的价值观,就像《少年派》这样的片子我爸妈都说一点也不好看,因为坐了两个多小时完全没有看懂。

因此如何更好地去解读《富春山居图》现象,需要我们理解国内市场的状况以及背景。我记得之前我问一个学广告的同学,为什么中国的广告这么难看,有的甚至还很弱智,国外的很多广告好像多好搞笑好有创意哦,有的就像小电影和连续剧一样,怎么中国的电视剧和电影反而更像是广告呢。他告诉我说,国内商家在电视台做的广告每一秒的成本都是十几万甚至几十万,没有人会冒这么大风险在大家都普遍用那么直白的营销手段的环境中过分标新立异,如果短短十秒钟你还不能让你的消费者知道你在卖什么,那你这广告就算是失败了。因此不是因为中国的广告人没创意,而是他们依然还是要以利益为出发点,那些我看得津津有味的广告,也许我隔壁的大妈我楼下的小弟就是看不懂,试想如果百分之五十的观众看不出你所谓的创意,那它带来的收益还不如十秒里把产品的名字念十遍来得多。同样的“十秒钟定律

”可以适用于我刚才所提到的所有问题,例如重金属音乐,喜欢的人爱得不行,不喜欢的人听不到十秒就关了,你让他认真听完去体会里面的内涵,这是非常不现实的一件事情,再例如电影《穆赫兰道》,我看完电影看了几篇影片才明白它的奥妙所在,你让观众带本说明书花钱去电影院看电影,这显然是在开玩笑。

另外一方面,我把原因归结到这个信息与商品爆炸的时代。当选择余地越来越多了以后,消费者就开始变得越发挑剔了,大多数人的耐性也变得越来越低,需要商品主动去迎合绝大多数人的口味,而不是仅仅做好自己之后等待消费者的挑选,因此这是一个“被惯坏了”的消费时代。就像我老师所说,一款网页游戏的流失率高达百分之九十,它能不能有人玩,很大程度上就取决于玩家点进来试玩的那几分钟,所以它可以“一键挂机”,“秒杀BOSS”,就是为了留住它的目标群体。平心而论,如果说过去是消费者在选择产品,那现在的市场则更像是产品在主动寻找它的受众群体。因为在很久之前,电影很少,音乐很少,小说很少,游戏更是没人玩,在不大的市场中,它们可以选择的群体有限,而观众听众读者和玩家们能够很容易地去选择自己喜欢的东西,并且那个年代国内人的口味也相对较为一致,至少同龄人之间的品位差距不会太大,比如我们的父母都听过邓丽君

,都崇拜过“四大天王”,在那样一个年代,想要剑走偏锋是没有活路的。然而在现在这个年代,由于一些行业门槛的降低,“草根阶级”的崛起,很多东西开始走下神坛,现在大学生都能拍电影,街边卖唱的都能出唱片,小学没毕业都能写小说出书,消费者的选择变得非常之多,因此商品必须把橄榄枝抛向那些最能为它带来利益的那个群体。

另外,随着文化的多元化,大众的品位开始走向截然不同的方向,不同年龄段不同文化水平不同工作乃至不同地域的人,喜欢的东西都开始变得光怪陆离,并且彼此组成了相互隔绝的圈子,相互排斥相互鄙夷,一些反传统非主流

的东西也因为“求异”的新潮感开始受到了很多人的追捧。这些都解释了为什么一些奇奇怪怪的东西开始走红,而一些大家都说好的东西,反而有遇冷的可能性。例如“凤姐”为什么能够越被骂越红,最后赚到钱成功飞往美利坚,而脸蛋身材都还算标致的女人,只要不“搏出位”,根本就火不了。正是因为这个年代“美”已经饱和,而只要能够在十秒钟内抓住你眼球的东西,都能够成为利益的来源,无论它是美是丑,让你赏心悦目还是让你作呕,只要目的能够达到,那么一切手段都是行得通的。

所以不要以为《富春山居图》烂,它就赚不到钱,那些本该觉得这片子烂的人,还有人愿意花钱去电影院看看它究竟有多烂,就更别提那些追星的,看特效的,周末花点钱去电影院随便看部片子娱乐一下的人了,如果说后者是这部电影票房的基础,那么前者就是这部片子票房奇迹的催化剂了,按正常的市场预期,这样一部片子的确是有人去看,但火不到这个地步,我相信尽管导演知道这电影能赚钱,但没想到会赚这么多,因为并不是所有类似的片子都会有人愿意骂的,当然除非“骂”本身也是一种炒作。我坦诚我的确没看过这部电影,不然为了写这篇文章,我又得为它的票房作出贡献了,但我可以推测,这电影虽然剧情可能如传言一样的确非常奇葩之外,至少有几个明星有那么点特效,在这个前提下,只要有足够多的曝光率,无论是捧是喷,那它绝对是不会亏本的。

我写这篇文的目的不是为了抨击什么现象,或者针对《富春山居图》这部电影,事实上不同的商品满足不同人的需求,这本身并没有什么过错,从票房上看,确实有不少人就是爱看这样的片子,觉得这片子不烂,这是个人喜恶,我无权干涉。但是不得不承认的是,我们的市场的确还不够成熟,不能为一些真正好的东西提供保障,反而成为一些残次品甚至“畸形儿”温床,试想一下,当一个好的东西不能给他的生产者带来甜头时,自然没有人会愿意去冒这个风险将它做得“好”,究其原因,我不再过多阐述。

在这篇文的结尾,我更想说的一点是,无论一个东西它有多么地不入你的眼,我们依然还是要抛开自己的傲慢与偏见去剖析它,并从它的身上领悟到一些东西,如果你想要改变一个东西,首先你要足够透彻它的运行机制,正如要战胜你的对手,你必须绝对了解他一样。在许多关于《富春山居图》的影评中,我看到的更多是一种戏谑与傲慢,正如我当时歪着嘴角摇着头写那篇体验报告一样,但现在我明白,世界上比你牛逼的人多了去了,你能够想到的别人一定也曾经想到过,他们如果没有做到,做得很离谱,甚至偏离了原来的方向,那么一定是因为一些原因被这个世界所改变了,然而如果你想要改变他们所不能改变的世界,你必须首先学会谦卑,冷静和客观。

毕竟,这个世界并不像你想象的那么简单。

PIXI.JS

PIXI是使用WebGL实现的前端绘图库。
最好的2D库就是PIXI,性能无敌;最好的3D库就是Three.js。
其它库就是一些游戏框架,很多游戏框架都是在这两个基础库的基础上封装的。

创建应用

import { Application } from 'pixi.js'
const app = new Application({
  width: 800,
  height: 800,
  antialias: true,    // default: false 反锯齿
  transparent: false, // default: false 透明度
  resolution: 1       // default: 1 分辨率
})
document.body.appendChild(app.view)

创建容器

// 在continer1中创建一个起点(0,0),宽高100*100的矩形
const container1 = new Container()
const rectangle = new Graphics()
rectangle.beginFill(0x66CCFF)
rectangle.drawRect(0,0,100, 100);
container1.position.set(100, 100)
container1.addChild(rectangle)
app.stage.addChild(container1)

添加文本


container1.position.set(100, 100)
const style = new TextStyle({
 fontSize: 36,
 fill: "white",
 stroke: '#ff3300',
 strokeThickness: 4,
 dropShadow: true,
})
const message = new Text("你好 Pixi", style)
container2.position.set(300, 100)
container2.addChild(message)

添加精灵

// 加载一张背景图
const bg = Sprite.from(images/lol-bg.jpg)
app.stage.addChild(bg)

加载图片

const IMAGES = [{
  name: '1',
  url: 'images/1.png'
}, {
  name: '2',
  url: 'images/2.png'
}, {
  name: '3',
  url: 'images/lol-bg.jpg'
}]
app.loader.add(IMAGES).load(() => {
    console.log('加载完成')
})

使用graphics

import { Application, Graphics } from 'pixi.js';

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

// 创建一个半径为32px的圆
const circle = new Graphics();
circle.beginFill(0xfb6a8f);
circle.drawCircle(0, 0, 32);
circle.endFill();
circle.x = 130;
circle.y = 130;

// 添加到app.stage里,从而可以渲染出来
app.stage.addChild(circle);

添加精灵图片

import { Application, Sprite } from 'pixi.js';

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

// 创建一个图片精灵
const avatar = new Sprite.from('http://anata.me/img/avatar.jpg');

// 图片宽高缩放0.5
avatar.scale.set(0.5, 0.5);

app.stage.addChild(avatar);

精灵添加事件

const avatar = new Sprite.from('http://anata.me/img/avatar.jpg');
avatar.scale.set(0.5, 0.5);
// 居中展示
avatar.x = 100;
avatar.y = 100;

// 可交互
avatar.interactive = true;
// 监听事件
avatar.on('click', () => {
   // 透明度
   avatar.alpha= 0.5;
})
app.stage.addChild(avatar);

让精灵一直转圈圈

const avatar = new Sprite.from('http://anata.me/img/avatar.jpg');
avatar.scale.set(0.5, 0.5);
avatar.x = 150;
avatar.y = 150;

// 修改旋转中心为图片中心
avatar.anchor.set(0.5, 0.5)

app.stage.addChild(avatar);

app.ticker.add(() => {
  // 每秒调用该方法60次(60帧动画)
  avatar.rotation += 0.01;
})

PIXI中的概念

  • Container (舞台,场景):Application.stage就是一个Container类型的对象
  • Renderer (渲染器)
  • Ticker (计时器)
  • Loader (资源加载器)
  • Sprite (精灵)
  • Graphics(图画):跟精灵一样,都是可以响应事件的元素。

Container

一个Container可以包含若干个对象,修改Container的属性时会影响它包含的子元素,例如修改位置、透明度等都会影响子元素。

Renderer

Application.renderer是一个Renderer的实例,如果希望重新渲染页面,可以使用这个对象。

// 把画布重新渲染为500*500大小
app.renderer.resize(500, 500);

// 渲染一个容器
const container = new Container();
app.renderer.render(container);

Sprite:精灵

可以根据图片渲染出来

const avatar = new Sprite.from('http://anata.me/img/avatar.jpg');

// 和普通的图形一样可以设置各种属性
avatar.width = 100;
avatar.height = 200;
avatar.position.set(20, 30);
avatar.scale.set(2, 2);

Loader:用于加载网络资源

import { Application, Sprite, Loader } from 'pixi.js';

// Loader.shared内置的单例loader
const loader = Loader.shared;

// 也可以使用自定义的loader
const loader = new Loader();

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

loader
.add('bili', 'http://pic.deepred5.com/bilibili.jpg')
.add('avatar', 'http://anata.me/img/avatar.jpg')
.load(setup)

// 监听加载事件
loader.onProgress.add((loader) => {
  console.log(loader.progress);
}); 

function setup() {
  const bili = new Sprite(
    loader.resources["bili"].texture
  );
  bili.width = 50;
  bili.height = 50;
  
  const avatar = new Sprite(
    loader.resources["avatar"].texture
  );
  avatar.width = 50;
  avatar.height = 50;
  avatar.position.set(50, 50);

  app.stage.addChild(bili);
  app.stage.addChild(avatar);
}

loader.add(xxx).add(yyy).load(callback) 给loader添加一些需要加载的资源,当加载完成之后调用setup函数。
在加载资源的过程中,可以通过loader.onProgress监听进度条事件,可以制作加载动画。

在游戏制作过程中,经常把多张图片打包成一张图片,称为图集。loader也可以加载图集,图集包括一张大图和一个JSON文件,JSON文件描述了每一个小图片的位置和大小。

import { Application, Container, Sprite, Graphics, Loader, Spritesheet } from 'pixi.js';

// myjson记录了每张图片的相对位置
import myjosn from './assets/treasureHunter.json';

// mypng里面有多张图片
import mypng from './assets/treasureHunter.png';

const loader = Loader.shared;

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

loader
.add('mypng', mypng)
.load(setup)

function setup() {
  const texture = loader.resources["mypng"].texture.baseTexture;
  const sheet = new Spritesheet(texture, myjosn);
  sheet.parse((textures) => {
    // mypng里面的一张叫treasure.png的图片
    const treasure = new Sprite(textures["treasure.png"]);
    treasure.position.set(0, 0);

    // mypng里面的一张叫blob.png的图片
    const blob = new Sprite(textures["blob.png"]);
    blob.position.set(100, 100);
    
    app.stage.addChild(treasure);
    app.stage.addChild(blob);
  });
}

Ticker:Ticker可以用于制作动画

import { Application, Sprite, Loader } from 'pixi.js';

const loader = Loader.shared;

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

loader
.add('bili', 'http://pic.deepred5.com/bilibili.jpg')                      
.load(setup)

function setup() {
  const bili = new Sprite(
    loader.resources["bili"].texture
  );
  bili.width = 50;
  bili.height = 50;

  app.stage.addChild(bili);

  app.ticker.add(() => {
    if (bili.x <= 200) {
      bili.x += 1;
    }
  })
}

使用前端的requestAnimationFrame也能够实现类似的效果

function setup() {
  const bili = new Sprite(
    loader.resources["bili"].texture
  );
  bili.width = 50;
  bili.height = 50;

  app.stage.addChild(bili);

  function move() {
    if (bili.x <= 200) {
      bili.x += 1;
      requestAnimationFrame(move)
    }
  }

  requestAnimationFrame(move)

}

补间动画

Ticker可以实现简单的动画,但是如果我们希望实现一些复杂效果,就需要自己编写很多代码。这时候可以选择一个兼容PIXI的动画库。常见的动画库有Tween.js,TweenMax等。

import { Application, Sprite, Loader } from 'pixi.js';

import { TweenMax } from 'gsap/all';

const loader = Loader.shared;

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});


document.body.appendChild(app.view);

loader
  .add('bili', 'http://pic.deepred5.com/bilibili.jpg')
  .load(setup)

function setup() {
  const bili = new Sprite(
    loader.resources["bili"].texture
  );
  bili.width = 50;
  bili.height = 50;

  app.stage.addChild(bili);

  // 1s内x和y轴移动100
  TweenMax.to(bili, 1, { x: 100, y: 100 });

}

TweenMax还提供了一个PixiPlugin,从而TweenMax可以理解PIXI的属性

import { Application, Sprite, Loader } from 'pixi.js';
import * as PIXI from 'pixi.js';
import gsap, { TweenMax, PixiPlugin } from 'gsap/all';

// 注册插件
gsap.registerPlugin(PixiPlugin);
PixiPlugin.registerPIXI(PIXI);

const loader = Loader.shared;

const app = new Application({
  width: 300,
  height: 300,
  antialias: true,
  transparent: false,
  resolution: 1,
  backgroundColor: 0x1d9ce0
});

document.body.appendChild(app.view);

loader
  .add('bili', 'http://pic.deepred5.com/bilibili.jpg')
  .load(setup)

function setup() {
  const bili = new Sprite(
    loader.resources["bili"].texture
  );
  bili.width = 50;
  bili.height = 50;

  app.stage.addChild(bili);


  // 一次修改多个属性
  TweenMax.to(bili, 1, { pixi: { scaleX: 1.2, scaleY: 1.2, skewX: 10, rotation: 20 } });

}

自定义Application

Applicatioin等于stage+loader+ticker等。
我们也可以自己创建这些对象。

import { Container, Renderer, Sprite, Loader, Ticker } from 'pixi.js';
import { TweenMax } from 'gsap/all';

// 自定义render
const renderer = new Renderer({
    width: 300,
    height: 300,
    antialias: true,
    transparent: false,
    resolution: 1,
    backgroundColor: 0x1d9ce0
});

// 自定义container
const stage = new Container();

// 自定义loader
const loader = new Loader();

// 自定义ticker
const ticker = new Ticker();

// 每次屏幕刷新重新渲染,否则只会渲染第一帧
ticker.add(() => {
    renderer.render(stage);
});

// 开始执行ticker,一定要调用这个方法,注册的回调函数才会被执行!!!
ticker.start();


document.body.appendChild(renderer.view);

loader
    .add('bili', 'http://pic.deepred5.com/bilibili.jpg')
    .load(setup)

function setup() {
    const bili = new Sprite(
        loader.resources["bili"].texture
    );
    bili.width = 50;
    bili.height = 50;

    stage.addChild(bili);

    // 动画效果
    ticker.add(() => {
        if (bili.x <= 200) {
            bili.x += 2;
        }
    });

    TweenMax.to(bili, 1, { y: 100, delay: 3 });
}
//兼容鼠标和触摸屏的共同触发
type InteractionPointerEvents = "pointerdown" | "pointercancel" | "pointerup" | "pointertap" | "pointerupoutside" | "pointermove" | "pointerover" | "pointerout";
//触摸屏触发事件
type InteractionTouchEvents = "touchstart" | "touchcancel" | "touchend" | "touchendoutside" | "touchmove" | "tap";
//鼠标触发事件
type InteractionMouseEvents = "rightdown" | "mousedown" | "rightup" | "mouseup" | "rightclick" | "click" | "rightupoutside" | "mouseupoutside" | "mousemove" | "mouseover" | "mouseout";

godot

Godot基于何种GUI库实现?

Godot 不使用标准的 GUI 工具箱,如 GTK、Qt 或 wxWidgets。相反,Godot 使用自己的用户界面工具包,使用 OpenGL ES 或 Vulkan 进行渲染。这个工具包以控件节点(Control)的形式暴露出来,用于渲染编辑器(用 C++ 编写)。这些控制节点也可以在 Godot 支持的任何脚本语言的项目中使用。

这个定制的工具包使它能获益于硬件加速,并在全平台上拥有一致的外观。最重要的是,它不必处理 GTK 或 Qt 所带来的 LGPL 许可注意事项。最后,这意味着 Godot 在“自产自用”,因为编辑器本身就是 Godot UI 系统中最复杂的用例之一。

这个自定义 UI 工具包不能作为一个库使用,但你仍然可以通过使用 Godot 编辑器来创建非游戏应用程序。

Godot为什么要造GDScript,而不是使用已经存在的、有广泛用户的语言?

为 Godot 创建一个紧密集成的自定义脚本语言的原因有两点:首先,它减少了启动和运行Godot 所需的时间,使开发人员能够快速上手引擎,提高了生产力;其次,它减少了维护的总体负担,减少了问题的维度,并允许引擎的开发人员专注于排除错误并改进与引擎核心相关的功能——而不是花费大量时间来尝试在一大堆语言中获得一小组增量功能。 由于 Godot 是开源项目,因此从一开始就必须优先考虑更加集成和无缝的体验,而不是通过支持大多人熟悉的编程语言来吸引更多用户——特别是在支持那些大多人熟悉的语言会导致更糟糕的体验时。我们理解你更想在 Godot 中使用其他语言(请参阅上面支持的选项列表)。话虽如此,如果你还没试过 GDScript,先试三天。就像 Godot 一样,一旦你看到它有多强大并且开发多迅速,我们认为你将对 GDScript 刮目相看。

为 Godot 创建自定义脚本语言的主要原因有:

  1. Godot 使用多线程,而大多数脚本虚拟机对线程的支持不佳(Lua、Python、Squirrel、JavaScript、ActionScript 等)。
  2. 大多数脚本语言(Lua、Python、JavaScript)的虚拟机没有很好地支持类扩展,适配 Godot 工作方式的效率极低。
  3. 许多现有语言的 C++ 绑定接口都非常糟糕,会产生大量代码、错误、瓶颈,而且效率普遍低下(例如 Lua、Python、Squirrel、JavaScript 等)。我们希望专注于一个更好的引擎,而不是大量的缝合。
  4. 没有原生的向量类型(vector3、matrix4 等),导致使用自定义类型实现时,性能大大降低(Lua、Python、Squirrel、JavaScript、ActionScript 等)。
  5. 在大部分解释型编程语言(Lua、Python、JavaScript、ActionScript 等)中,垃圾收集器会导致延迟或不必要的大内存使用。
  6. 难以与 Godot 代码编辑器集成从而支持代码补全、动态编辑等(其他语言都这样)。但这方面 GDScript 支持得很好。

GDScript 是为了减少上述问题以及防止更多问题而设计的。

Godot官方教程中用很多篇幅描述了GDScript的必要性,然而开发者并不太买账。
在我看来,无法使用一个现有语言的主要缺点包括:

  1. 无法使用该语言生态中的库。
  2. 开发者需要学习一门新语言
  3. GDScript用途过于有限,仅能用于一个游戏引擎

造出GDScript虽然对于Godot作者来说简单了很多,但是对于Godot用户来说却变复杂了一些。

Godot的历史

2001年Godot开始开发,2014年开源,最初是由一家阿根廷游戏工作室内部开发。

godot和mono和Android

godot有两个版本:.net版和标准版。
godot支持Android吗?只有标准版才支持Android,mono版并不支持android。

如果使用mono版的godot,在导出的时候需要安装导出模板。
mono版的godot不支持Android平台的导出,只支持PC端的导出。

转换项目之后只是单纯转换,要想打开还需要双击才能打开。

克隆github上的godot官方demo

https://github.com/godotengine/godot-demo-projects

下载godot

使用godot扫描godot-demo-projects文件夹

godot导入项目使用的是入口文件project.godot.

逐个打开项目进行查看

godot中文社区

godot工坊:https://godot.pro/ 中文godot社区。 https://godoter.cn/

godot中文文档

godot中文文档: https://docs.godotengine.org/zh_CN/latest/

https://godot-zh-cn.readthedocs.io/zh_CN/latest/classes/class_gltfcamera.html

oculus教程:https://developer.oculus.com/documentation/native/android/mobile-openxr/ openXR官方文档:https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html https://github.com/KhronosGroup/OpenXR-SDK-Source/tree/master/src/tests/hello_xr

  • pico应用商店:https://www.pico-interactive.com/cn/store.html
  • pico设备neo3:https://www.pico-interactive.com/cn/neo3/
  • pico开发者平台:http://developer.pico-interactive.com/
  • pico demo:https://github.com/picoxr/support
  • unity xr SDK:https://sdk.picovr.com/docs/UnityXRSDK/cn/chapter_one.html

python

物理引擎

pymunk pybullet

游戏框架

pygame

unity

概论

https://learn.unity.com/tutorial/shi-yao-shi-dots-wei-shi-yao-shuo-dotsfei-chang-zhong-yao?language=zh#

DOTS:Data Oriented Tech Stack,面向数据的技术栈。

DoD:Data Oriented Design。

OOD:Object Oriented Design。

作用:提升游戏性能,目前的架构阻碍了游戏性能的优化。

定位:Unity的下一代架构,是Unity的未来架构、最终架构。不仅仅是Unity,Unreal也正在转向DOTS。DOTS系统是游戏开发领域的开发范式的一次革命,影响巨大。DOTS之后,会产生新的一批有影响力的大型游戏。

在现在的游戏开发中,每一个GameObject都有自己的数据和行为,这叫面向对象。DOTS认为,一个游戏只需要定义一大堆全局变量就足够了,游戏逻辑通过函数控制。说白了,DOTS等于面向过程的卷土重来。

包括三部分,可以看出这三部分都是为了优化性能:

  • ECS:Entity Component System,组件化、数据化的开发模式。它的主要思想就是对数据结构加以组织,提高缓存命中率。Entity+Component都是数据,System是行为。

  • Job System:更加安全、简便地实现多线程。充分利用多核。目前Unity渲染都是单线程的,没有充分利用多核进行渲染。

  • Burst编译:基于LLVM的高效编译器

那么DOTS到底能够提升多大的性能呢?根据官方Demo估算,大概有10倍性能提升。

在DOTS的三个核心部分中,只有ECS会影响到开发者的代码。这一部分的主要思路就是:放弃面向对象,改为面向数据,大量地使用全局变量,游戏画面只是针对这些全局变量的渲染。

Unity正在使用DOTS革自己的命,为什么要放弃面向对象?

  1. 多人游戏场景下,游戏对象太多非常复杂,影响性能。

  2. DOTS能够模拟大量的、成千上万的Agent。

  3. 面向对象的GameObject总是在渲染,而DOTS则能够做到局部渲染,只渲染用户视野范围内的物体即可。

Megacity

巨型都市,Unity的一个Demo,用于演示DOTS的强大功能。

DOTS系统三个包,分别对应DOTS的三个重要组成部分:

Burst:对应Burst

Entities:对应Entity

Jobs:对应Jobs

产品列表

Editor

  • Unity专业版、加强版、个人版

  • Plastic SCM:git仓库管理工具,针对大文件进行优化

  • Bolt:可视化编程工具

  • Unity Mars:构建AR程序的工具

    服务端

  • 游戏分发平台

  • Multiplay:游戏托管服务,匹配

  • 云端资源分发平台

  • 语音文本通信Unity Vivox

  • 分布式算力方案

解决方案

  • 游戏
  • 汽车、制造
  • 建筑
  • 电影动画

官网域名

脚本方面

  1. 不需要高频调用的,使用InvokeRepeating或者Time.frameCount%n,总之添加一些频控。
  2. Dictionary使用TryGet代替ContainsKey
  3. 使用对象池

全球版:https://learn.unity.com/, VR开发专题:https://learn.unity.com/pathway/vr-development?signup=true

国内版:https://learn.u3d.cn/

可选包:https://docs.unity3d.com/Manual/pack-safe.html

w3school:https://www.w3cschool.cn/unity3d_jc/unity3d_jc-6jet38c2.html

unity国内开发者社区:https://developer.unity.cn/

Unity资源商店:https://assetstore.unity.com/

Unity教程的结构

Unity教程分为手册和脚本API两部分。

手册是功能说明。

脚本API 是API Reference。

Unity教程都是讲的Unity的内置模块。

Unity Registry是Unity官方提供的可选包,这些可选包的文档在另一个地方:https://docs.unity3d.com/Manual/pack-safe.html

Unity教程的主题

其中比较重要的是图形、物理,了解了这两块基本上做游戏就没问题了。我最需要恶补的就是图形。

输入

2D

图形

世界构建:地形、树

物理

脚本

多人游戏

音视频

动画

UI

导航和寻路

Unity服务

XR

Unity的包

打开Unity的PackageManager,浏览一下Unity的各种包。

Unity的包分为四类:

  • 内置包:Unity 编辑器自带的包

  • Unity Registry:Unity官方包

  • InProject:项目中已经导入的包

  • MyAssets:我收藏的包

Unity的内置包

UI

  • IMGUI:Unity的立即UI,只能用在Editor中

  • UI:Unity的拖拽式UI,每一个元素都是一个GameObject,只能用在运行时

  • UI Elements:Unity新版UI

  • UI Elements Native

地形

  • Terrain

  • TerrainPhysics

  • Tilemap

  • Wind:风区,可以影响地形和粒子效果

物理

  • Cloth

  • Vehicles:汽车的物理模拟,使用轮子碰撞器组件

  • ParticleSystem

  • Physics、Physics2D

多媒体

  • ImageConversion

  • ScreenCapture

  • Audio

  • Video

网络

  • Unity Web Request

    • Unity Web Request Asset Bundle

    • Unity Web Request Audio

    • Unity Web Request Texture

其它

  • AI:实现了寻路算法

  • AndroidJNI

  • AssetBundle

  • Director

  • JsonSerialize:Unity最早实现的JSON序列化库,基本上全是bug。

  • Subsystems

  • Umbra:遮挡剔除系统

  • Analytics:埋点相关

  • XR、VR:

  • Wind

Unity官方包

2D相关

  • 2D Animation

  • 2D IK:preview阶段,根据部分动作预测整体动作。

  • 2D Pixel Perfect

  • 2D PSD Importer

  • 2D Sprite

  • 2D SpriteShape

  • 2D Tilemap Editor

  • 2D Tilemap Extras(Preview阶段)

自适应性能

  • Adaptive Performance

  • AdaptivePerformance 三星

Unity服务

Advertisement:广告;iOS14 Advertising Support:iOS上的广告支持

Authentication:授权服务,可以用Oculus等一些著名的ID进行登录。

Economy:经济

UDP:Unity Distribution Portal,让开发者可以访问其它三方Android商店。

GameFoundation:

基础服务

Cloud Code:云服务,server-less

CloudSave:云存储

RemoteConfig:类似字节的TCC,存储远程配置。

CCD:Cloud Content Dilivery,云内容,例如直播流、视频流等

Analytics

Analytics Library:Unity的埋点、客户端分析服务

WebGL Publisher:将webgl导出的东西一键部署到远端,类似一个静态站点。

IAP

因为Unity对IAP投入力度非常大,涉及模块较多,因此单独讨论。

小米SDK:小米商店的支付SDK

In App Purchasing

多人游戏

Lobby:多人游戏

MultiPlayerHLAPI:多人游戏高级API

Netcode For Entities:用于DOTS的多人游戏

Relay:中继

XR

XR部分有很多公司的XR 开发插件。

  • AR foundation

  • ARCore XR Plugin:苹果的XR工具包

  • ARKit Face Tracking

  • ARKit XR Plugin

  • MagicLeap XRPlugin

  • Oculus XR Plugin

  • OpenXR Plugin

  • Windows XR Plugin:提供了Unity XR SDK的实现,允许使用windows 混合现实设备。

  • XR Interaction Toolkit

  • XR Plugin Management

  • MockHMD XR Plugin

文件格式

  • FBX Exporter:允许将场景导出为fbx文件,然后再AutoDesk、maya、3dmax等软件中编辑,然后导回到unity。

  • Alembic:支持.abc文件,这是一种动画传输文件。

移动端

Android Logcat:用于在UnityEditor中展示来自Android设备的日志。可以在Window/Analysize/Android Logcat中查看。

DeviceSimulator:它是过去Game窗口的替代者,Game窗口的缺点就是尺寸无法模拟真实设备的尺寸。

编辑器工具

QuickSearch:在Assets、场景、菜单、包、API、配置中搜索,相当于Unity中的SearchEverything

EditorCoroutines:在Unity Editor中构建类似MonoBehavior的对象。

Unity Recorder:在编辑器的Play模式下允许录制动画、音频、视频等。

测试

ProfileAnalyzer:性能分析工具

MemoryProfiler:内存分析工具

TestFramework:包括一个UI和一个测试框架。

Code Coverage:代码覆盖率统计

脚本编辑器

JetBrains Rider Editor

Visual Studio Editor

Visual Studio Code Editor

编辑器

Unity是一系列子编辑器的集合,所谓游戏引擎就是游戏开发工具集合,而游戏开发工具就是地形编辑、场景编辑、UI编辑、时间线编辑、shader编辑、动画编辑。

PolyBrush:MeshPaiting、Sculpting,集合绘制工具

Timeline:时间线工具。

UI Builder:拖拽形式构建UI

Terrain Tools:地形工具

ShaderGraph:可以无需编程编辑shader。

ProBuilder:构建、编辑特定几何形状。

Scriptable BuildPipeline:将AssetBundle移动到C#中进行。

Cinemachine:专业的摄像机

  • VisualEffectGraph:视觉效果图,可以用于编辑视觉效果。

  • PostProcessing:有一系列特效和图片过滤器,可以直接用在摄像机上,来提升游戏视觉效果。

  • Animation Rigging:Animation Rigging工具箱。

渲染管线RP

  • Core RP

  • HighDefinition RP

  • UniversalRP

DOTS

HavokPhysics for Unity,用于DOTS系统的物理引擎。

Jobs

Burst:将IL字节码转成高度优化的二进制码,基于LLVM。

其它

InputSystem

Kinematica:下一代动画库

Mathematics:提供类似shader数学函数的语法。

AI navigation

ML Agents:基于机器学习的角色行为

Addressables:资源管理

MobileNotification:在Android、IOS上发送通知。

TextMeshPro:文字库。

VersionControl:版本管理,使用Plastic SCM。

Unity UI:像GameObject一样的UI。

Newtonsoft.JSON因为使用过于广泛,从Unity2020开始,已经成为Unity的标准JSON库了。

Unity不仅仅局限于做一个游戏引擎,它还想做游戏服务。

Unity不仅想做游戏服务,还想做代码仓库、持续集成等开发阶段的事情。

Unity服务包含的内容:

  1. 开发

    1. 代码仓库

    2. 持续集成:Cloud Build

  2. 游戏内容相关

    1. 多人游戏
  3. 广告和变现

    1. IAP

    2. 广告

    3. 应用运营分析

  4. 分发

    1. UDP:UnityDistributionPlatform,通过单个中心将应用分发到多个应用商店。
  • 广告

  • Ayalytics:数据分析

  • Cloud Build:云构建,用于持续集成,可以更快速的编译。这是一种

  • MultiPlayer:多人游戏

  • IAP

https://docs.unity.cn/cn/current/Manual/UnityServices.html

以下为一个视频教程的目录,从中可以了解Unity的内容。

本视频教程将详细讨论使用Unity进行游戏开发的各个功能模块,每章实践都从零开始,逐步使用讲解的技术完成一个demo。学员按照视频课程进行学习并练习,能够掌握使用Unity进行单机或者联网游戏开发的必要技术。在讲解Unity各功能模块的同时,还将介绍相关的技术原理,使学员能够知其所以然,增强技术运用能力。

1. Unity引擎基础

主要讲解Unity的基本操作,比如Unity编辑器基本使用方法、地形系统,和必要的C#脚本基础。
第1节: Unity游戏编辑器

第2节: 地形系统

第3节: 脚本系统

2. 动画系统

主要讲解Unity中使用的场景动画和角色动画,重点是角色动画,会详细探讨骨骼动画基础、骨骼动画中的curve、骨骼动画中的layer、逆向运动学IK、动画状态机,和动画融合等动画相关技术。
第1节: 场景动画

第2节: 骨骼动画基础

第3节: 曲线

第4节: 动画层

第5节: 逆向运动学-注视动画

第6节: 逆向运动学-末端节点动画

第7节: 子状态

第8节: 混合树

3. 图形渲染

主要讲解如何实现局部和全局光影效果,会详细讨论全局光照基础、材质及其相关概念、摄像机及剔除优化、后处理效果、光照和反射探针、视频播放,以及粒子系统。

第1节: 全局光照明系统

第2节: 材质

第3节: 摄像机设置

第4节: 后处理效果

第5节: 探针

第6节: 视频播放

第7节: 粒子系统

4. 图形用户界面

主要介绍Unity中的图形用户界面,及如何使用脚本进行交互

5. 物理系统

介绍如何在游戏中使用物理效果,使用游戏中常见的子弹,来讲解Unity中的物理仿真基础,还会讨论到如何进行关节模拟,以及使用脚本处理碰撞事件。

第1节: 物理仿真基础

第2节: 物理仿真的子弹

第3节: 关节结构

第4节: 碰撞事件

6. 人工智能

主要介绍以自动寻路为代表的人工智能实现,会通过一个demo来详细介绍如何在游戏中实现自动巡逻、NPC的视野功能,以及攻击和追踪。

第1节: 自动寻路

第2节: 敌人巡逻

第3节: 敌人视野

第4节: 追踪和攻击

7. 音频

讨论如何在游戏中使用音效,详细介绍音源和接收器组件,如何使用混音器,如何应用声效。
第1节: 音频基础

第2节: 混音器

第3节: 声音特效

8. 联网

介绍如何在Unity中实现简单的联网功能,通过demo讲解多玩家同时在线、游戏物体和角色的网络同步。
第1节: 双玩家连线

第2节: 联网的子弹

第3节: 联网的NPC

9. 时间轴

主要讨论时间轴和动画的异同,如何使用时间轴功能来制作引擎动画,并讨论使用脚本控制时间轴的方法,最后还将介绍时间轴和Cinemachine结合使用的基本方法。
第1节: 和动画的异同

第2节: 简单场景

第3节: 角色动画

第4节: 脚本控制

第5节: Cinemachine

10. 二维游戏开发

主要讨论精灵技术、瓦片地图和二维角色控制方法。
第1节: 精灵

第2节: 瓦片地图

第3节: 二维角色

Unity做的相当一般,很多模块都是残次品,有点抛砖引玉的感觉。

Json

Unity自带的JSON基本上不可用,对于public readonly字段无法序列化出来,对于数组类型也无法序列化。

UGUI

GUI界面简单,只能用来做简单的游戏。

对象树许多关键函数设计不合理

很多常见的函数是缺失的。

  • 从子物体中寻找物体
  • 从inactive物体中寻找物体

so

如果so中导致崩溃,unity editor会直接崩溃。
原因是unity的editor进程与运行游戏的进程是同一个进程,游戏的卡顿会导致editor卡顿。

工程的路径

unity对于包含中文的路径会报错,不能把unity项目放在包含中文的路径中。

Unity的Collider没有圆柱体的collider

什么是unity?

unity是一个游戏框架,它的功能非常丰富,相比其它框架的优势在于对于3d游戏的支持比较完善。
什么是游戏框架?游戏框架就是把游戏常用到的代码抽象成通用的代码,避免任何游戏都从头开发。例如提供物理、碰撞检测、3d渲染等功能。
游戏首先就有物体,有了物体,可以给物体挂载一堆脚本,从而控制物体的行为。物体是游戏中的核心东西。

学习资料

脚本系统

unity支持 csharp,js,boo三种语言,然而csharp是支持最好的,js和boo逐渐被淘汰。boo其实就是python的变形。
unity为什么要支持csharp而不是java、python?

csharp

官方教程:https://docs.microsoft.com/zh-cn/dotnet/csharp/

unity的IDE

  • rider:强烈推荐,jetbrain出品,必属精品。
  • visual studio:mac下不推荐,相比windows上的visual studio属于阉割版。
  • visual studio code:不推荐

unity入口

从一些根类、根包出发学习unity。

尝试use UnityEngine.,查看有哪些子包

  • UnityEngine
  • this
  • GameObject

unity有许多场景,一个场景包含多个GameObject,它们之间组成了对象树。每个GameObject有若干个Component。

GameObject和Component是Unity的核心哲学。Camera、Transform等都可以看做是一种Component,其中Transform这个Component是必备的。 如果想用纯代码实现一个unity程序,只需要创建一个空的GameObject,然后给这个GameObject挂一个脚本即可。 一切皆资源。GameObject就像一个衣服架子,上面可以挂一些Component。Component代表功能、代表特效。

unity界面重要入口:

  • File/BuildSettings:构建设置,设置目标平台
  • Edit/ProjectSettings:整个项目的设置,重要入口PlayerSettings
  • Window/Package Manager:整个项目的包管理
  • Unity/Preference:整个Unity的设置

Unity脚本API四大入口

  • UnityEngine:引擎相关
    • Accessibility:可访问性
    • AI:AI算法
    • Analytics:分析
    • Android:安卓相关
    • Animations:动画相关
    • Apple:苹果手机相关
    • Assertions:断言相关
    • Audio:语音
    • CrashReportHandler:系统崩溃报告
    • Diagnostics:诊断
    • Events:时间
    • Experimental:实现模块
    • iOS
  • UnityEditor:Unity编辑器相关
  • Unity:性能、渲染、Jobs,一些工具类
  • Other

unity架构的特点

  • EntityComponentSystem:ECS架构,GameObject是实体,实体包含多个Component。
  • C# Job System
  • Burst Compiler
  • shader解决方案:shaderLab+HLSL。 合称为DataOrientedTechStack(DOTS)。

2d和3d

图像在2d模式下是sprite,在3d模式下是纹理,texture。 3d的分类:

  • 透视3d:摄像机是一个点,是一种第一人称风格。
  • 正交3d:摄像机是一个面,有时候也成为鸟瞰图,2.5d,是一种第三人称风格。

基本概念

MeshFilter:定义了3d物体的形状,要想给一个GameObject定义形状,只需要给它添加一个MeshFilter。
MeshRenderer:定义了MeshFilter确定的形状的外观。
材质:Materials,材质把可视化表面组合起来,例如Textures、Color tints,Shaders等。使用材质可以定义如何渲染物体的表面。
Prefabs:预制体,预制体相当于Class,可以创造出很多个对象。
游戏由一个一个的场景组成,场景有若干个游戏物体组成,游戏物体有许多分量,每个分量都是代表了一类功能,分量就是Component,一个GameObject可以持有多个Component。脚本也是Component。

unity安装历史版本

在unity hub中打开install页面,通常只会显示大版本的最新版,要想安装历史版本,需要去网页中点击使用unityhub下载。
中文版通常小版本有6位,建议使用英文版。
中文版:https://unity.cn/releases/full/2019 英文版:https://unity3d.com/cn/get-unity/download/archive

Unity为什么采用在Update里面主动获取事件而不是像GUI一样通过事件触发回调?

Unity把输入看做一个存储,当开发者需要某个事件的时候需要手动去读取这个输入。
GUI把输入看做一个触发源。

GUI是静态的,刷新频率很低,当用户没有操作的时候,界面就不需要刷新。
游戏是动态的,刷新频率很高,当用户没有操作的时候,对于大多数游戏来说依旧需要不停地更新UI。所以游戏一定需要Update()方法。
GUI的输入相对简单,并且大部分都是离散信号,例如鼠标点击了某个按钮,键盘敲击了某个字符等。
游戏的输入是连续的,例如在XR游戏中需要不停地获取手柄的位置信息、角度信息,这些信息是始终存在输入的,如果使用事件回调的机制就会导致不停地回调。

Unity编译优化

  1. 如果有多个平台,可以使用Assets软链接的方式,让多个项目共享一套代码,避免频繁切换平台导致的效率低下
  2. 使用mono+32位能够显著提升打包效率
  3. 拆分程序集。使用动态链接库,把脚本分成几个不同的assemblies,分别构建成dll
  4. 使用更高配置的电脑,CPU好、内存大对于编译速度的提升非常明显,例如使用固态硬盘

Unity的文档系统

  • Manual:包括两部分,核心手册和模块手册,例如Input System,XR Interaction Toolkit等是两个重要的
  • Scripting API

Unity的内置模块:

  • Android JNI
  • Animation
  • Asset Bundle
  • Audio
  • Cloth
  • Director
  • Image Conversion
  • IMGUI
  • JSONSerialize
  • Particle System
  • Physics
  • Physics 2D
  • Screen Capture
  • subsystems
  • Terrain
  • Terrain Physics
  • TileMap
  • UI
  • UIElements
  • Umbra
  • Unity Anylytics
  • Unity Web Request
  • Unity Web Request Asset Bundle
  • Unity Web Request Audio
  • Unity Web Request Texture
  • Unity Web Request WWW
  • Vihicles
  • Video
  • VR
  • Wind
  • XR

课程资料

GAMES101-现代计算机图形学入门-闫令琪: https://www.bilibili.com/video/BV1X7411F744?p=1&vd_source=3057378174726ae1406e5bbc9f83c3b2

GAMES202-高质量实时渲染 https://www.bilibili.com/video/BV1YK4y1T7yY/?spm_id_from=333.788.recommend_more_video.0&vd_source=3057378174726ae1406e5bbc9f83c3b2

GAMES203: 三维重建和理解 https://www.bilibili.com/video/BV1pw411d7aS/?spm_id_from=333.788.recommend_more_video.0&vd_source=3057378174726ae1406e5bbc9f83c3b2

计算机图形学基础 https://www.bilibili.com/video/BV1rL411x7KC/?spm_id_from=333.788.recommend_more_video.5&vd_source=3057378174726ae1406e5bbc9f83c3b2

一个知乎大佬: https://www.zhihu.com/people/lxyhpp/posts

Image和RawImage的区别

RawImage核心代码比Image少很多,Raw Image不支持交互,可用于显示任何图片而不仅仅是Sprite,一般用在背景、图标上,支持UV Rect(用来设置只显示图片的某一部分),而Image不支持UV Rect

unity3d打印日志

Debug.Log("This is debug");
//也可以直接使用print
print("hello world");

场景管理器

using UnityEngine.SceneManagement;

SceneManager.LoadScene("2048");
SceneManager.GetAllScenes();
SceneManager.GetSceneByName();
SceneManager.GetSceneByPath();
SceneManager.MoveGameObjectToScene();
Debug.Log(SceneManager.getscene)

unity的so的易错点

  • 如果so中出现segment fault等崩溃性错误,则unity整个都会崩溃。
  • unity editor在打开的整个过程中,so只会加载一次。因此如果so发生变化,只能重启unity editor。

动态加载资源的方式?

  1. Resources.Load();
  2. AssetBundle:Unity5.1版本后可以选择使用Git: https://github.com/applexiaohao/LOAssetFramework.git

使用text存储日志存在的问题

如果使用UI中的text存储日志并展示日志,存在一个巨大的问题。当text长度过长之后,unity就会报错,说渲染的数据量过大。

让一个MonoBehavior汇集多个MonoBehavior的功能

[RequireComponent (typeof(XXXX))] 其中XXXX为依赖的脚本,或者Unity组件(理论上都算作脚本),这样,当你挂这个脚本时,XXXX脚本也被挂上去了

activeSelf、activeInHierarchy

  • activeSelf:只读字段,物体本身的active状态,对应它在inspector中的checkbox是否被勾选。
  • activeInHierarchy:只读字段,物体在层次中是否是active的,也就是说只有当这个物体及其祖先物体都是active时这个值才为true。
    一个物体如果是activeSelf状态,那么这个物体不一定可见,因为它的父物体可能是inactive状态。在Unity中,一个物体要想处于可见状态,activeHierachy必须为true。

改变物体active状态的两个函数:

  • SetActive():设置物体自身的activeSelf状态。
  • SetActiveRecursively()(obsolete):改变物体及其所有子物体的active状态。

使当前物体朝向目标物体,修改目标物体的朝向

Vector3 dir = m_chooseTarget.transform.position - transform.position;
dir.y = 0;
transform.forward = dir.normalized;

取消关联prefab

选中GameObject,右键菜单选择prefab/unpack prefab completely.
如果A prefab里面包含着B prefab,则需要编辑A prefab,把B的prefab去掉。

2d的spriteRenderer,3d的meshRenderer

SpriteRenderer是干什么的?简单来说就是一个renderer,这个renderer的输入是一堆资源文件(例如图片、动画等),输出就是一个二维图像。
unity中以GameObject作为基本对象,因此渲染的时候渲染顺序就很重要,它直接决定了物体的可见性。

unity中与sprite有关的组件

  • 提供占位的SpriteCreator
  • 内置的SpriteEditor
  • SpriteRenderer

Unity的最佳实践

使用容器盛装的时候,尽量使用具体的MonoBehaviour类而不是GameObject,因为GameObject本质上就相当于是一种void*,它里面包含什么东西是不太确定的。

游戏框架

GameObject,把游戏中的很多东西看做是一个物体。 过去我编写的小游戏,没有GameObject+不停渲染的过程,大部分是基于事件进行刷新的过程。
GUI框架与游戏框架的最重要的区别就在于,GUI框架基本上都是需要用户操作来触发UI的变化,而游戏框架则包含一个游戏主循环,即便用户不执行任何操作,游戏状态也在不停地改变。

Editor

要查看 Editor 日志,请在 Unity 的 Console 窗口中选择 Open Editor Log。

操作系统 日志文件

macOS	~/Library/Logs/Unity/Editor.log
Windows	C:\Users\username\AppData\Local\Unity\Editor\Editor.log

Unity的Editor跟正在播放的游戏是同一个线程,游戏崩了,可能需要查看日志才能看出来。

SpriteRenderer中的DrawMode

SpriteRenderer是精灵渲染器,一个精灵渲染器接受一个精灵对象。一个精灵对象其实就是一个图片。
SpriteRenderer决定了如何渲染一个精灵。
DrawMode有三个取值:

  • simple:直接把精灵画上去
  • sliced:把精灵的一部分画上去
  • tiled:把精灵重复地画出来

Texture2d

Sprite是Texture2d的一种,精灵是Texture2d的一种特殊情况。

Material

默认材质是不可编辑的。
材质可以指定一个shader,unlit/texture是最常用的一种材质shader。
用户自己创建的材质有用可编辑的属性。

材质是一个全局对象,所以默认材质是不可编辑的。如果把同一个材质赋给两个不同的GameObject,在编辑器中,当其中一个的材质的Offset发生变化的时候,另一个GameObject的材质的Offset也会发生变化;在代码中,当其中一个材质的Offset发生变化的时候,另一个GameObject的材质的Offset并不会发生变化,这表明运行时每个GameObject持有的材质是不相同的。

Material不是Component,GameObject无法直接持有一个Material,GameObject其实并不直接拥有Material,而是拥有SpriteRenderer,SpriteRenderer又拥有Material。在Unity中,GameObject的Inspector面板之所以展示Material,是unity为了方便直接编辑Material。获取材质的时候:material = GetComponent<Renderer>().material;

MainTexture是最常用的Texture,为此unity提供了直接访问mainTexture的一些属性。

    private static readonly int MainTex = Shader.PropertyToID("_MainTex");

    void Update()
    {
        //直接使用mainTextureXXX属性
        var m = GetComponent<SpriteRenderer>().material;
        var p = m.mainTextureOffset;
        p.x += v * Time.deltaTime;
        m.mainTextureOffset = p;
    }

    void update1()
    {
        //获取Texture然后更新属性
        var m = GetComponent<SpriteRenderer>().material;
        var p = m.GetTextureOffset(MainTex);
        p.x += v * Time.deltaTime;
        m.SetTextureOffset(MainTex, p);
    }

三角面

三维物体的渲染依赖3D建模,3D建模就是构建三角面片。
为什么使用三角面片而不是四边形? 因为三角形是二维空间中最简单的封闭形状,具备足够高的灵活性,任意一个四边形面片都可以用两个三角形面片来表示。 移动端GPU算力较低,通常面数低于10w面。
与游戏性能相关的参数:

  • 帧率
  • DrawCall
  • 面数

Unity设置程序在后台始终运行

默认情况下,Unity程序在失去焦点之后渲染随之停止。 设置Application在背景运行,避免UI停止之后不在运行 Application.runInBackground=true

UnityEngine的Object.cs

Object.CurrentthreadIsMainThread():当前线程是否是主线程 DontDestroyOnLoad:在切换场景的时候不要销毁对象。

Unity中有两个最关键的类:MonoBehavior和GameObject,它们共同继承了Object这个类。 MonoBehavior=>Behavior=>Component=>Object GameObject=>Object

GUI

Unity的UI系统

Unity官方支持三套UI系统。

  • UI Toolkit:也叫UIElements,是Unity最新的UI系统,Unity有意将其作为默认的UI系统,但是目前不够完善。和web、android、javafx、WPF等技术一样,它使用XML定义UI树,使用CSS定义样式。
  • uGUI:是一个较旧的,使用GameObject的UI系统。它最大的优势就是支持拖拽式创建UI界面、支持3D,像处理普通的GameObject那样处理UI元素。
  • IMGUI:Instant Mode GUI,是一个代码驱动的UI工具包。IMGUI就是著名的data=GUI(data) 这种模式的API,在Unity中用于编写Inspector和EditorWindow。

两种UI模式:

  • 运行时UI,用于游戏内
  • GUI扩展UI,用于编写Editor扩展

uGUI只能用于游戏内,IMGUI只能用于编辑器扩展,而UI Toolkit统一了Unity的UI,既能用于游戏内又能用于GUI扩展。
Unity中经常存在一种东西,多种实现方案。例如UI系统包括UI toolkit、uGUI、IMGUI等。输入系统包括InputSystem和InputManager。

在使用UnityEditor做工具的时候选择哪种UI?

首先,可选项只有IMGUI和UI Toolkit这两种方案,uGUI只能在运行时使用。
IMGUI的优势在于只需要考虑第一次渲染的时候的界面设计,之后每次更改都会触发重新渲染。这种反复重绘的方式优点就是响应灵活,缺点是性能比较差。然而,性能差这个缺点实在算不上什么,因为在Unity Editor里面UI通常很小,这种性能损耗算不上什么。许多新兴的UI库都是这种模式,例如react、flutter等,都是基于重绘实现的。基于组件实现的缺点就是不灵活。
IMGUI的响应可以做到更及时。例如一个配置,可以在两个窗口内都能进行修改。如果同时打开两个窗口,期望是在一个窗口中的改动可以很快在另一个窗口中看到变化。如果使用UI Toolkit,就需要处理OnFocus之类的事件,在这些事件里面对控件进行手动修改。
再比如,一个窗口里面有一个中英文切换按钮,点击中文之后所有控件的文本变成中文,点击英文之后,所有空间的文本变成英文。如果使用IMGUI,轻而易举就能实现;如果使用UI Toolkit,则需要手动处理每个空间的显示。界面上有多少个控件,点击按钮的时候就需要操作多少个控件,代码写起来复杂度较高。
另外,IMGUI存在了很长时间,相对成熟一些,UI Toolkit的PropertyField对于列表类型的数据展示较为原始,没有排序、没有添加和删除按钮,只能更改列表长度。

那么UI Toolkit有什么优点呢?style控制相对灵活,可以为每个元素精准设置样式,对于熟悉CSS的人非常友好。

IMGUI是函数式编程,UI Toolkit是面向过程编程。函数式编程操作值,面向过程编程操作对象。

一言以蔽之,IMGUI写起来爽,用起来慢;UI Toolkit看上去清晰,写起来麻烦,用起来快。
如果做的UI需要多次渲染,那么使用IMGUI;如果做的UI只需要静态展示,使用UI Toolkit。

UGUI和NGUI的区别

UGUI是unity官方推出的UI库,NGUI是社区实现的改进版的UI,经过一段时间发展,UGUI基本上超越了NGUI。以后不需要再学NGUI了。

1.UGUI界面展示是在画布下(Canvas),而NGUI是在UIRoot下

2.UGUI继承RectTransform,RectTransform继承Transform,而Ngui直接继承Transform

3.UGUI没有图集Atlas,是直接使用图片,而Ngui需要使用图集,对图集进行管理和维护

4.UGUI有锚点,可以自动适配屏幕,NGUI没有暂未发现此功能

5.UGUI中Btn需要有sprite,button,而NGUI只需要一个UIButton方法,和一个BoxCollider。

6.NGUI基于C#编写的,会产出比较多的GC,UGUI是基于C++,性能比较好。基于canvas渲染比较好。

unity富文本TMP

在window/textMeshPro中通过import可以导入富文本的资源和实例。 TMP:text mesh pro,是高级版的富文本框。可以灵活地控制字体。 它在ProjectSettings中都有专门的TextMeshPro部分进行设置,在Window菜单中也有专门的入口,因此TMP是比较重要的功能。

img_5.png

VisualElement注册回调

public void RegisterCallback<TEventType>(
  EventCallback<TEventType> callback,
  TrickleDown useTrickleDown = TrickleDown.NoTrickleDown)
  where TEventType : EventBase<TEventType>, new()
  
public void RegisterCallback<TEventType, TUserArgsType>(
  EventCallback<TEventType, TUserArgsType> callback,
  TUserArgsType userArgs,
  TrickleDown useTrickleDown = TrickleDown.NoTrickleDown)
  where TEventType : EventBase<TEventType>, new()

TEventType是一种缩写,它表示EventBase<TEventType>TEventType : EventBase<TEventType>

自定义事件

    private void CreateGUI()
    {
        var l = new Label("one");
        l.RegisterCallback<MyEvent>(e => { });
        rootVisualElement.Add(l);
    }

    class MyEvent : EventBase<MyEvent>
    {
    }

在一个线程里面执行任务,任务执行结束之后,把事件返回给UI线程

核心在于使用SynchronizationContext mainThreadSyncContext;把主线程的上下文保存下来,然后执行post。
通过Thread.CurrentThread.ManagedThreadId查看线程ID,发现确实是符合预期。

        public class ValueCallback
        {
            private Action<string> act;
            public string data;
            SynchronizationContext mainThreadSyncContext;

            public ValueCallback()
            {
                mainThreadSyncContext = SynchronizationContext.Current;
            }

            public void OnValue(Action<string> act)
            {
                if (data != null)
                {
                    act.Invoke(data);
                    return;
                }

                this.act = act;
            }

            public void Call(string value)
            {
                this.data = value; //设置data供以后可能会用到
                if (this.act != null)
                {
                    //如果在主线程中更新样式和
                    mainThreadSyncContext.Post(_ =>
                    {
                        Debug.Log($"=========POST内部:{Thread.CurrentThread.ManagedThreadId} {value}");
                        this.act.Invoke(value);
                    }, null);
                }
            }
        }

runInUIThread

        static SynchronizationContext mainThreadSyncContext;

        static void runInUIThread(Action act)
        {
            mainThreadSyncContext.Post(_ => { act(); }, null);
        }

C#中的线程池

            ThreadPool.QueueUserWorkItem((o) =>
            {
                var p = Process.Start(file, arguments);
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.StandardOutputEncoding = Encoding.UTF8;
                p.StartInfo.RedirectStandardOutput = true;
                var res = p.Start();
                p.WaitForExit();
                var content = p.StandardOutput.ReadToEnd();
                callback.Invoke(content);
            });

自定义事件

    private void CreateGUI()
    {
        var value = 1;
        var l = new Label("one");
        l.RegisterCallback<MyEvent>(e => { l.text = $"{value}"; });
        rootVisualElement.Add(l);
        var b = new Button(() => { l.SendEvent(new MyEvent()); });
        b.text = "点我加一";
        rootVisualElement.Add(l);
        rootVisualElement.Add(b);
    }

    class MyEvent : EventBase<MyEvent>
    {
    }

如何快速学习新版UI?

要想使用UI Toolkit,需要使用UnityEditor2022。
查看菜单windows/UI Toolkit/Samples,里面带着一些样例。这些样例主要学习UXML的写法。

如何快速创建一个使用新版UI的EditorWindow?

右键create,选择UIElements/EditorWindow,就会弹窗提示创建三个文件。

UXML概览

在生成的脚本中,可以发现UXML文件是动态加载并渲染的。


public class one : EditorWindow
{
    [MenuItem("Window/UIElements/one")]
    public static void ShowExample()
    {
        one wnd = GetWindow<one>();
        wnd.titleContent = new GUIContent("one");
    }

    public void CreateGUI()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;

        // VisualElements objects can contain other VisualElement following a tree hierarchy.
        VisualElement label = new Label("Hello World! From C#");
        root.Add(label);

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scenes/新版UI/Editor/one.uxml");
        VisualElement labelFromUXML = visualTree.CloneTree();
        root.Add(labelFromUXML);

        // A stylesheet can be added to a VisualElement.
        // The style will be applied to the VisualElement and all of its children.
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Scenes/新版UI/Editor/one.uss");
        VisualElement labelWithStyle = new Label("Hello World! With Style");
        labelWithStyle.styleSheets.Add(styleSheet);
        root.Add(labelWithStyle);
    }
}

如何在runtime UI中使用UXML?

使用UXML有两种方式:在editor UI中、在runtime UI中。
在editor中,使用EditorWindow的rootVisualElement,添加UXML资源。
在runtime中,添加UIDocument类型的元素,然后为UI Document类型的元素设置UXML文件。

label+text

var label = new Label("App ID");
var input = new TextField("App ID");
input.value = Gs.appId;
input.RegisterValueChangedCallback(e => { Gs.appId = e.newValue; });
root.Add(label);
root.Add(input);

Label就可以省略了,可以简写为

var input = new TextField("App ID");
input.value = Gs.appId;
input.RegisterValueChangedCallback(e => { Gs.appId = e.newValue; });
root.Add(input);

Unity的脚本分为两类:运行时和Editor中。写脚本的时候一定要区分好脚本的位置,在运行时调用Editor脚本肯定会出错,在Editor中调用运行时脚本也会出错。

Unity的Editor相关的脚本都位于UnityEditor命名空间,与构建相关的脚本位于UnityEditor.Build命名空间。

Unity Editor的构建过程中的回调基本上都位于UnityEditor.Build这个命名空间中,每一个回调是一个接口,每一个接口只包含一个回调函数。编辑器会扫描所有实现了这些接口的类,在执行的过程中调用这些回调。由此引入一个问题:如果某个回调接口有多个实现,它们的调用顺序需要怎么控制呢?

实际上,所有的回调接口都继承自IOrderedCallback,IOrderedCallback有一个callbackOrder属性,这个属性决定了执行顺序。

https://docs.unity3d.com/ScriptReference/Build.IOrderedCallback.html

IProcessBuildWithReport在开始构建之前执行的回调。IProcessBuildWithReport继承自接口IOrderedCallback。

IProcessBuildWithReport有一个OnPreprocessBuild函数,在开始构建之前执行这个回调。
IActiveBuildTargetChanged构建目标发生改变的时候执行此回调。
IFilterBuildAssemblies忽略不需要打包assemblies
Ill2CppProcessor已经删除,在构建IL2cpp之前执行这个函数
IPostBuildPlayerScriptDLLs在编译脚本结束之后执行此回调
IPostprocessBuild已废弃,推荐使用IPostProcessBuildWithReport,这个回调没有report参数,信息太少,因此被废弃。
IPreprocessBuild已废弃,推荐使用IPreprocessBuildWithReport
IProcessSceneWithReport在构建每一个场景的时候都会执行此回调。
IProcessScene已废弃,推荐使用IProcessSceneWithReport。

从上述接口中,可以看出很多接口已经废弃了。原因是它们参数太少,改用一个WithReport函数替代。

IPostGenerateGradleAndroidProject:生成gralde项目之后的回调

虽然与构建相关的回调都位于UnityEditor.Build目录下,但是与平台相关的、又与构建相关的脚本位于平台命名空间下。

UnityEditor.Android包下唯一的接口,在Editor目录下所有实现这个接口的类都会执行OnPostGenerateGradleAndroidProject函数。

趁此机会,了解一下UnityEditor.Android包下的内容。这个包是一个非常简单的包,代码量非常小。

两个类:

  • AndroidExternalToolSettings:对应PlayerSettings里面的一些配置。

    • gradlePath:gradle可执行程序的路径。

    • jdkRootPath:jdk的路径

    • keystoreDedicatedLocation

    • maxJvmHeapSize:用于构建Android应用的Jvm堆大小

    • ndkRootPath、sdkRootPath:Android SDK、NDK的路径

  • AndroidPlatformIconKind:是一个类,包含一些静态属性。UnityEditor.PlatformIconKind是一个接口,它的实现包括AndroidPlatformIconKind,iOSPlatformIconKind。

    • Adaptive:自适应

    • Legacy:遗留的

    • Round:圆角的

XRBuildHelper:专为XR实现的生命周期回调

UnityEditor.XR.Management是一个专门的包,用于管理XR开发相关的任务。

XRBuildHelper位于UnityEditor.XR.Management命名空间下,用于管理编译中的回调。

XRBuildHelper实现了IPreprocessBuildWithReport和IPostprocessBuildWithReport两个接口。

public abstract class XRBuildHelper<T> : IPreprocessBuildWithReport, IPostprocessBuildWithReport where T : Object

https://docs.unity3d.com/cn/2020.3/Manual/editor-EditorWindows.html

ScriptableObject

使用ScriptableObject可以把对象以资源的形式进行存储。

Unity编辑器扩展

三个重要的类:MenuItem、EditorWindow、ScriptableWizard。

MenuItem(string itemName,bool isValidateFunction,int priority)

我知道我们通常使用MenuItem常常使用的是它的第一个参数,即定义一个菜单项的名称,我们可以使用”/”这样的分隔符来表示菜单的层级,MenuItem需要配合一个静态方法来使用,可以理解为当我们点击当前定义的菜单后就会去执行静态方法中的代码,因此MenuItem常常可以帮助我们做些编辑器扩展开发的工作。

unity中有两个一模一样的菜单项

先导入谁就是谁生效

[UnityEditor.MenuItem("Pico/Platform/Edit Settings")]
public static void Edit()
{
    Debug.Log("Two");
    // UnityEditor.Selection.activeObject = PlatformSettings.Instance;
}
[UnityEditor.MenuItem("Pico/Platform/Edit Settings")]
public static void EditHaha()
{
    Debug.Log("One");
    // UnityEditor.Selection.activeObject = PlatformSettings.Instance;
}

HideInInspector

在变量上使用这个属性,可以让public的变量在Inspector上隐藏,也就是无法在Editor中进行编辑。

Header

public class ExampleClass : MonoBehaviour {
    [Header("生命值")]
    public int CurrentHP = 0;
    public int MaxHP = 100;

    [Header("魔法值")]
    public int CurrentMP = 0;
    public int MaxMP = 0;
}

RangeAttribute

在int或者float类型上使用,限制输入值的范围

public class TestRange : MonoBehaviour
{
    [Range(0, 100)] public int HP;
}

TextAreaAttribute

该属性可以把string在Inspector上的编辑区变成一个TextArea。 例子:

public class TestTextAreaAttributeByLvmingbei : MonoBehaviour {
    [TextArea]
    public string mText;
}

TooltipAttribute

这个属性可以为变量上生成一条tip,当鼠标指针移动到Inspector上时候显示。

public class TestTooltipAttributeByLvmingbei : MonoBehaviour {
    [Tooltip("This year is 2015!")]
    public int year = 0;
}

入口类

  • EditorUserBuildSettings
  • PlayerSettings

在Inspector中隐藏属性

Inspector中会自动显示所有的public属性,要想隐藏某个属性,则使用hideInInspector注解。

[HideInInspector]
在inspector中隐藏属性。

editor里面每帧刷新


            EditorApplication.update += () => { Debug.LogError("updating"); };

imGUI的frameBox

GUIStyle style = "frameBox";
style.fixedWidth = frameWidth;
EditorGUILayout.BeginVertical(style);

imGUI的启用禁用

IMGUI是一种函数式语法,设置全局变量,然后调用Button函数即可。

GUI.enabled = hasSomethingToFix;
if (GUILayout.Button(strApplyButtonText[(int) language], GUILayout.Width(130)))
{
    this.ApplyRecommendConfig();
}

GUI.enabled = true;

toggle如何设置labelWidth

float originalValue = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = 250;
field.value = EditorGUILayout.Toggle(field.value);
EditorGUIUtility.labelWidth = originalValue;

Unity的扩展编辑器支持三种形式:

  • 编辑器窗口:EditorWindow,弹出窗口
  • 属性绘制器:PropertyDrawer,改写OnGUI属性。
  • 自定义编辑器:Editor,改写一个类的Inspector界面。

自定义编辑器

主要函数OnInspectorGUI,这个函数会在每次改动的时候进行重新调用。

在InspectorGUI中,使用Time.frameCount可以发现,只有在发生UI交互的时候才会进行重绘。

使用CustomEditor(typeof(XXX))可以指定某一个对象的编辑器。当Selection.activeObjectXXX类型的对象的时候,Unity就会调用CustomEditor所注解类的OnInspectorGUI()绘制UI界面。

[CustomEditor(typeof(PlatformSdkConfig))]
public class PlatformSdkConfigEditor : UnityEditor.Editor
{

如果不改写OnInspectorGUI()方法,就会显示对象PlatformSdkConfig的成员变量编辑器。

如果改写了OnInspectorGUI(),并且是一个空的函数。则什么都不会显示。 使用base.OnInsectorGUI()可以实现重新显示成员变量编辑器。

Unity的GUI常用范式:IMGUI

Unity的编辑器扩展,目前主要使用IMGUI。IMGUI意思是Instant Mode GUI,立即模式GUI。
核心在于每一帧都会调用OnGUI()、OnInspectorGUI()绘制界面,在绘制界面的同时检测用户是否执行了某个操作,在这个OnGUI()、OnInspector()函数里面同时实现实现UI显示和数据绑定。

OnGUI(){
    data=UI组件(data);
}

UI组件接收数据并进行渲染,然后返回数据。这跟React也有点像。
在OnGUI里面可以随意写for循环和条件语句。

IMGUI的常用类

  • GUI:需要矩形布局
  • GUILayout:自动布局
  • EditorGUILayout:Editor中的自动布局

枚举框

  • EditorGUILayout.EnumPopup:只能单选的枚举下拉框
  • EditorGUILayout.EnumFlagsField:可多选的枚举下拉框

如果使用asset方式存储配置文件,则需要把对象单独定义一个文件

如果使用Unity的方式保存一个ScriptableObject,则这个Object必须放在一个单独的文件里面。原因是Unity的Asset是一个yaml文件,里面记录着一些fileId,必须做到一个文件里面只有一个东西。例如MonoBehavior这种,文件名和类名必须保持一致。

No script asset for PresetConfig. Check that the definition is in a file of the same name and that it compiles properly.

Text最佳实践

设置minSize和maxSize和bestFit。 如果minSize设置太小,当有一行特别长的时候,就会导致字体特别小。

获取系统语言

这个系统语言是操作系统的语言,既可以用于Runtime也可以用于Editor。 Application.systemLanguage

GUI更新不阻塞主UI

EditorApplication.update += Progress;
EditorApplication.update -= Progress;

XR

Unity官方的XR Interaction Toolkit并不是XR Interaction的唯一选项,有许多竞品。
一切interaction库的底层还是InputDevice系列。

常见的Interaction库包括:

  • MRTK
  • OculusInteraction
  • Unity XR Interaction Toolkit
  • VR Interaction Framework
  • Auto Hand
  • Hurricane VR
  • Stereokit

OpenXR资料

openXR 官网:https://www.khronos.org/openxr/

API 规范:Khronos OpenXR Registry - The Khronos Group Inc

Github:OpenXR-SDK-Source/src/loader at main · KhronosGroup/OpenXR-SDK-Source · GitHub

Reference: https://www.khronos.org/files/openxr-10-reference-guide.pdf

3dof和6dof

3dof和6dof描述的是物体的状态有几个自由度,也就是是一个几维的向量。3dof就是3维向量,6dof就是6维向量。 3dof描述的是3个角度,6dof描述的是位置向量和角度向量。 3dof和6dof的检测是一个非常有趣的物理问题。 3dof很早就被攻克了,它用到的是陀螺仪这种传感器。陀螺仪能够感知到每一时刻的旋转速度,在三个方向上设置好陀螺仪就能够获取三个方向上的角速度。通过积分就能够得到当前的欧拉角。 6dof的核心是检测物体的位置,有两种实现方案,一种是把摄像头放在外面,感知物体的变化,这样做的好处是实现简单,坏处是用户用起来会多一个设备。第二种方案是把摄像头与头显放置在一起,摄像头会检测周围环境的变化,通过关键点匹配算法感知物体的位移,这种方式对于环境有一些特殊的要求。

VR成像的原理

VR的特点就是立体感,在图像的基础上多了远近,也就是多了一个z轴。两个眼睛看到的图片不一样,这就是立体感的来源。与其说是立体感,不如说人眼是一种非常容易欺骗的东西。与其说人眼是一种容易欺骗的东西,不如说人的大脑是一个复杂的处理器,立体感来源于大脑。只要对大脑的输入相同,大脑就会产生立体感。 我们看过VR电影,需要带VR眼镜。VR眼镜的原理是就是光的偏振,同一束光穿过左右两个镜片得到的图像会变得不一样。镜片就是偏振片,能够把特定方向的光过滤掉。通过调整光的偏振就能够让两个眼睛接收到不同的图像信息。 现实世界中,人并没有佩戴VR眼镜,为什么照样能够看到立体的世界?因为人的两只眼睛中间有一个距离,这个距离决定了人眼看到的东西不一样。 有两种视差,一种叫做双目视差,就是上面说的。另一种叫做运动视差,由于物体运动导致投在视网膜上的像不一致。运动视差会导致只能够看到运动的物体到眼睛的距离。 既然说双目视觉差异带来了立体感,那么人如果只有一只眼睛,还有立体感吗?答案是:可能有。立体感这个问题实际上是一门综合学科,涉及到神经学、心理学等。失去一只眼睛的人生活一段时间之后凭借生活经验也能够产生立体感,人的大脑具备较强的可塑性。 在CV领域中有一个方向就是根据一个二维图片建模三维图像,这就是模拟“独眼龙”产生立体感的算法。

色散和反色散

VR眼睛那么小,我们却能够看到很大的世界,原因就是里面用到了透镜。透镜就像显微镜一样把投屏到微小空间中的图像放大。图像有RGB三种颜色,每种颜色的光频率不同,因此同一像素经过透镜之后RGB三种颜色不再位于同一个点上,这就会导致用户看到的图像不真实。 反色散的原理就是把通过透镜前的图像进行校正,从而通过透镜后变得正常。

参考资料

XR Interaction Toolkit

https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@2.1/manual/index.html

概述

Unity输入关键类

InputDevices系列是比较低级的API,XRInteractionToolkit则是对InputDevices的封装,提供了更易用的方式。

命名空间:UnityEngine.XR.Interaction.Toolkit

XRInteractionToolkit的意义

如果直接使用InputDevices写交互式逻辑,就感觉像写汇编语言一样。

XR 交互工具包是一个高级的、基于组件的交互系统,用于创建 VR 和 AR 体验。它提供了一个框架,使 Unity 输入事件中的 3D 和 UI 交互可用。该系统的核心是一组基本的 Interactor 和 Interactable 组件,以及将这两种类型的组件联系在一起的 Interaction Manager。它还包含可用于运动和绘制视觉效果的组件。

XR 交互工具包包含一组支持以下交互任务的组件:

  • 跨平台 XR 控制器输入:Meta Quest (Oculus)、OpenXR、Windows Mixed Reality 等。

  • 基本对象悬停、选择和抓取

  • 通过 XR 控制器进行触觉反馈

  • 视觉反馈(色调/线条渲染)以指示可能的和活跃的交互

  • 与 XR 控制器的基本画布 UI 交互

  • 用于与 XR Origin 交互的实用程序,这是一种用于处理静止和房间规模 VR 体验的 VR 摄像机装置

架构

交互系统的三种状态:悬停hovered、选择select、激活activated。这三种状态同时涉及Interactor和interactable,interactable是被交互对象,interactor是触发交互的对象(可以理解为是手柄)。

hover:射线指向了物体

select:例如grip按下

activated:例如trigger按下

Interactable和Interactor

Interactable和Interactor都会在InteractionManager里面进行注册。

ActionBased和DeviceBased

一些行为,例如Snap Turn Provider,有两种变体:基于动作的行为和基于设备的行为。基于动作的行为使用动作间接读取来自一个或多个控件的输入。基于设备的行为用于[InputDevice.TryGetFeatureValue](https://docs.unity3d.com/ScriptReference/XR.InputDevice.TryGetFeatureValue.html)直接从[InputDevice](https://docs.unity3d.com/ScriptReference/XR.InputDevice.html)行为本身配置的特定控件中读取输入。

ActionBased:基于参考的推荐输入类型行动及其在输入系统中的控制器绑定。

DeviceBased:使用InputDevice

建议您使用基于操作的变体而不是基于设备的变体,以利用输入系统包提供的优势。

使用基于操作的变体就是说绑定输入源的时候使用XRI系列。

使用基于设备的变体就是说绑定输入源的时候直接使用Controller。

版本问题

Pico Integration SDK中XR SDK的Interaction Toolkit版本是0.9-preview版,目前最新的interaction toolkit版本是2.2。所以导入Pico Integration SDK之后,建议升级一下Interaction SDK。

InGameDebugConsole目前似乎不支持新版的InputManager。

在低版本的XR Interaction Toolkit下,有一些组件是不包含的,例如ContinuousMove和ContinuousTurn。

如何升级XR Interaction Toolkit?

在PackageManager里面搜索XR Interaction Toolkit,升级即可,升级完成之后还要安装它提供的一些样例。

升级XR Interaction Toolkit的过程中,注意不要使用新的输入系统(否则会导致很多常用库用不了,现在使用新的输入系统有点早,各个开源库还没有及时跟上)。

包含的组件

https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@2.1/manual/components.html

Controller

手柄相关

XR Controller:包括ActionBased和DeviceBased

XR Controller Recorder

能够把XR控制器的输入转换为事件。

交互

Interactable

为GameObject添加可交互属性

XR Grab Interactable

XR Simple Interactable

AR Interactables

与AR相关的可交互属性

Interactors

AR Gesture Interactor

XR Direct Interactor:直接触摸式交互

XR RayInteractor:射线交互

XR Socket Interactor:

Locomotion

  • CharactorControllerDriver

  • Continuous (Move+Turn) Provider*(Action Based + Device Based)

  • LocomotionSystem

  • Snap Turn Provider*(ActionBased + Device Based)

  • TeleportationProvider

Visuals

  • XR Interactor Line Visual:射线形状

  • XR Interactor Reticle Visual:准星

  • XR Tint Interactable Visual

其它组件

  • InputActionManager

  • XR Device Simulator

  • XR Interaction Manager

  • XR Target Filter

Interactor&Interactable

Interaction Manager

是交互管理器,全局唯一。

Interactable和Interactor之间的交互就是由InteractorManager进行管理的。

在一个场景里面Interactor和Interactable都存在很多个,当Interactor发出事件的时候,这个事件应该交给哪一个Interactable来处理,这就是由Interactor Manager处理的。

InteractionManager所面临的一个算法问题:如何快速地匹配Interactor和Interactable?这看上去跟二分图匹配有点像,其实也是要解决一个连线题。

XRBaseInteractable:一切Interactable的祖先

移动类型MovementType

  • VelocityTracking

  • Kinematic

  • Instantaneous

XRBaseInteractable的成员

  • InteractionManager:全局Manager,如果没有设置Manager,则自动寻找全局Manager;如果全局没找到,则创建一个InteractionManager。

  • colliders:一个Interactable的碰撞器列表,一个Interactable首先应该包含一个collider列表。当手柄与colliders碰撞的时候,才表示交互发生。如果没有给Interactable设置colliders列表,则默认取所有children中的Collider。

  • hoveringInteractors:哪些Interactor在我头上晃悠

  • isHovered:是否有人在我头上晃悠

  • isSelected:当前interactor是否处于选中状态

事件

  • OnFirstHoverEnter

  • OnHoverEnter

  • OnHoverExit

  • OnLastHoverExit

  • OnSelectEnter

  • OnSelectExit

  • OnActivate

  • OnDeactivate

方法:

  • GetDistanceSqrToInteractor(XRBaseInteractor):获取interactor到我的距离(我是interactable),我身上挂着多个collider,取interactor到我的collider的最短距离。

所有的XRBaseInteractable都会往InteractionManager里面注册Interactable,相当于告诉Manager说:我是可交互对象,当光线射到我身上的时候,把事件告诉我。

当XRBaseInteractable析构的时候,会从InteractionManager里面取消注册这个Interactable。

常见的Interactable

GrableInteractable:可被抓握的对象

BaseTeleportationInteractable:可以去往的对象,与位置相关,下面Locomotion系列会对它进行详细解读。

XRBaseInteractor:一切Interactor都继承它

XRBaseInteractor是所有Interactor的基类。

Interactor是什么呢?手柄就应该具有Interactor组件,这样手柄才能够触发Interactable。

3D空间中的交互是基于Interactor、Interactable、InteracionManager三个东西的,3D作为一种新的交互形式,Unity走在了时代的前列。

成员变量

  • InteractionManager:全局的交互管理器,它需要在Manager里面注册Interactor。

  • InteractionLayerMask:LayerMask

  • attachTransform

  • XRBaseInteractable selectTarget:当前选中的对象

  • XRBaseInteractable startingSelectedInteractable :最初选中的interactable,表示在组件Start的时候默认选中的对象

  • AllowHover

  • AllowSelect

事件

  • HoverEnter

  • HoverExit

  • SelectEnter

  • SelectExit

常见的Interactor

  • XRBaseControllerInteractor:基本的手柄Interactor

    • Ray Interactor:可以发射射线的Interactor

    • Direct Interactor:必须接触的Interactor

  • SocketInteractor:?

XR Grab Interactable:可以被抓握的对象

使用GrabInteractable可以实现隔空取物。隔空取物的两个关键类:

  • XR Grab Interactable

  • XR Ray Interactor

XR Simple Interactable:简单交互器

一个空的交互器,啥都没有,仅仅是继承了XRBaseInteractable。

public class XRSimpleInteractable : XRBaseInteractable
{
}

XRBaseControllerInteractor:基本的手柄交互器

手柄交互器主要设置select、hover时候的一些音效、触感等,至于具体怎么样才能触发select、hover,则交给Ray Interactor和DirectInteractor去处理。

SelectActionTriggerType:以什么方式触发Selection

bool HideControllerOnSelect:在选中的时候是否隐藏Controller

音效

bool playAudioClipOnSelectEnter:在选中的时候是否播放音频

AudioClip AudioClipForOnSelectEnter:在选中的时候播放什么音频

bool playAudioClipOnSelectExit:在退出选中的时候是否播放音频

AudioClip AudioClipForOnSelectExit :在退出选中的时候播放什么音频

bool playAudioClipOnHoverEnter:

AudioClip AudioClipForOnHoverEnter

bool playAudioClipOnHoverExit

AudioClip AudioClipForOnHoverExit

触觉

  • 触觉的强度、触觉的时长

  • selectEnter、selectExit、hoverEnter、hoverExit

XR Ray Interactor

光线交互器

XR Direct Interactor

DirectInteractor持有一个Interactable数组,并且记录了每个Interactable到它的距离。

这个Interactable数组表示与这个Interactor正在发生交互的对象。

// reusable list of valid targetsList<XRBaseInteractable> m_ValidTargets = new List<XRBaseInteractable>();// reusable map of interactables to their distance squared from this interactor (used for sort)Dictionary<XRBaseInteractable, float> m_InteractableDistanceSqrMap = new Dictionary<XRBaseInteractable, float>();

XR Direct Interactor的函数主要在维护它的这个Interactable数据库:当triggerEnter时,把Interactable添加进去;当triggerExit时,把Interactable移除掉。

CanHover、CanSelect两个函数则为这个Interactor定制它所能交互的对象。

Locomotion 运动系列详解

Locomotion系列的主要作用是控制XRRig的移动,使用一个全局的LocomotionSystem来管理XRRig的位置。

LocomotionSystem持有一个LocomotionProvider,LocomotionProvider提供了XRRig的移动方式。

传送的基本用法

  1. 创建一个Plane作为地面

  2. 为地面添加Teleportation Area组件

  3. 全局添加LocomotionSystem,它负责管理全局的移动,

  4. 为XR-Rig添加TeleportationProvider

  5. 为地面的TeleportationArea添加LocomotionSystem

  6. 为LocomotionSystem添加XR-Rig,需要告知它移动哪个物体,应该移动XR-Rig

  7. 为TeleportationProvider指定LocomotionSystem,这一步其实可以省略,因为它会默认寻找全局的LocomotionSystem

Teleportation Anchor

为地面添加Teleportation Anchor即可,其它步骤同上。

TeleportationArea可以到达任意一个位置,TeleportationAnchor只能到达一个固定位置。

LocomotionSystem:互斥地管理当前的LocomotionProvider

有一个当前的LocomotionProvider,就像搜狗输入法持有一个当前的输入框一样。

当一个LocomotionProvider持有的时间超过了timeOut时间,则清空互斥性(清空provider成员和超时时间)。

在启动的时候自动寻找XROrigin

protected void Awake()
{
    if (m_XROrigin == null)        m_XROrigin = FindObjectOfType<XROrigin>();
}

provider可以向LocomotionSystem请求锁,然后LocomotionSystem会返回请求锁是否成功。

LocomotionSystem的设计非常值得学习,试想,如果没有LocomotinoSystem,任何脚本都能够随意修改Camera的位置,那么很有可能出现Camera一会儿被A修改,一会儿被B修改,会导致XR-Rig的位置忽左忽右、闪闪烁烁。Unity虽然是单线程执行MonoBehavior,但是依旧需要考虑并发问题。

LocomotionProvider:提供BeginLocomotion和EndLocomotion的封装

需要为LocomotionProvider指定LocomotionSystem。即便不设置,也会从全局去自动获取。

一般来说,全局只需要有一个LocomotionSystem。

protected virtual void Awake()
{
    if (m_System == null)        m_System = FindObjectOfType<LocomotionSystem>();
}

LocomotionProvider实现了BeginLocomotion和EndLocomotion两个函数用于回调处理,这两个函数会被LocomotionSystem调用。

protected bool BeginLocomotion()
{
    if (m_System == null)        return false;    var success = m_System.RequestExclusiveOperation(this) == RequestResult.Success;    if (success)        beginLocomotion?.Invoke(m_System);    return success;
}

LocomotionProvider的常见用法如下,使用BeginLocomotion和EndLocomotion来获取锁。

class MyMoveClass:LocomotionProvider{    void myMove(){        if(BeginLocomotion()){            ....            system.xrRig.MoveCamera().....            ....            EndLocomotion();        }    }}

LocomotionProvider的子类:

  • SnapTurnProvider:使用一个2d轴移动头戴

  • TeleportationProvider:实现位置移动

  • ContinuousMoveProvider:位置连续移动

  • ContinuousTurnProvider:连续转动

TeleportationProvider:处理TeleportationRequest

继承自LocomotionProvider,这个类只有短短的65行代码。

建议一切的对XRRig的移动都通过TeleportationProvider。

TeleportProvider持有一个TeleportRequest,在Update的时候执行这个TeleportRequest。

public struct TeleportRequest
{
  public Vector3 destinationPosition;//目标位置  public Quaternion destinationRotation;//目标旋转  public Vector3 destinationUpVector;  public Vector3 destinationForwardVector;  public float requestTime;  public MatchOrientation matchOrientation;//是摄像机匹配还是整个Rig都匹配
}

当处理这个请求的时候

switch (m_CurrentRequest.matchOrientation)
{
    case MatchOrientation.None:
        xrRig.MatchRigUp(m_CurrentRequest.destinationUpVector);
        break;
    case MatchOrientation.Camera:
        xrRig.MatchRigUpCameraForward(m_CurrentRequest.destinationUpVector, m_CurrentRequest.destinationForwardVector);
        break;
    //case MatchOrientation.Rig:
    //    xrRig.MatchRigUpRigForward(m_CurrentRequest.destinationUpVector, m_CurrentRequest.destinationForwardVector);
    //    break;
}

Vector3 heightAdjustment = xrRig.rig.transform.up * xrRig.cameraInRigSpaceHeight;

Vector3 cameraDestination = m_CurrentRequest.destinationPosition + heightAdjustment;

xrRig.MoveCameraToWorldLocation(cameraDestination);

BaseTeleportationInteractable

TeleportationArea和TeleportationAnchor的基类。

有了这个类,TeleportationArea和TeleportationAnchor实现起来就变得非常简单了,只需要提供一个GenerateTeleportRequest即可。

现在Unity里面流行一种啰里啰嗦的写法:定义一个成员变量先定义一个protected字段,然后使用get set处理这个字段。

[SerializeField]
[Tooltip("The teleportation provider that this Teleport interactable will communicate Teleportation Requests to.")]
protected TeleportationProvider m_TeleportationProvider = null;
/// <summary>
/// The teleportation provider that this Teleport interactable will communicate Teleportation Requests to.
/// If no teleportation provider is configured, then on awake the base teleportation interactable will attempt to find a teleportation provider to work with.
/// </summary>
public TeleportationProvider teleportationProvider { get { return m_TeleportationProvider; } set { m_TeleportationProvider = value; } }

BaseTeleportationInteractable

TeleportationArea和TeleportationAnchor的基类。

有了这个类,TeleportationArea和TeleportationAnchor实现起来就变得非常简单了,只需要提供一个GenerateTeleportRequest即可。

现在Unity里面流行一种啰里啰嗦的写法:定义一个成员变量先定义一个protected字段,然后使用get set处理这个字段。

[SerializeField]
[Tooltip("The teleportation provider that this Teleport interactable will communicate Teleportation Requests to.")]
protected TeleportationProvider m_TeleportationProvider = null;
/// <summary>
/// The teleportation provider that this Teleport interactable will communicate Teleportation Requests to.
/// If no teleportation provider is configured, then on awake the base teleportation interactable will attempt to find a teleportation provider to work with.
/// </summary>
public TeleportationProvider teleportationProvider { get { return m_TeleportationProvider; } set { m_TeleportationProvider = value; } }

BaseTeleportationInteractable包含的字段说明:

  • TeleportationProvider

  • MatchOrientation:可以指定Camera或者XRRig

  • TeleportTrigger:四种触发时机,select(enter+exit);activate+deactivate

它有一个虚方法:GenerateTeleportRequest,它的所有子类都只需要提供这个方法即可。

这个方法接收两个参数:

  • XRBaseInteractor:

  • Raycast

BaseTeleportationInteractable的核心逻辑:根据射线,检测是否与colliders相撞。如果找到了collider,则向TeleportationProvider发送移动请求。

bool found = false;
for(int i = 0; i < colliders.Count; i++)
{
    if (colliders[i] == raycastHit.collider)
    {
        found = true;
        break;
    }
}

if (found)
{
    TeleportRequest tr = new TeleportRequest();
    tr.matchOrientation = m_MatchOrientation;
    tr.requestTime = Time.time;
    if (GenerateTeleportRequest(interactor, raycastHit, ref tr))
    {
        m_TeleportationProvider.QueueTeleportRequest(tr);
    }
}

TeleportationArea

设置Request的时候,使用Transform自身的方向。

protected override bool GenerateTeleportRequest(XRBaseInteractor interactor, RaycastHit raycastHit, ref TeleportRequest teleportRequest)
{          
    teleportRequest.destinationPosition = raycastHit.point;
    teleportRequest.destinationUpVector = transform.up; // use the area transform for data.
    teleportRequest.destinationForwardVector = transform.forward;
    teleportRequest.destinationRotation = transform.rotation;
    return true;
}      

TeleportationAnchor

TeleportationAnchor接受一个Transform组件,当跳转的时候,直接跳转到该transform

teleportRequest.destinationPosition = m_TeleportAnchorTransform.position;teleportRequest.destinationUpVector = m_TeleportAnchorTransform.up;teleportRequest.destinationRotation = m_TeleportAnchorTransform.rotation;            teleportRequest.destinationForwardVector = m_TeleportAnchorTransform.forward;

SnapTurnProvider:指定一个Controller(手柄),通过手柄实现转动。

snapTurn:急转弯,快速转身。意思是通过手柄上的摇杆快速实现变向。

SnapTurnProvider也是继承自LocomotionProvider,使用它可以实现用手柄的2D轴控制转向。

  • turnUsage:控制转动的时候,一定是2D轴。一般的手柄只有一个primaryAxis,但是Unity提供了选项可以使用secondary2DAxis。turnUsage表示使用primaryAxis还是使用secondaryAxis

  • Controllers:一个controller数组,表示使用哪一个控制器来执行操作。

  • turnAmount:转动量,默认是45度。

  • debounceTime:防止调用太频繁,默认500ms

  • deadZone:摇杆必须偏移多少才执行转向,跟debounceTime一样,都是为了让输入更精确。

计算旋转的幅度

只允许左右旋转,当摇杆转向前面或者后面的时候,不执行任何操作。当摇杆向左的时候,则向左转45度;当摇杆向右的时候,则向右转45度。

InputDevice device = controller.inputDevice;

Vector2 currentState;
if (device.TryGetFeatureValue(feature, out currentState))
{
    if (currentState.x > deadZone)
    {
        StartTurn(m_TurnAmount);
    }
    else if (currentState.x < -deadZone)
    {
        StartTurn(-m_TurnAmount);
    }
}

执行旋转

if (Math.Abs(m_CurrentTurnAmount) > 0.0f && BeginLocomotion())
{
    var xrRig = system.xrRig;    if (xrRig != null)    {        xrRig.RotateAroundCameraUsingRigUp(m_CurrentTurnAmount);    }    m_CurrentTurnAmount = 0.0f;    EndLocomotion();
}

ContinuousMoveProvider:连续移动提供者

它能够提供平滑的移动。

protected virtual Vector3 ComputeDesiredMove(Vector2 input)
{
    if (input == Vector2.zero)
        return Vector3.zero;

    var xrOrigin = system.xrOrigin;
    if (xrOrigin == null)
        return Vector3.zero;

    // Assumes that the input axes are in the range [-1, 1].
    // Clamps the magnitude of the input direction to prevent faster speed when moving diagonally,
    // while still allowing for analog input to move slower (which would be lost if simply normalizing).
    var inputMove = Vector3.ClampMagnitude(new Vector3(m_EnableStrafe ? input.x : 0f, 0f, input.y), 1f);

    var originTransform = xrOrigin.Origin.transform;
    var originUp = originTransform.up;

    // Determine frame of reference for what the input direction is relative to
    var forwardSourceTransform = m_ForwardSource == null ? xrOrigin.Camera.transform : m_ForwardSource;
    var inputForwardInWorldSpace = forwardSourceTransform.forward;
    if (Mathf.Approximately(Mathf.Abs(Vector3.Dot(inputForwardInWorldSpace, originUp)), 1f))
    {
        // When the input forward direction is parallel with the rig normal,
        // it will probably feel better for the player to move along the same direction
        // as if they tilted forward or up some rather than moving in the rig forward direction.
        // It also will probably be a better experience to at least move in a direction
        // rather than stopping if the head/controller is oriented such that it is perpendicular with the rig.
        inputForwardInWorldSpace = -forwardSourceTransform.up;
    }

    var inputForwardProjectedInWorldSpace = Vector3.ProjectOnPlane(inputForwardInWorldSpace, originUp);
    var forwardRotation = Quaternion.FromToRotation(originTransform.forward, inputForwardProjectedInWorldSpace);

    var translationInRigSpace = forwardRotation * inputMove * (m_MoveSpeed * Time.deltaTime);
    var translationInWorldSpace = originTransform.TransformDirection(translationInRigSpace);

    return translationInWorldSpace;
}

ContinuousTurnProvider:连续转向提供者

它能够提供平滑的转向,与snapTurnProvider相对应,SnapTurnProvider是立即转身提供器。

把它的Controller的size设置成1,然后把XRRig里面的LeftController设置进去,通过调整左手柄的primaryAxis就能够实现平滑的旋转。

综合的例子

  1. 添加XR-Origin

  2. 添加Plane,一个地面,为地面添加TeleportationArea

  3. 为XR-Origin添加LocomotionSystem,TeleportationProvider,ContinuousTurnProvider_Device(绑定左手手柄),ContinuousMoveProvider_Device(绑定右手手柄)

  4. 为XR-Origin的左右手柄绑定Pico的Prefab。

头戴XRRig

任何GameObject都有自己的X轴、Y轴、Z轴。一个GameObject的up指的就是这个GameObject的Y轴正方向,forward指的就是Z轴正方向。

在空间中,制定up和forward两个方向就能够确定一个GameObject的朝向。

XRRig的代码非常值得阅读,看完能够对向量变换有更深的理解。

XRRig的属性

  • Rig base game Object:尽量不要设置这个变量,默认就是当前GameObject。Rig表示相机+手柄

  • Camera Floor Offset Object:相机的偏移量,不需要设置,默认是当前GameObject

  • Camera GameObject:相机对象

  • Tracking Origin Mode:

Rig和Camera的相对位置

主要学习InverseTransformPoint。每个GameObject都有自己的XYZ坐标系,它看其它物体都能够求出其它物体的坐标。

m_CameraGameObject.transform.InverseTransformPoint(m_RigBaseGameObject.transform.position)

public float cameraYOffset { get { return m_CameraYOffset; } set { m_CameraYOffset = value; TryInitializeCamera(); } }

/// <summary>Gets the rig's local position in camera space.</summary>
public Vector3 rigInCameraSpacePos { get { return m_CameraGameObject.transform.InverseTransformPoint(m_RigBaseGameObject.transform.position); } }

/// <summary>Gets the camera's local position in rig space.</summary>
public Vector3 cameraInRigSpacePos { get { return m_RigBaseGameObject.transform.InverseTransformPoint(m_CameraGameObject.transform.position); } }

/// <summary>Gets the camera's height relative to the rig.</summary>
public float cameraInRigSpaceHeight { get { return cameraInRigSpacePos.y; } }

头戴的transform操作:有些操作Rig,有些操作Camera

bool RotateAroundCameraUsingRigUp(float angleDegrees):绕着竖直方向旋转angleDegrees

bool RotateAroundCameraPosition(Vector3 vector, float angleDegrees):绕着直线vector旋转angleDegrees

bool MatchRigUp**(Vector3 destinationUp):将相机的y轴角度设置为destinationUp**

public bool MatchRigUp(Vector3 destinationUp)
{
    if (m_RigBaseGameObject.transform.up == destinationUp)
        return true;

    if (m_RigBaseGameObject == null)
    {
        return false;
    }
    Quaternion rigUp = Quaternion.FromToRotation(m_RigBaseGameObject.transform.up, destinationUp);
    m_RigBaseGameObject.transform.rotation = rigUp * transform.rotation;
    
    return true;
}

bool MatchRigUpCameraForward**(Vector3 destinationUp, Vector3 destinationForward):将相机的up和forward分别调整为destinationUp和destinationForward。**

public bool MatchRigUpCameraForward(Vector3 destinationUp, Vector3 destinationForward)
{
    if (m_CameraGameObject != null && MatchRigUp(destinationUp))
    {
        // project current camera's forward vector on the destination plane, whose normal vector is destinationUp.
        Vector3 projectedCamForward = Vector3.ProjectOnPlane(cameraGameObject.transform.forward, destinationUp).normalized;

        // the angle that we want the rig to rotate is the signed angle between projectedCamForward and destinationForward, after the up vectors are matched. 
        float signedAngle = Vector3.SignedAngle(projectedCamForward, destinationForward, destinationUp);

        RotateAroundCameraPosition(destinationUp, signedAngle);

        return true;
    }

    return false;
}

MatchRigUpRigForward

public bool MatchRigUpRigForward (Vector3 destinationUp, Vector3 destinationForward)
{
    if (m_RigBaseGameObject != null && MatchRigUp(destinationUp))
    {
        // the angle that we want the rig to rotate is the signed angle between the rig's forward and destinationForward, after the up vectors are matched. 
        float signedAngle = Vector3.SignedAngle(m_RigBaseGameObject.transform.forward, destinationForward, destinationUp);

        RotateAroundCameraPosition(destinationUp, signedAngle);

        return true;
    }

    return false;
}

MoveCameraToWorldLocation

将相机移动到特定位置

public bool MoveCameraToWorldLocation(Vector3 desiredWorldLocation)
{
    if (m_CameraGameObject == null)
    {
        return false;
    }

    Matrix4x4 rot = Matrix4x4.Rotate(cameraGameObject.transform.rotation);
    Vector3 delta = rot.MultiplyPoint3x4(rigInCameraSpacePos);
    m_RigBaseGameObject.transform.position = delta + desiredWorldLocation;

    return true;
}

与手柄有关的脚本:XR Controller

属性

  • UpdateType:分为Update、BeforeRender、UpdateAndBeforeRender三种,trackedPosDriver需要使用这个字段来判断什么时候采样数据。Update表示Update阶段,BeforeRender表示渲染前。

手柄的模型:

modelPrefab,它是一个Transform类型的变量。

modelTransform:实际上modelPrefab跟modelTransform基本上都是重合的,这个modelTransform的存在使得这两者可以不重合。

三种操作对应的手柄按键:

enum InteractionTypes { select, activate, uiPress };

  • select:默认是grip,选择物体。

  • activate:默认trigger,表示激活物体

  • press:UI中的点击操作等,默认是trigger键

动画:

  • selectTransition:选中时候的渐变

  • DeSelectTransition:取消选中时候的渐变

Visual:可视化

XR Interactor Line Visual:射线可视化

这个类也是位于UnityEngine.XR.Interaction.Toolkit命名空间下。

手柄射线的可视化

Reticle:准星。

XR Interactor Recicle Visual:准星可视化

准星可视化。

画布TrackedDeviceGraphicRaycaster

TrackedDeviceGraphicRaycaster使得画布能够接受手柄的射线。

InputActionManager:基于动作的输入

XR 配置插件

如果直接使用AssignedSettings可能没有东西,需要使用特定平台的

var m=XRGeneralSettings.Instance.Manager;
m.TrySetLoaders(new List<XRLoader>());
var se=XRGeneralSettings.Instance.AssignedSettings;

正确的设置XR的方法:

        static XRManagerSettings GetXrSettings()
        {
            XRGeneralSettings generalSettings = XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(BuildTargetGroup.Android);
            if (generalSettings == null) return null;
            var assignedSettings = generalSettings.AssignedSettings;
            return assignedSettings;
        }

        static PXR_Loader GetPxrLoader()
        {
            var x = GetXrSettings();
            if (x == null) return null;
            foreach (var i in x.activeLoaders)
            {
                if (i is PXR_Loader)
                {
                    return i as PXR_Loader;
                }
            }

            return null;
        }
        public static bool UsePicoXr
        {
            get { return GetPxrLoader() != null; }
            set
            {
                var x = GetXrSettings();
                if (x == null) return;
                var loader = GetPxrLoader();
                if (value == false)
                {
                    if (loader == null)
                    {
                    }
                    else
                    {
                        x.TryRemoveLoader(loader);
                    }
                }
                else
                {
                    if (loader == null)
                    {
                        var res = XRPackageMetadataStore.AssignLoader(x, nameof(PXR_Loader), BuildTargetGroup.Android);
                        Debug.Log($"设置XR{res} {value}");
                    }
                    else
                    {
                    }
                }
            }
        }

XR Plugin Management文档:https://docs.unity3d.com/Packages/com.unity.xr.management@4.0/manual/EndUser.html

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

namespace Sougou.Scripts.SougouKeyboard
{
    public class testClick : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start()
        {
        }

        // Update is called once per frame
        void Update()
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            if (Input.GetMouseButtonDown(0))
            {
                //Debug.Log("mouse position:" + Input.mousePosition.ToString());
                if (Physics.Raycast(ray, out hitInfo))
                {
                    string name = hitInfo.collider.gameObject.name;
                    //if (name == "KeyBoard-LAYOUT")
                    {
                        GameObject kbd = hitInfo.collider.gameObject;
                        Vector3 vecKbd = kbd.transform.InverseTransformPoint(hitInfo.point);
                        Vector2 pixelUV = hitInfo.textureCoord;
                        Renderer rend = hitInfo.transform.GetComponent<Renderer>();
                        Texture2D tex = rend.material.mainTexture as Texture2D;
                        Vector2 pixelOrg;
                        Vector2 texSize = new Vector2(813, 345);
                        pixelOrg.x = pixelUV.x * texSize.x;
                        pixelOrg.y = (1 - pixelUV.y) * texSize.y;
                        Debug.Log("ray click " + name + ": 3d point=" + vecKbd.ToString() + " uv=(" + pixelUV.x + "," + pixelUV.y + ") org=(" + pixelOrg.ToString() + ")" + " w=" + texSize.x + ",h=" + texSize.y);
                    }
                }
            }
        }

        public void Click()
        {
            Debug.Log("click kbd");
        }

        private void LogEvent(string prefix, PointerEventData eventData)
        {
            Debug.Log(prefix + ": " + eventData.pointerCurrentRaycast.gameObject.name + " x=" + eventData.position.x + ",y=" + eventData.position.y);
        }
    }
}

搜狗输入法测试click。

  • immersed
  • link
  • VirtualDesktop:行业翘楚,韩国人制作

几何

Unity的几何工具类GeometryUtility

namespace UnityEngine
{

/// <summary>

/// <para>Utility class for common geometric functions.</para>

/// </summary>

[StaticAccessor("GeometryUtilityScripting", StaticAccessorType.DoubleColon)]

[NativeHeader("Runtime/Graphics/GraphicsScriptBindings.h")]

public sealed class GeometryUtility

unity中的坐标系

img.png

欧拉角

如何描述三维空间中的一个角度?使用欧拉角进行描述

  • 绕x轴旋转:俯仰角Pitch
  • 绕y轴旋转:偏航角Yaw
  • 绕z轴旋转:滚动角Roll

四元数Quaternion和旋转

如何描述一个空间角?欧拉角是一种方法。还有一种四元数方法。 四元数表示,绕着某个向量旋转theta角度。右手定则。 四元数用于表示旋转 相对欧拉角的优点:

  • 能进行增量旋转
  • 避免万向锁
  • 给定方位的表达方式有两种,互为负(欧拉角有无数种表达方式)

欧拉角的缺点:

  • 万向节锁
  • 无法平滑表示角度

三种矩阵变换

  • 平移
  • 缩放
  • 旋转

其中平移+缩放叫做错切变换。
平移+旋转不改变形状,只改变位置。

如何描述一个旋转

  • 一个变换矩阵和一个轴
  • 一个欧拉角矩阵
  • 一个四元数

用法举例

transform.rotation = Quaternion.AngleAxis(degrees, transform.right) * transform.rotation;
Vector3 newVector = Quaternion.AngleAxis(90, Vector3.up) * Quaternion.LookRotation(someDirection) * someVector;

参考资料

https://blog.csdn.net/candycat1992/article/details/41254799

四元数详细推导: https://zhuanlan.zhihu.com/p/78987582

Vector Vector.ones=1,1,1 Vector.forward=001 Vector.left=1,0,0 Vector.top=0,1,0 RotateAround 绕着目标点point旋转一个角度。根据axis+angle可以得到四元数,这就是旋转变量。 根据position-point得到一个向量,四元数乘以这个向量,得到距离向量。point加上距离向量。

public void RotateAround(Vector3 point, Vector3 axis, float angle)
{
  Vector3 position = this.position;
  Vector3 vector3 = Quaternion.AngleAxis(angle, axis) * (position - point);
  this.position = point + vector3;

长方体如何绕着一个端点旋转

例如胳膊绕着一个端点运动。 Unity如何给一个RectTransform的对象添加外边框

动画

Timeline可以在UnityEditor中进行时间调度。

一个Timeline=若干个Track

一个Track=一个Track对象+若干个TrackClip,Track对象就是TrackClip所作用的对象。

概念

TimeLine是Unity中比较综合的概念,从TimeLine出发可以牵扯出许多Unity概念。

Track Clip:片段,一个片段有起始时间、结束时间。

Track:轨道的概念,一个轨道控制一件事情,在一个轨道上可以添加多个片段。

Player Director:播放导演,是一个组件,持有一个PlayableAsset。Player Director=Timeline+Bindings。

Signal Emitter:发射信号的组件

Signal Receiver:接收信号的组件

Signal:是一种资源文件,描述了一种信号。Signal Emitter发送信号,Signal Receiver接收信号。

Playable:Playable是PlayerDirector所持有的主要对象,Timeline就是一个Playable对象。PlayableAsset和PlayableBehaviour也都是Playable对象。Timeline是PlayableAsset对象。

PlayableBehaviour:包含一堆回调函数

PlayableAsset:PlayableBehaviour的工厂方法。

Animator:负责播放动画,接收一个Animate资源文件

Animator Controller:负责动画之间的切换

Animate:一种资源文件

基本步骤

  1. 创建Timeline资源,编辑Timeline的右侧时间条

  2. 在一个GameObject上创建PlayerDirector,在PlayerDirector上绑定Timeline和bindings。

轨道类型

  • TrackGroup

  • ActivationTrack

  • AudioTrack

  • ControlTrack

  • PlayableTrack

  • SignalTrack

  1. Activation Track,与游戏对象的Activate属性进行绑定。

  2. Animation Track,与动画进行绑定,到达时间之后游戏开启某种动画

  3. Audio Track,设置一个AudioSource,表示声音从何处播放。一个Audio Track可以设置多个AudioClip。

  4. Control Track

  5. Playable Track:用户自定义Playable事件

  6. Signal Track:可以发射信号,触发事件。右键在轨道上添加SignalEmitter,SignalEmitter绑定一个Signal。当SignalTrack触发信号的时候,只有当前Track绑定的GameObject会收到信号,可以指定这个GameObject收到信号的回调函数。

Timeline有两类自定义:

  1. 使用PlayableTrack:自定义轨道的Playable

  2. 自定义Track:这是一种更为彻底的定制。

Timeline提供的这些Track基本上覆盖了大多数情况

使用SignalTrack可以设置回调函数,这基本上就足够使用了。

PlayableDirector

WrapMode:循环模式

  • hold:播放完一遍就停止且停止在最后一帧

  • loop:无限循环

  • None:播放完成后回到第一帧并停止播放

自定义轨道

自定义轨道是对Unity Editor进行定制。

继承TrackAsset就会在Timeline中出现这个可选的Track。

using UnityEngine; 
using UnityEngine.Timeline;  
public class DialogueTrack : TrackAsset {        } 

TrackAsset有三个注解:

  1. TrackBindingTypeAttribute:向轨道拖放无提示,执行绑定类型检查。例如,若物体不含有Light组件,则在拖放物体时,自动添加绑定的组件类型。
using UnityEngine; 
using UnityEngine.Timeline;  
[TrackBindingType(typeof(Light), TrackBindingFlags.AllowCreateComponent)] 
public class LightTrack : TrackAsset {        }
  1. TrackClipType:可以拖放的片段类型,在Timeline编辑器的右侧。
using UnityEngine; 
using UnityEngine.Timeline;  
[TrackClipType(typeof(DialogueClip))] 
public class DialogueTrack : TrackAsset {       } 
  1. TrackColor:指定轨道边界的颜色
using UnityEngine; 
using UnityEngine.Timeline;  
[TrackColor(1.0f, 0.0f, 0.0f)]            
// red public class DialogueTrack : TrackAsset {       
}

Playable Track

实现PlayableBehavior

[System.Serializable]
  public class LightData : PlayableBehaviour{
    
      public float range;
      public Color color;
      public float intensity;
      [HideInInspector]
      public Transform lookTarget;

      public override void OnPlayableCreate(Playable playable){

          var duration = playable.GetDuration();

          if(Mathf.Approximately((float)duration, 0)){
              throw new UnityException("A Clip Cannot have a duration of zero");
          }

      }
  }

实现PlayableAsset

  [System.Serializable]
  public class LightClip : PlayableAsset {

      public LightData templete = new LightData();
      public ExposedReference<Transform> lookTarget;

      // Factory method that generates a playable based on this asset
      public override Playable CreatePlayable(PlayableGraph graph, GameObject go) {
          var playable = ScriptPlayable<LightData>.Create(graph, templete); 
          LightData clone = playable.GetBehaviour();
          clone.lookTarget = lookTarget.Resolve(graph.GetResolver());
          return playable;
      }

  }

参考资料

https://zhuanlan.zhihu.com/p/513872343

动画的实现方式

  1. 在Unity引擎中使用Animator系列制作动画,Unity编辑器提供了一系列动画制作工具

  2. 使用外部工具3dmax、maya制作动画,导入到Unity

  3. 使用代码直接修改GameObject的Transform实现动画,通常使用协程实现。布娃娃系统是使用代码实现动画的典型例子。使用代码控制动画的好处是能够实现最大的灵活性,例如让一个小球在某个半径内随机运动,如果使用关键帧动画就无法做到完全随机。

本文只介绍在Unity引擎中制作动画。

Unity旧版动画系统

Unity是一个不断发展的游戏引擎,它的很多模块都在与时俱进。

  1. 输入系统从InputManager到InputSystem

  2. UI系统从Unity UI到Element新版UI

  3. 基础架构从面向对象到DOTS

Unity的动画系统也有两套,旧版animation动画系统和新版的Animator动画系统,其中Animator动画系统也叫Mecanim动画系统。因为旧版的动画系统叫animation,新版的动画系统只能找一个动画的同义词,也就是mecanim。旧版的animation比新版的动画系统简单很多,个人认为不该删掉,两者并存更好。

旧版的Animation组件直接持有一个Animation对象,新版的Animator持有一个AnimateController。

Macanim动画系统的特点

  1. 面向动画应用的动画系统,使用3dmax、maya可以导出fbx文件,unity中直接导入动画文件

  2. 基于状态机的动画控制系统

  3. 核心是关键帧动画。

Mecanim最初与人形角色动画密切相关,后来经过扩充可以适用于其它动画。

动画相关的组件

  • Animate:也叫AnimationClip,动画本身,是一种资源文件,后缀名为.animate,像视频一样。这种资源有两种来源:1. 使用Animation窗口进行编辑;2.从外部导入动画。

  • AnimateController:一个有限状态自动机,持有若干个Animate。AnimateController也是一种资源文件,后缀名为.controller.可以设置进行状态转移的条件。

  • Animator:MonoBehavior,一个GameObject需要持有一个Animator才能播放动画。Animator依赖AnimateController。

Animator持有AnimateController,AnimateController是一个状态机,每个结点都是Animate动画。AnimateController和Animate都是资源文件。

动画与Playables与Timeline的关系

Playables API是动画系统的底层接口,Animator是基于Playables API的封装。

AnimateController底层实现基于PlayableGraph,所以可以使用状态机描述。

  • Playables API 允许动态动画混合。这意味着对象在场景 可以提供自己的动画。例如,武器、箱子和陷阱的动画可以动态添加到 PlayableGraph 并使用一定的持续时间。

  • Playables API 允许您轻松播放单个动画,而无需创建和管理 AnimatorController 资产所涉及的开销。

  • Playables API 允许用户动态创建混合图并直接逐帧控制混合权重。

  • PlayableGraph 可以在运行时创建,根据需要添加可播放节点。与启用和禁用节点的巨大“一刀切”图不同,PlayableGraph 可以进行定制以适应当前情况的要求。

Animation窗口

选择GameObject,然后添加属性,创建关键帧。

有两种模式:

  1. 录制模式,可以录制物体的属性变化

  2. 关键帧模式,手动创建关键帧,直接编辑属性

在底部,Animation窗口有两种视图,一个是Dopesheet(关键帧),另一个是Curbes(曲线模式)。可以使用曲线编辑器编辑动画。

AnimationController窗口

Entry为入口

Exit为出口

Entry会默认连接一个结点,这个默认结点就是最初的动画,这条线为橙色。可以右键结点,选择Set as Layer default State.

Any State:任意状态,是一个始终存在的特殊状态。

SubState Machine:子状态机,相当于包含多个结点的一个子状态机,它是为了便于整理状态。

Solo和Mute功能,这个功能主要是用于测试,使用Solo仅启用勾选了Solo的动画过渡。

为什么需要AnyState?例如角色有一个死亡状态,从跑步、吃饭、运动三个状态都可以到达死亡状态,那么需要画三条线:跑步-死亡,吃饭-死亡,运动-死亡。有了AnyState只需要画一条线,AnyState-死亡。

AnimationController中的每一个结点都有一些属性:

  1. Motion:结点对应的动画

  2. Speed:动画播放速度

  3. Multiplier:用于平滑播放动画

  4. Mirror:仅适用于人形动画,表示是否使用动画的镜像

  5. Cycle Offset:动画其实偏移量,取值0到1

  6. Foot IK:人形动画是否启用脚部IK

  7. Transision:该状态向其它状态的转移列表,其中的Solo和Mute两个参数用于调试。开启Solo之后表示该State只能进行这一种过渡,其它过渡暂时关闭;Mute表示这个过渡暂时关闭,只能进行其它过渡。

  8. AddBehaviour:用于向状态机添加行为

Animator

ApplyRootMotion:是否应用根对象的位移。勾选后, 在动画播放期间, 物体的运动相关参数完全由动画本身接管, 此时脚本控制无效. 取消勾选后, 则是由脚本来控制物体的运动参数。脚本中实现了OnAnimatorMove,相当于勾选了ApplyRootMotion,可以在脚本中控制位置和旋转。

deltaPosition:相对上一帧的位置变化量(必须允许根运动才能被计算)
deltaRotation:相对上一帧的角度变化量(必须允许根运动才能被计算)
    void OnAnimatorMove()
    {
        var transform1 = transform;
        transform1.position += animator.deltaPosition;
        transform1.rotation *= animator.deltaRotation;
    }

CullingMode:剔除模式,可用于提升性能。取值如下:

  • Always Animate:始终渲染

  • Cull Update Transforms:当物体不被摄像机可见时,仅计算根节点的位移,只保证位置正确

  • Cull Completely:当物体不可见时,完全停止动画。

骨骼-蒙皮动画

人类的很多研究都是针对人类本身进行的,例如人体骨骼动画,人类面部识别。

游戏中有很多角色,每个角色有自己的样式。

一个角色包括:骨骼、蒙皮、动画。角色的动画系统非常复杂,可能是多个骨骼同时运动。

动画控制的是骨骼的关键帧,蒙皮跟着动,角色就动起来了。

Animation的时序

  • ProcessAnimations 读取骨骼信息

  • FireAnimationEventsAndBehaviours 读取动画事件

  • ApplyOnAnimatorMove 根节点应用动画信息

  • WriteAnimatedValues 动作数值写入

  • DirtySceneObjects 对骨骼的transform进行更新写入

  • MeshSkinning.CalcMatrices 计算蒙皮矩阵

  • ScheduleGeometryJobs 子线程处理

  • MeshSkinning.Skin 计算模型,网格顶点位置

  • MeshSkinning.Render 渲染

    • PutGeometryJobFench 几何计算

    • Mesh.DrawVBO DrawCall调用

角色模型性能优化:

  1. 导入人形动画时,如果不需要IK,使用Avatar遮罩将其移除

  2. 取消掉Update When Offscreen,在不可见时不用更新动画。

Unity外部资源

使用其它软件制作复杂模型,然后导入Unity,这是一种常见的工作方式。

Blender、AutoDesk、Maya是三种比较常见的建模软件。

fbx文件是一种常见的3D文件格式。

动画复用:Avatar化身系统

在制作骨骼蒙皮动画的时候,可以为骨骼设置动画。能否将一套骨骼动画应用于两个不同的角色?可以,这就是Avatar化身系统,它的作用是将一套动画应用于多个模型。

分层和遮罩

动画分层意思是把两个动画进行组合,例如上半身动画和下半身动画分开播放。

Animator API

GetCurrentAnimatorStateInfo

GetNextAnimatorStateInfo

animator.Play("动画状态名称")

animator.Update(duration) 将动画时间更新到duration之后

animatorController结点的动画重写:

Animator animator = GetComponent<Animator>();
AnimatorOverrideController overrideController = new AnimatorOverrideController();
overrideController.runtimeAnimatorController = animator.runtimeAnimatorController;
overrideController["name"] = newAnimationClip;
animator.runtimeAnimatorController = overrideController;

检查动画的状态

//检查是否正在播放jump动画.
AnimatorStateInfo stateinfo = anim.GetCurrentAnimatorStateInfo(0);   
bool playingJump = stateinfo.IsName("jump");
if(playingJump)
{
    if(stateinfo.normalizedTime < 1.0f)
    {
        //正在播放
    }
    else
    {
        //播放结束
    }
     
}

为AnimatorController中的每个结点添加脚本

AnimatorController的每个状态都可以挂载脚本,只需要继承StateMachineBehaviour

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JumpState : StateMachineBehaviour
{
    private GameObject player;
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 正在played的状态的第一帧被调用
        Debug.Log("------OnStateEnter------------");
    }
    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {        
    }
    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 转换到另一个状态的最后一帧 被调用
        Debug.Log("-------------OnStateExit-----------------");
    }
    // OnStateMove is called right after Animator.OnAnimatorMove()
    override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
            // 在OnAnimatorMove之前被调用 
        
    }
    // OnStateIK is called right after Animator.OnAnimatorIK()
    override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 在OnAnimatorIK之后调用,用于在播放状态时的每一帧的monobehavior。
        // 需要注意的是,OnStateIK只有在状态位于具有IK pass的层上时才会被调用。
        // 默认情况下,图层没有IK通道,所以这个函数不会被调用
        // 关于IK的使用,可以看看这篇文章《Animator使用IK实现头部及身体跟随》
        // https://www.jianshu.com/p/ae6d65563efa
    }
}

使用代码控制播放速度

Animator ator = go1.GetComponent<Animator>();
var stateinfo = ator.GetCurrentAnimatorStateInfo(0);
if(stateinfo.IsName("Jump"))
{
    ator.speed = 2;
}

动画事件

动画事件用于通知GameObject当前正在播放何种动画,可以携带float、int、string、object四个参数。

Animator IK

在动画中应用逆向骨骼。

MatchTarget

当人的手抓住某个位置时,人的手位置固定,动画继续播放。

例如,扣篮的时候手与篮筐相对静止,身体绕着篮筐旋转。

    public Animator ani;
    public Transform LeftHand;
    bool hasJump = false;
    void Start () {
        ani = GetComponent<Animator>();
    }
    
    
    void Update () {
        if (ani)
        {
            AnimatorStateInfo info = ani.GetCurrentAnimatorStateInfo(0);
            
            if (Input.GetKeyDown(KeyCode.Space))
            {
                ani.SetBool("Jump", true);
              
            }
            if (info.IsName("Base Layer.Vault"))
            {
                ani.SetBool("Jump", false);
                // 第一个参数动作位置,第二个参数角色旋转,第三个是做动作的某个身体部位,第四个是权重信息,第五六参数是获取动画曲线
                ani.MatchTarget(LeftHand.position, LeftHand.rotation, AvatarTarget.LeftFoot, new MatchTargetWeightMask(new Vector3(1, 1, 1), 0), ani.GetFloat("StartA"), ani.GetFloat("EndA"));
                hasJump = true;
            }
        }
    }
}

使用代码根据fbx创建状态机

using System.Collections;
using UnityEditor;
using UnityEditor.Animations;//5.0改变 UnityEditorInternal;并不能用了。
 
public class CreateAnimatorController : Editor 
{
    [MenuItem("ModelConfig/创建Controller")]
    static void DoCreateAnimationAssets()
    {
        //创建Controller
        AnimatorController animatorController = AnimatorController.CreateAnimatorControllerAtPath("Assets/animation.controller");
        //得到它的Layer
        AnimatorControllerLayer layer = animatorController.layers[0];
        //将动画保存到 AnimatorController中
        AddStateTransition("Assets/Art Resources/Character/moster-002/basic/moster-002@run.FBX", layer);
        AddStateTransition("Assets/Art Resources/Character/moster-002/basic/moster-002@stand.FBX", layer);
        AddStateTransition("Assets/Art Resources/Character/moster-002/basic/moster-002@born.FBX", layer);
    }
 
    private static void AddStateTransition(string path, AnimatorControllerLayer layer)
    {
        AnimatorStateMachine sm = layer.stateMachine;
        //根据动画文件读取它的AnimationClip对象
        AnimationClip newClip = AssetDatabase.LoadAssetAtPath(path, typeof(AnimationClip)) as AnimationClip;
        
        ////取出动画名子 添加到state里面
        AnimatorState state = sm.AddState(newClip.name);
        //5.0改变
        state.motion = newClip;
        Debug.Log(state.motion);
        //把state添加在layer里面
        AnimatorStateTransition trans = sm.AddAnyStateTransition(state);
    }
}

参考资料

https://zhuanlan.zhihu.com/p/492136094

https://www.jb51.net/article/221837.htm

Unity官方文档:https://docs.unity3d.com/cn/2021.3/Manual/AnimationOverview.html

三种基本的动画类型

  • 关键帧动画:也叫单一网格模型动画,在动画序列的关键帧里记录各个顶点的原位置及其改变量,然后插值运算实现动画效果,角色动画较真实。
  • 关节动画:布娃娃系统,缺点,关节处存在缝隙。把角色分成若干独立部分,一个部分对应一个网格模型,部分的动画连接成一个整体的动画,角色比较灵活,Quake2中使用这种动画;
  • 骨骼蒙皮动画:也叫骨骼动画,在关节动画基础上加上蒙皮。广泛应用的动画方式,集成了以上两个方式的优点,骨骼按角色特点组成一定的层次结构,有关节相连,可做相对运动,皮肤作为单一网格蒙在骨骼之外,决定角色的外观;

骨骼蒙皮动画

一个骨骼蒙皮动画通常包含:

  • 骨骼层次结构:BoneHierachy
  • 蒙皮:SkinedMesh=Mesh+网格蒙皮数据,网格蒙皮数据=顶点对应的骨骼和权重。
  • 骨骼的动画关键帧:AnimalClip

骨骼蒙皮动画可以拆解为:骨骼动画(Rigging)+蒙皮(SkinedMesh)

参考资料

骨骼动画:https://jishuin.proginn.com/p/763bfbd37420
骨骼动画详解:https://blog.csdn.net/chenwu_843402773/article/details/8635917

多媒体

视频的渲染方式RenderMode

摄像机的远景近景:摄像机是一个光锥,这个光锥只能看到近平面和远平面之间的物体。

  • 远景又叫远平面,是摄像机能够看到的最远的那个光锥截面

  • 近景又叫近平面,是摄像机能够看到的最近的那个光锥截面

  • Camera Far Plane:在摄像机的远平面上渲染视频

  • Camera Near Plane:在摄像机的近平面上渲染视频

    • Camera:摄像机
    • Alpha:因为是近景,通过设置透明度可以防止视频完全遮挡后面的物体
  • Render Texture

Camera Far Plane和Camera Near Plane模式下,视频跟随摄像机固定。如果在VR设备上,会产生严重的眩晕感。

如何将VideoPlayer在画布上显示?

在文件夹里面添加RenderTexture,把这个RenderTexture添加到VideoPlayer里面。
在Canvas上面创建一个RawImage,把这个RawImage的Texture设置为这个RenderTexture。

也就是说,以RenderTexture为纽带,可以把两个GameObject关联起来。

AudioSource和AudioClip的最佳实践

level 1

导入AudioClip,然后手动创建一堆空的AudioSource,每个AudioSource绑定AudioClip,最后再把每个AudioSource放到主脚本中。

这种方式的缺点就是需要手动创建AudioSource,然后设置AudioSource的Clip属性。这个操作是完全没必要的。

level 2

在主脚本上创建AudioClip,然后在脚本里面创建AudioSource并绑定AudioClip。
这种方式直接设置一些public clip即可。

level 3

直接把所有的音频拖动到场景中的某个物体下面,这些AudioClip会自动变成AudioSource,然后再把AudioSource拖动到场景中。
如果有一个音频组合经常使用,可以把它们创建成为一个prefab,也可以为这个prefab设置一些常用代码。

平台

在Native中获取当前Activity

在unity中有时需要传给android插件Context,这样可调用一些如getApplicationxxx的android api。
可通过下面的代码获取currentActivity:

private AndroidJavaObject getUnityContext()
{
    AndroidJavaClass unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    AndroidJavaObject unityActivity = unityClass.GetStatic<androidjavaobject>("currentActivity");
    return unityActivity;
}

在C++里面获取当前Activity

//get unity activity first
SYMBOL_HIDDEN jobject getUnityActivity(JNIEnv *jni) {

    jclass clazz = jni->FindClass("com/unity3d/player/UnityPlayer");
    if (clazz == NULL) {
        LOGE("[getUnityActivity]cannot find class:com/unity3d/player/UnityPlayer");
        return NULL;
    }
    jfieldID activityFI = jni->GetStaticFieldID(clazz, "currentActivity", "Landroid/app/Activity;");
    jobject activityObj = jni->GetStaticObjectField(clazz, activityFI);
    return activityObj;
}

Unity常用路径

editor

  • dataPath D:/Documents/Xuporter/Assets
  • persistentDataPath C:/Users/Administrator/AppData/LocalLow/mj/path
  • streamingAssetsPath D:/Documents/Xuporter/Assets/StreamingAssets
  • temporaryCachePath C:/Users/ADMINI~1/AppData/Local/Temp/mj/path

android

  • dataPath /data/app/com.mi.path-1.apk 无权限
  • persistentDataPath /data/data/com.mi.path/files 读写,强推荐
  • streamingAssetsPath jar:file:///data/app/com.mi.path-1.apk!/assets 只读
  • temporaryCachePath /data/data/com.mi.path/cache 读写

iphone

  • dataPath /var/mobile/Containers/Bundle/Application/AFE239B4-2FE5-48B5-8A31-FC23FEDA0189/ad.app/Data 无权限
  • persistentDataPath /var/mobile/Containers/Data/Application/FFEEF1E0-E15E-4BC0-9E8F-78084A2085A0/Documents 读写,强推荐
  • streamingAssetsPath /var/mobile/Containers/Bundle/Application/AFE239B4-2FE5-48B5-8A31-FC23FEDA0189/ad.app/Data/Raw 只读
  • temporaryCachePath /var/mobile/Containers/Data/Application/FFEEF1E0-E15E-4BC0-9E8F-78084A2085A0/Library/Caches 读写

Android项目的编译

如果是包含了Android工程就需要进行gradle的修改,在Unity的安装目录下面Editor\Data\PlaybackEngines\AndroidPlayer\Tools\GradleTemplates找到这个目录,这里面包含了gradle的配置,如果需要对gradle进行修改,把相应的拷贝出来放在plugins/Android目录,在编译的时候就会替换默认的gradle文件。 在编译安卓工程的时候,会将主工程编译成一个库,然后launcher项目编译应用程序去包含这个主工程,所以在gradle主要修改就baseProjectTemplate和mainTemplate,安卓工程引用了kotlin,这边需要在baseProjectTemplate中

dependencies {
                classpath   'com.android.tools.build:gradle:3.6.0'
                classpath   "org.jetbrains.kotlin:kotlin-  gradle  -plugin:1.3.71"
              **BUILD_SCRIPT_DEPS**
          }
kolin gradle,然后还需要在maintemplate中添加
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
**APPLY_PLUGINS**
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.71"
**DEPS**}

其实就是把安卓工程中的gradle配置写入到maintemplate.gradle文件中,gradle的修改需要根据项目需求进行修改。 第一种编译方式 以Unity为主,把安卓工程编译成一个jar包,放入到Plugins/Android中,然后通过Unity引擎编译成Apk,File-buildsetting-build 第二种编译方式,以Android为主,勾选BuildSetting 中的ExportProject,然后export,把Unity工程导出,这个操作会把所有的dll 库编译成cpp的代码,把资源编译成android的assets然后再放到android工程进行编译

UI Orientation

一共有五种取值。

  • Portrait:纵向,包括Portrait和Portrait Upside down
  • Landscape:横向,分为Landscape Left和Landscape Right
  • Audo Rotation:自动旋转

Unity编译报错:缺少appcompat类,找不到context compat

https://stackoverflow.com/questions/69021225/resource-linking-fails-on-lstar/69045181#69045181

mainTemplate.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.bytedance.speechengine:speechengine_asr_tob:1.0.6'

只加这个依赖,可能会报错

Execution failed for task ':app:processDevelopmentDebugResources'.

> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
   > Android resource linking failed
.../app/build/intermediates/incremental/mergeDevelopmentDebugResources/merged.dir/values/values.xml:2682: AAPT: error: resource android:attr/lStar not found

launchTemplate.gradle

configurations.all {
  resolutionStrategy {
    force 'androidx.core:core:1.6.0'
    force 'androidx.core:core-ktx:1.6.0'
  }
}

Unity的JNI反射获取函数签名是基于反射

例如load64(android.content.Context,android.content.Context,int ,int ,int ,int ,int)这个签名不能接受application类型的参数。

var getApiFunction = driverClass.CallStatic<long>("load64", application.Call<AndroidJavaObject>("getContext"), packageContext.Call<AndroidJavaObject>("getContext"), 1, 1, 50, 0, 0);

Android移动端脚本

应用程序可以通过 InputHandheld 类来访问 Android 设备的大多数功能。有关更多信息,请参阅:

振动支持

可通过调用 Handheld.Vibrate 来触发振动。不含振动硬件的设备将忽略此调用。

屏幕方向

可在 iOS 和 Android 设备上控制应用程序的屏幕方向。检测方向变化或强制使用特定方向对于创建一些取决于用户如何握持设备的游戏行为很有用。

通过访问 Screen.orientation 属性来获取设备方向。允许的方向如下:

  • Portrait:设备处于纵向模式,直立握持设备,主屏幕按钮位于底部。

  • Portrait Upside down:设备处于纵向模式,但是上下颠倒,直立握持设备,主屏幕按钮位于顶部。

  • LandscapeLeft:设备处于横向模式,直立握持设备,主屏幕按钮位于右侧

  • LandscapeRight:设备处于横向模式,直立握持设备,主屏幕按钮位于左侧

Screen.orientation 设置为上述方向之一,或使用 ScreenOrientation.AutoRotation 来控制屏幕方向。启用自动旋转后,仍可根据具体情况禁用某个方向。

使用以下脚本来控制自动旋转:

创建Android项目编译失败

可能是keystore有损坏,删掉旧版的keystore重新编译即可。

Unity的RuntimeInitializeOnLoadMethodAttribute

Unity提供了一些回调钩子,这些回调的场景如下:

RuntimeInitializeLoadType

  • AfterSceneLoad:场景加载之后

  • BeforeSceneLoad:场景加载之前

  • AfterAssembliesLoaded:

  • BeforeSplashScreen:Splash显示之前

  • SubsystemRegistration:子系统注册的时候的回调

这套回调机制只有两个东西,一个类和一个枚举类型

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Initializing()
{
    ShootsStub.GetInstance();
}

Unity demo停一段时间之后报错

  报错Mesh包含的顶点数过多。一般是因为渲染的东西太多导致的。

  在我的程序中,有一个Text显示Log,这个Text越来越长,导致文本渲染不过来。


C++调用JNI常见套路

在OnLoad的时候把env保存下来,然后getEnv的时候使用AttachCurrentThread。需要注意的是:每个进程都有自己的JNIEnv对象,不能把JNIEnv作为全局变量保存下来,可以把JavaVM这个对象保存下来。

当开发者通过JNI_OnLoad的方式加载so的时候,JNI_OnLoad才会被调用。如果开发者直接把env传了进来,则也能够通过env把JavaVM保存下来。

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  //将vm对象保存下来
  return JNI_VERSION_1_6;
}
JNIEnv*getEnv(){
    JNIEnv* jni_env = 0;
    vm->AttachCurrentThread(&jni_env, 0);
}

通过env获取JavaVM

SYMBOL_HIDDEN void initGlobalData(JNIEnv *env) {
    auto g = getGlobalData();
    if (g->Vm == nullptr) {
        env->GetJavaVM(&(g->Vm));
    }
}

传递activity给C++层

    AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity");    var requestId = CLIB.ppf_InitializeAndroidAsynchronous(appId, activity.GetRawObject(), IntPtr.Zero);

动态库

如果一个so使用了JNI_ON_LOAD

那么加载它的时候,必须使用java的system.loadlibrary加载这个动态链接库,而不能直接使用C++去加载它。JVM虚拟机加载了一个动态链接库,跟C++加载了一个动态链接库,不一样。

在Unity里面使用JVM显式加载一个库。

//AndroidJavaObject system = new AndroidJavaClass("java.lang.System");

// system.CallStatic("loadLibrary", "volcenginertc");

//system.CallStatic("loadLibrary", "byteaudio");

Unity报错:找不到dll

  1. 首先检查dll的属性,是否勾选了android armv7或者armv8等。

  2. 检查apk里面是否有dll,如果没有,说明动态链接库压根没有打包进去

  3. Unity在加载A.dll的过程中,如果发生错误,则直接报A.dll不存在;因此,找不到dll的原因可能是加载dll的时候报错了。

    1. 在JNI_OnLoad等处判断是否有抛出异常

    2. A.dll是否依赖了另一个dll,如果另一个dll缺失,则A.dll加载失败。

Unity调C++时无法传递JNIEnv

https://stackoverflow.com/questions/38862197/getting-valid-jnienv-pointer

只能在C++侧使用JNI_Onload来实现获取JVM。

unity导入package含有dylib

设置/安全与隐私/通用,设置允许xxx。

C++调用C#的方式

Unity支持的Android插件

Unity的Android插件可以通过以下方式提供。

  • AAR:一个压缩包

  • Android Library项目,使用Android Studio创建的一个代码目录。Unity会自动扫描Assets下面的目录,如果一个目录下面有mylibrary.androidlib文件,则Unity将其视为一个AndroidLibrary项目。

  • jar插件:jar插件就是一个jar包,放在Assets目录下任意文件夹即可。

  • 直接把Java源文件放在Plugins里面

C#调用Java

Unity调用JNI的原理:实际上Unity是无法直接调用Java的,它只能直接调用C++。但是在Android平台上调用Java是一个非常强烈的需求。而JNI接口又几乎是不怎么变化的,因此Unity团队把JNI(C++)的头文件用C#封装了一遍,从而可以在C#里面调用JNI。JNI不够好用,又对它使用AndroidJavaObject系列包装了一层。

Unity C#调用Java插件的有三层封装,对应三种级别的API。

第一层:AndroidJNI是调用原始JNI接口的包装器。这个类底层实现是C语言,这个类的所有方法都是静态的,并且与Java原生接口一一对应。可以认为,它就是JNI的原始封装,它所有的方法都是静态方法。

第二层:AndroidJNIHelper是AndroidJNI的一些便利封装。

第三层:AndroidJavaObject和AndroidJavaClass是对AndroidJNI和AndroidJNIHelper的封装。在使用JNI调用时自动执行许多任务,并且使用缓存加快Java的调用速度。AndroidJavaObject对应java.lang.Object,AndroidJavaClass对应java.lang.Class,它们的封装也是一一映射的。它们基本上提供与Java端的三种交互:

  • 调用一个方法

  • 获取字段的值

  • 设置字段的值

这三种交互每种都包含两种调用:操作实例和操作类的静态成员。

总结一下,C#调用Java的三种方式

  • 裸用AndroidJNI,比较底层

  • AndroidJNIHelper+AndroidJNI

  • AndroidJavaObject和AndroidJavaClass,高级API。

实例一:实例化对象

AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); 
  // jni.FindClass("java.lang.String");
  // jni.GetMethodID(classID, "<init>", "(Ljava/lang/String;)V");
  // jni.NewStringUTF("some_string");
  // jni.NewObject(classID, methodID, javaString);
  int hash = jo.Call<int>("hashCode"); 
  // jni.GetMethodID(classID, "hashCode", "()I");
  // jni.CallIntMethod(objectID, methodID);

实例化内部类的时候,使用$

内部类必须使用 $ 分隔符。请使用 android.view.ViewGroup$LayoutParams

实例二:获取应用程序的缓存目录

AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); 
 // jni.FindClass("com.unity3d.player.UnityPlayer"); 
 AndroidJavaObject jo = jc.GetStatic <AndroidJavaObject>("currentActivity"); 
 // jni.GetStaticFieldID(classID, "Ljava/lang/Object;");  
 // jni.GetStaticObjectField(classID, fieldID); 
 // jni.FindClass("java.lang.Object"); 

 Debug.Log(jo.Call <AndroidJavaObject>("getCacheDir").Call<string>("getCanonicalPath")); 
 // jni.GetMethodID(classID, "getCacheDir", "()Ljava/io/File;"); // 或其任何基类!
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.FindClass("java.io.File"); 
 // jni.GetMethodID(classID, "getCanonicalPath", "()Ljava/lang/String;"); 
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.GetStringUTFChars(javaString);

实例三:Java将数据传递到Unity

UnityActivity有一个UnitySendMessage方法,用于将消息发送给Unity。

UnitySendMessage(gameObjectName,gameObjectCallbackFunctionName,realMessage)

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour { 

    void Start () { 
        AndroidJNIHelper.debug = true; 
        using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { 
        jc.CallStatic("UnitySendMessage", "Main Camera", "JavaMessage", "NewMessage");
        } 
    } 

    void JavaMessage(string message) { 
        Debug.Log("message from java: " + message); 
    }
}

实例四:尽量使用using来管理AndroidJavaClass和AndroidJavaObject的生命周期。

如果不使用using,则回收AndroidJavaClass、AndroidJavaObject的时候自动释放。

如果将 AndroidJNIHelper.debug 设置为 true,您将在调试输出中看到垃圾回收器的活动记录。

//安全地获取系统语言
void Start () { 
    using (AndroidJavaClass cls = new AndroidJavaClass("java.util.Locale")) { 
        using(AndroidJavaObject locale = cls.CallStatic<AndroidJavaObject>("getDefault")) { 
            Debug.Log("current lang = " + locale.Call<string>("getDisplayLanguage")); 

        } 
    } 
}

实例五:liangdong的例子,isLogin(activity)

Java侧

    
    public void GetUserInfo()
    {
        AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject activityObject = jc.GetStatic<AndroidJavaObject>("currentActivity");
        Debug.Log("Login  update activityObject " + activityObject);

        AndroidJavaClass jc2 = new AndroidJavaClass("com.bytedance.picovr.sdk.usercenter.utils.UserCenterUtils");
        Boolean isLogin = jc2.CallStatic<Boolean>("isLogin", activityObject);
        Debug.Log("Login  update isLogin " + isLogin);
    }

C#侧


    public class LoginCallBack : MonoBehaviour
    {

        public void LoginSuccess(string loginInfo)
        {
            Debug.Log("LoginCallBack LoginSuccess :" + loginInfo);
            Text PhoneNumtext = GameObject.Find("PhoneNumText").GetComponent<Text>();
            Text GenderText = GameObject.Find("GenderText").GetComponent<Text>();
            Text UniqidText = GameObject.Find("UniqidText").GetComponent<Text>();

            if (loginInfo != null)
            {
                JsonData jsrr = JsonMapper.ToObject(loginInfo);
                jsrr["phone"].ToString();
                PhoneNumtext.text = "用户手机号码:" + jsrr["phone"].ToString();
                GenderText.text = "用户性别:" + jsrr["gender"].ToString();
                UniqidText.text = "用户ID:" + jsrr["uniqid"].ToString();
            } else {

                PhoneNumtext.text = "用户未登录";
            }
        }

        public void UnLogin(string message) {
            Text PhoneNumtext = GameObject.Find("PhoneNumText").GetComponent<Text>();
            PhoneNumtext.text = "用户未登录";
        }

    }

实例六:XR旧代码中的支付和登录

登录支付部分的代码,它会通过调用Java的方式来完成一些动作。

// Copyright © 2015-2021 Pico Technology Co., Ltd. All Rights Reserved.

#if !UNITY_EDITOR
#if UNITY_ANDROID
#define ANDROID_DEVICE
#elif UNITY_IPHONE
#define IOS_DEVICE
#elif UNITY_STANDALONE_WIN
#define WIN_DEVICE
#endif
#endif

using UnityEngine;

namespace Unity.XR.PXR
{
    public class PicoPaymentSDK
    {
        private static AndroidJavaObject _jo = new AndroidJavaObject("com.pico.loginpaysdk.UnityInterface");

        public static AndroidJavaObject jo
        {
            get { return _jo; }
            set { _jo = value; }
        }

        public static void Login()
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("authSSO");


        }

        public static void Pay(string payOrderJson)
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("pay", payOrderJson);

        }

        public static void QueryOrder(string orderId)
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("queryOrder", orderId);

        }

        public static void GetUserAPI()
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("getUserAPI");
        }
    }
}
AndroidJavaObject jo = new AndroidJavaObject("com.pico.loginpaysdk.UnityInterface");
public static void Login(){
    ...
    jo.Call("init", mJo);
    jo.Call("authSSO");
    ...
}


public static void Pay(string payOrderJson){
    ...
    jo.Call("init", mJo);
    jo.Call("pay", payOrderJson);
    ...
}

实例七:

string packageName = "com.unity3d.player.UnityPlayer";
              if (unityAcvity == null)
              {
                  unityAcvity = new AndroidJavaClass(packageName).GetStatic<AndroidJavaObject>("currentActivity");
              }
  //直接获取主Activity的类名
  AndroidJavaObject a = new AndroidJavaObject("packagename.classname");
  //获取指定包名下面的类
  classname.Call<returntype>(string methodname,params object[] args);
  // 通过AndroidJavaObject 中封装的方法获取调用类中的方法,同时传递参数,并获得返回类型
  这边只是其中的一种调用,在Api中还包含其他很多种可自行查阅

Java调用C#

Java要想调用C#,就需要引入Unity的clasess.jar,这样Java调用C#的时候才能找到一些关键类。

在项目的libs目录中添加Unity 安装目录中的classes.jar(该jar文件中的UnityPlayer类中的UnitySendMessage方法可以实现Java方法调用Unity GameObject上绑定的C#脚本中的方法)。同时在build.gradle文件中的dependencies添加compileOnly files('libs/classes.jar') 。

此处compileOnly表示只用于编译,不要把classes.jar打包进aar里面去。

Java调用C#有两种方式:

  1. UnitySendMessage:直接根据GameObject的名称找到GameObject,调用GameObject的某个函数。

  2. 定义一个C#类,定义一个Java类,直接调用Unity

UnitySendMessage

UnityPlayer.UnitySendMessage(String GameObjectName, String MethodName, String param)

这个函数只能向C#侧的某个GameObject发送消息,并且函数参数只能是string类型,C#侧可能需要解析string类型的消息。

// 需要先引入Unity的Jar包
import com.unity3d.player.UnityPlayer;
// 调用 object上 C# 脚本中的方法
UnityPlayer.UnitySendMessage("objectName", "methodName", "data");

示例一:loginpay中的实现

Android端,调用Java 方法进行数据处理 , 在回调中通知Unity端

public void init(Activity activity) {
    // 初始化
    ...
}

// 授权登录    
public void authSSO(){
   // 调用Java登录入口
   Login mLogin = new Login();
   mLogin.login(new Callback(){
       public void loginCallback(boolean isSuccess,String mesage){
           ...
           // 向 Unity 发送结果
           UnityPlayer.UnitySendMessage("PicoPayment", "LoginCallback", message);
           ...
       }
   });
}
// 支付
public void pay(String payOrderJson) {
    // 调用Java支付入口
    PayOrder order = PayOrder.parse(payOrderJson);
    PicoPay.getInstance(mUnityPlayerActivity).pay(order, new PaySDKCallBack(){
        @Override
        public void callback(String code, String msg) {
            ...
            // 向 Unity 发送结果
            UnityPlayer.UnitySendMessage("PicoPayment", "QueryOrPayCallback", msg);
            ...
        }
        ...
        ...
        
    });
}

示例二:Android改变画布颜色

Unity添加一个MonoBehavior

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UnityAndroidTest: MonoBehaviour
{   
    // 0. 该成员就是 Canvas 内部的 Text 控件.
    public Text text1;

    void Start()
    {
        // 1. 页面初始化时,初始化 Android 通信对象
        AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");

        // 2.定义一个int变量
        int res = 0;

        try
        {
            // 3.调用Android平台的 increment 自增方法,该方法会返回一个 int 值
            res = jo.Call<int>("increment", 2);
            // 4.将返回值更新到 Text 上
            text1.text = res.ToString();
        }
        catch (Exception e)
        {
            text1.text = "error";
        }
    }

    void Update()
    {
        
    }

    // 5.这里我们向Android平台暴露一个方法,调用后可以改变Text的颜色
    public void ChangeColor()
    {
        text1.color = Color.red;
    }
}

Android调用Unity

public class UnityPlayerActivity extends Activity implements IUnityPlayerLifecycleEvents {
  // ...  
  // 省略其它代码
  // ...
  
  private int count = 1;
  
  // 1.Unity项目中的 UnityAndroidTest.cs 脚本,会主动唤起该方法
  public int increment(int value) {
      count += value;
      // 2.在这个方法中,我们通过发送消息,让 Canvas 执行 ChangeColor 方法
      //   将文字从黑色变成红色
      UnityPlayer.UnitySendMessage("Canvas", "ChangeColor", "");
      
      // 3.最终,将 int 类型的 count = 3 作为桥接方法的返回值返回
      return count;
  }
}

示例三:UnitySendMessageExtension

UnityPlayer.UnitySendMessage只能用来Java向C#发送一个通知,无法得到C#侧的Response。

基于UnitySendMessage可以实现带返回值的调用,原理就是C#侧主动把返回值推送给Java侧。

UnitySendMessage是一个同步函数,会等待C#侧函数执行结束。

Java侧的封装

public final class MyPlugin
{
    //Make class static variable so that the callback function is sent to one instance of this class.public static MyPlugin testInstance;

     public static MyPlugin instance()
     {
         if(testInstance == null)
         {
             testInstance = new MyPlugin();
         }
         return testInstance;
     }

    string result = "";


    public string UnitySendMessageExtension(string gameObject, string functionName, string funcParam)
    {
        UnityPlayer.UnitySendMessage(gameObject, functionName, funcParam);
        string tempResult = result;
        return tempResult;
    }

    //Receives result from C# and saves it to result  variablevoid receiveResult(string value)
    {
        result = "";//Clear old data
        result = value; //Get new one
    }
}

C#侧的配合

class TestScript: MonoBehaviour
{
    //Sends the data from PlayerPrefs to the receiveResult function in Javavoid sendResultToJava(float value)
    {
        using(AndroidJavaClass javaPlugin = new AndroidJavaClass("com.company.product.MyPlugin"))
        {
             AndroidJavaObject pluginInstance = javaPlugin.CallStatic("instance");
             pluginInstance.Call("receiveResult",value.ToString());
        }
    }

    //Called from Java to get the saved PlayerPrefsvoid testFunction(string key)
    {
        float value = PlayerPrefs.GetFloat(key) //Get the saved value from keysendResultToJava(value); //Send the value to Java
    }
}

在Java侧使用带返回值的UnitySendMessage

String str = UnitySendMessageExtension("ProfileLoad", "testFunction","highScore");

使用类

Unity
public class A :AndroidJavaProxy
{
    private const string interfaceName = "packageName.IB";
    public A() : base(interfaceName)
    {
    }
    func()
    {
    }
}

Android
public interface IB
{
    func();
}
public void setA(AscanMatcher) {
    func();
}
利用接口的方式,做回调函数。


Unity中C#与Java高频互调产生ANR

现象

ANR:Application Not Respond,应用无响应。与stackOverlow、segmentFault、nullPointerReference等著名错误类似,是一种错误的程序状态。

Unity中,C#与Java层的互相调用一般情况下没问题,但是在高频、互调的情况下会产生死锁,进而导致ANR问题。

首先,如果程序只涉及到C#调用Java,因为这个调用发生在主线程中,所以不会发生死锁问题。

其次,如果程序只是低频的出现Java调用C#,那么发生死锁的概率极低。

最后,如果程序高频出现C#调用Java和Java调用C#,那么出现死锁的概率就挺大。

C#调用Java一般都是在主线程中进行,Java调用C#则有可能是其它的线程。

当Java调用C#的时候,有一个加锁语句。如果C#和Java互调的时候,线程出现竞争,导致了死锁,那么就会出现程序卡住的情况。

解决方案

ANR产生的根本原因在于线程死锁。解决思路就是避免从Java侧主动调用C#。

实现方式是:保证Java调用C#的时候,统一收敛到UnityMain线程去执行。

在Java侧维护一个队列,Java想调用C#的时候,往Java的任务队列里面塞任务即可。

在C#侧创建一个MonoBehavior,每一帧调用Java,Java受到调用的时候,从队列里面弹出一个Runnable并执行之。

本文中第二部分介绍了很多Java调用C#的内容,其实这一部分内容是很危险的,只能在主线程中使用,不然就容易出现死锁。

需要封装的函数:

ppfUserHandle ppf_UserArray_GetElement(const ppfUserArrayHandle obj , size_t index);

分析

size_t是C++中的数据类型,在32位平台上是uint32,在64位平台上是uint64。

size_t映射到C#的备选项有:

  • size_t=>ulong

  • size_t=>uint

  • size_t=>UIntPtr:跟随系统,与size_t类型最相匹配。

如果使用方案一:在unity32位打包的时候,就会把C#的ulong传递给C++的size_t,这就是64位转32位,会出错。

如果使用方案二:C#侧会受到一些限制,C#的可用数值范围变小了。但是这种方法在数值较小的情况下,不会有什么问题。

如果使用方案三:一切完美解决。

关键在于C#的封装:

Oculus把所有的size_t转成了UIntPtr,所以就自动避免了这个问题。


[DllImport(*DLL_NAME*, CallingConvention = CallingConvention.*Cdecl*)]
public static extern IntPtr *ovr_UserArray_GetElement*(IntPtr obj, UIntPtr index);

解决方案

  1. 因为在64位下面总是正常的,所以只需要考虑兼容32位。把所有出现size_t的地方都使用UIntPtr替代。

https://stackoverflow.com/questions/32906774/what-is-equal-to-the-c-size-t-in-c-sharp

目录

解压apk之后的目录结构

.
├── AndroidManifest.xml
├── apktool.yml
├── assets
│   ├── bin
│   │   └── Data
│   │       ├── Managed
│   │       │   ├── Metadata
│   │       │   │   └── global-metadata.dat
│   │       │   ├── Resources
│   │       │   │   └── mscorlib.dll-resources.dat
│   │       │   └── etc
│   │       │       └── mono
│   │       │           ├── 2.0
│   │       │           │   ├── Browsers
│   │       │           │   │   └── Compat.browser
│   │       │           │   ├── DefaultWsdlHelpGenerator.aspx
│   │       │           │   ├── machine.config
│   │       │           │   ├── settings.map
│   │       │           │   └── web.config
│   │       │           ├── 4.0
│   │       │           │   ├── Browsers
│   │       │           │   │   └── Compat.browser
│   │       │           │   ├── DefaultWsdlHelpGenerator.aspx
│   │       │           │   ├── machine.config
│   │       │           │   ├── settings.map
│   │       │           │   └── web.config
│   │       │           ├── 4.5
│   │       │           │   ├── Browsers
│   │       │           │   │   └── Compat.browser
│   │       │           │   ├── DefaultWsdlHelpGenerator.aspx
│   │       │           │   ├── machine.config
│   │       │           │   ├── settings.map
│   │       │           │   └── web.config
│   │       │           ├── browscap.ini
│   │       │           ├── config
│   │       │           └── mconfig
│   │       │               └── config.xml
│   │       ├── UnitySubsystems
│   │       │   └── PxrPlatform
│   │       │       └── UnitySubsystemsManifest.json
│   │       ├── boot.config
│   │       ├── data.unity3d
│   │       ├── sharedassets0.resource
│   │       └── unity\ default\ resources
│   └── res.json
├── lib
│   └── arm64-v8a
│       ├── libPicoAmbisonicDecoder.so
│       ├── libPicoSpatializer.so
│       ├── libPxrPlatform.so
│       ├── libil2cpp.so
│       ├── libmain.so
│       ├── libpxr_api.so
│       ├── libpxrplatformloader.so
│       └── libunity.so
├── original
│   ├── AndroidManifest.xml
│   └── META-INF
│       ├── CERT.RSA
│       ├── CERT.SF
│       └── MANIFEST.MF
├── res
│   ├── mipmap-anydpi
│   │   ├── app_icon.xml
│   │   └── app_icon_round.xml
│   ├── mipmap-mdpi
│   │   ├── app_icon.png
│   │   ├── ic_launcher_background.png
│   │   └── ic_launcher_foreground.png
│   ├── values
│   │   ├── ids.xml
│   │   ├── public.xml
│   │   ├── strings.xml
│   │   └── styles.xml
│   └── values-v30
│       └── strings.xml
└── smali
    ├── bitter
    │   └── jnibridge
    │       ├── JNIBridge$a.smali
    │       └── JNIBridge.smali
    ├── com
    │   ├── bytedance
    │   │   └── learPicoXr
    │   │       ├── BuildConfig.smali
    │   │       ├── R$id.smali
    │   │       ├── R$mipmap.smali
    │   │       ├── R$string.smali
    │   │       ├── R$style.smali
    │   │       └── R.smali
    │   ├── example
    │   │   └── tobserviceclient
    │   │       └── MainActivity.smali
    │   ├── google
    │   │   ├── androidgamesdk
    │   │   │   ├── ChoreographerCallback$1.smali
    │   │   │   ├── ChoreographerCallback$a.smali
    │   │   │   ├── ChoreographerCallback.smali
    │   │   │   ├── SwappyDisplayManager$1.smali
    │   │   │   ├── SwappyDisplayManager$a.smali
    │   │   │   └── SwappyDisplayManager.smali
    │   │   └── gson
    │   │       ├── DefaultDateTypeAdapter.smali
    │   │       ├── ExclusionStrategy.smali
    │   │       ├── FieldAttributes.smali
    │   │       ├── FieldNamingPolicy$1.smali
    │   │       ├── FieldNamingPolicy$2.smali
    │   │       ├── FieldNamingPolicy$3.smali
    │   │       ├── FieldNamingPolicy$4.smali
    │   │       ├── FieldNamingPolicy$5.smali
    │   │       ├── FieldNamingPolicy.smali
    │   │       ├── FieldNamingStrategy.smali
    │   │       ├── Gson$1.smali
    │   │       ├── Gson$2.smali
    │   │       ├── Gson$3.smali
    │   │       ├── Gson$4.smali
    │   │       ├── Gson$5.smali
    │   │       ├── Gson$6.smali
    │   │       ├── Gson$FutureTypeAdapter.smali
    │   │       ├── Gson.smali
    │   │       ├── GsonBuilder.smali
    │   │       ├── InstanceCreator.smali
    │   │       ├── JsonArray.smali
    │   │       ├── JsonDeserializationContext.smali
    │   │       ├── JsonDeserializer.smali
    │   │       ├── JsonElement.smali
    │   │       ├── JsonIOException.smali
    │   │       ├── JsonNull.smali
    │   │       ├── JsonObject.smali
    │   │       ├── JsonParseException.smali
    │   │       ├── JsonParser.smali
    │   │       ├── JsonPrimitive.smali
    │   │       ├── JsonSerializationContext.smali
    │   │       ├── JsonSerializer.smali
    │   │       ├── JsonStreamParser.smali
    │   │       ├── JsonSyntaxException.smali
    │   │       ├── LongSerializationPolicy$1.smali
    │   │       ├── LongSerializationPolicy$2.smali
    │   │       ├── LongSerializationPolicy.smali
    │   │       ├── TypeAdapter$1.smali
    │   │       ├── TypeAdapter.smali
    │   │       ├── TypeAdapterFactory.smali
    │   │       ├── annotations
    │   │       │   ├── Expose.smali
    │   │       │   ├── JsonAdapter.smali
    │   │       │   ├── SerializedName.smali
    │   │       │   ├── Since.smali
    │   │       │   └── Until.smali
    │   │       ├── internal
    │   │       │   ├── $Gson$Preconditions.smali
    │   │       │   ├── $Gson$Types$GenericArrayTypeImpl.smali
    │   │       │   ├── $Gson$Types$ParameterizedTypeImpl.smali
    │   │       │   ├── $Gson$Types$WildcardTypeImpl.smali
    │   │       │   ├── $Gson$Types.smali
    │   │       │   ├── ConstructorConstructor$1.smali
    │   │       │   ├── ConstructorConstructor$10.smali
    │   │       │   ├── ConstructorConstructor$11.smali
    │   │       │   ├── ConstructorConstructor$12.smali
    │   │       │   ├── ConstructorConstructor$13.smali
    │   │       │   ├── ConstructorConstructor$14.smali
    │   │       │   ├── ConstructorConstructor$2.smali
    │   │       │   ├── ConstructorConstructor$3.smali
    │   │       │   ├── ConstructorConstructor$4.smali
    │   │       │   ├── ConstructorConstructor$5.smali
    │   │       │   ├── ConstructorConstructor$6.smali
    │   │       │   ├── ConstructorConstructor$7.smali
    │   │       │   ├── ConstructorConstructor$8.smali
    │   │       │   ├── ConstructorConstructor$9.smali
    │   │       │   ├── ConstructorConstructor.smali
    │   │       │   ├── Excluder$1.smali
    │   │       │   ├── Excluder.smali
    │   │       │   ├── JsonReaderInternalAccess.smali
    │   │       │   ├── LazilyParsedNumber.smali
    │   │       │   ├── LinkedHashTreeMap$1.smali
    │   │       │   ├── LinkedHashTreeMap$AvlBuilder.smali
    │   │       │   ├── LinkedHashTreeMap$AvlIterator.smali
    │   │       │   ├── LinkedHashTreeMap$EntrySet$1.smali
    │   │       │   ├── LinkedHashTreeMap$EntrySet.smali
    │   │       │   ├── LinkedHashTreeMap$KeySet$1.smali
    │   │       │   ├── LinkedHashTreeMap$KeySet.smali
    │   │       │   ├── LinkedHashTreeMap$LinkedTreeMapIterator.smali
    │   │       │   ├── LinkedHashTreeMap$Node.smali
    │   │       │   ├── LinkedHashTreeMap.smali
    │   │       │   ├── LinkedTreeMap$1.smali
    │   │       │   ├── LinkedTreeMap$EntrySet$1.smali
    │   │       │   ├── LinkedTreeMap$EntrySet.smali
    │   │       │   ├── LinkedTreeMap$KeySet$1.smali
    │   │       │   ├── LinkedTreeMap$KeySet.smali
    │   │       │   ├── LinkedTreeMap$LinkedTreeMapIterator.smali
    │   │       │   ├── LinkedTreeMap$Node.smali
    │   │       │   ├── LinkedTreeMap.smali
    │   │       │   ├── ObjectConstructor.smali
    │   │       │   ├── Primitives.smali
    │   │       │   ├── Streams$AppendableWriter$CurrentWrite.smali
    │   │       │   ├── Streams$AppendableWriter.smali
    │   │       │   ├── Streams.smali
    │   │       │   ├── UnsafeAllocator$1.smali
    │   │       │   ├── UnsafeAllocator$2.smali
    │   │       │   ├── UnsafeAllocator$3.smali
    │   │       │   ├── UnsafeAllocator$4.smali
    │   │       │   ├── UnsafeAllocator.smali
    │   │       │   └── bind
    │   │       │       ├── ArrayTypeAdapter$1.smali
    │   │       │       ├── ArrayTypeAdapter.smali
    │   │       │       ├── CollectionTypeAdapterFactory$Adapter.smali
    │   │       │       ├── CollectionTypeAdapterFactory.smali
    │   │       │       ├── DateTypeAdapter$1.smali
    │   │       │       ├── DateTypeAdapter.smali
    │   │       │       ├── JsonAdapterAnnotationTypeAdapterFactory.smali
    │   │       │       ├── JsonTreeReader$1.smali
    │   │       │       ├── JsonTreeReader.smali
    │   │       │       ├── JsonTreeWriter$1.smali
    │   │       │       ├── JsonTreeWriter.smali
    │   │       │       ├── MapTypeAdapterFactory$Adapter.smali
    │   │       │       ├── MapTypeAdapterFactory.smali
    │   │       │       ├── ObjectTypeAdapter$1.smali
    │   │       │       ├── ObjectTypeAdapter$2.smali
    │   │       │       ├── ObjectTypeAdapter.smali
    │   │       │       ├── ReflectiveTypeAdapterFactory$1.smali
    │   │       │       ├── ReflectiveTypeAdapterFactory$Adapter.smali
    │   │       │       ├── ReflectiveTypeAdapterFactory$BoundField.smali
    │   │       │       ├── ReflectiveTypeAdapterFactory.smali
    │   │       │       ├── SqlDateTypeAdapter$1.smali
    │   │       │       ├── SqlDateTypeAdapter.smali
    │   │       │       ├── TimeTypeAdapter$1.smali
    │   │       │       ├── TimeTypeAdapter.smali
    │   │       │       ├── TreeTypeAdapter$1.smali
    │   │       │       ├── TreeTypeAdapter$GsonContextImpl.smali
    │   │       │       ├── TreeTypeAdapter$SingleTypeFactory.smali
    │   │       │       ├── TreeTypeAdapter.smali
    │   │       │       ├── TypeAdapterRuntimeTypeWrapper.smali
    │   │       │       ├── TypeAdapters$1.smali
    │   │       │       ├── TypeAdapters$10.smali
    │   │       │       ├── TypeAdapters$11.smali
    │   │       │       ├── TypeAdapters$12.smali
    │   │       │       ├── TypeAdapters$13.smali
    │   │       │       ├── TypeAdapters$14.smali
    │   │       │       ├── TypeAdapters$15.smali
    │   │       │       ├── TypeAdapters$16.smali
    │   │       │       ├── TypeAdapters$17.smali
    │   │       │       ├── TypeAdapters$18.smali
    │   │       │       ├── TypeAdapters$19.smali
    │   │       │       ├── TypeAdapters$2.smali
    │   │       │       ├── TypeAdapters$20.smali
    │   │       │       ├── TypeAdapters$21.smali
    │   │       │       ├── TypeAdapters$22.smali
    │   │       │       ├── TypeAdapters$23.smali
    │   │       │       ├── TypeAdapters$24.smali
    │   │       │       ├── TypeAdapters$25.smali
    │   │       │       ├── TypeAdapters$26$1.smali
    │   │       │       ├── TypeAdapters$26.smali
    │   │       │       ├── TypeAdapters$27.smali
    │   │       │       ├── TypeAdapters$28.smali
    │   │       │       ├── TypeAdapters$29.smali
    │   │       │       ├── TypeAdapters$3.smali
    │   │       │       ├── TypeAdapters$30.smali
    │   │       │       ├── TypeAdapters$31.smali
    │   │       │       ├── TypeAdapters$32.smali
    │   │       │       ├── TypeAdapters$33.smali
    │   │       │       ├── TypeAdapters$34.smali
    │   │       │       ├── TypeAdapters$35$1.smali
    │   │       │       ├── TypeAdapters$35.smali
    │   │       │       ├── TypeAdapters$36.smali
    │   │       │       ├── TypeAdapters$4.smali
    │   │       │       ├── TypeAdapters$5.smali
    │   │       │       ├── TypeAdapters$6.smali
    │   │       │       ├── TypeAdapters$7.smali
    │   │       │       ├── TypeAdapters$8.smali
    │   │       │       ├── TypeAdapters$9.smali
    │   │       │       ├── TypeAdapters$EnumTypeAdapter.smali
    │   │       │       ├── TypeAdapters.smali
    │   │       │       └── util
    │   │       │           └── ISO8601Utils.smali
    │   │       ├── reflect
    │   │       │   └── TypeToken.smali
    │   │       └── stream
    │   │           ├── JsonReader$1.smali
    │   │           ├── JsonReader.smali
    │   │           ├── JsonScope.smali
    │   │           ├── JsonToken.smali
    │   │           ├── JsonWriter.smali
    │   │           └── MalformedJsonException.smali
    │   ├── pico
    │   │   └── pbslibrary
    │   │       └── system_attribute
    │   │           ├── IBrightnessManager$Stub$Proxy.smali
    │   │           ├── IBrightnessManager$Stub.smali
    │   │           └── IBrightnessManager.smali
    │   ├── picovr
    │   │   └── picovrlib
    │   │       └── R.smali
    │   ├── psmart
    │   │   └── aosoperation
    │   │       ├── AudioReceiver.smali
    │   │       ├── BatteryReceiver.smali
    │   │       ├── BrightNessReceiver$1.smali
    │   │       ├── BrightNessReceiver.smali
    │   │       ├── MRCCalibration.smali
    │   │       ├── NativeVerfyInterface.smali
    │   │       ├── SysActivity$1.smali
    │   │       ├── SysActivity$2.smali
    │   │       ├── SysActivity$3.smali
    │   │       ├── SysActivity.smali
    │   │       ├── VRResUtils$ResID$ID_BOUNDARY_DIALOG.smali
    │   │       ├── VRResUtils$ResID$ID_LGP_MAIN_SCREEN$IMAGE.smali
    │   │       ├── VRResUtils$ResID$ID_LGP_MAIN_SCREEN$TEXT.smali
    │   │       ├── VRResUtils$ResID$ID_LGP_MAIN_SCREEN.smali
    │   │       ├── VRResUtils$ResID$ID_LGP_PERMISSION_TOAST.smali
    │   │       ├── VRResUtils$ResID.smali
    │   │       ├── VRResUtils.smali
    │   │       ├── VerifyTool$1.smali
    │   │       ├── VerifyTool$2.smali
    │   │       ├── VerifyTool$MyHandler.smali
    │   │       └── VerifyTool.smali
    │   ├── pvr
    │   │   ├── Constants.smali
    │   │   ├── Country.smali
    │   │   ├── IPvrCallback$Stub$Proxy.smali
    │   │   ├── IPvrCallback$Stub.smali
    │   │   ├── IPvrCallback.smali
    │   │   ├── IPvrManagerService$Stub$Proxy.smali
    │   │   ├── IPvrManagerService$Stub.smali
    │   │   ├── IPvrManagerService.smali
    │   │   ├── PvrCallback.smali
    │   │   ├── PvrManager.smali
    │   │   ├── PvrManagerInternal$PvrCallBackTransport.smali
    │   │   ├── PvrManagerInternal.smali
    │   │   ├── PvrXmlUtils.smali
    │   │   ├── tobservice
    │   │   │   ├── ToBServiceHelper$1.smali
    │   │   │   ├── ToBServiceHelper$10.smali
    │   │   │   ├── ToBServiceHelper$11.smali
    │   │   │   ├── ToBServiceHelper$12.smali
    │   │   │   ├── ToBServiceHelper$13.smali
    │   │   │   ├── ToBServiceHelper$14.smali
    │   │   │   ├── ToBServiceHelper$15.smali
    │   │   │   ├── ToBServiceHelper$16.smali
    │   │   │   ├── ToBServiceHelper$17.smali
    │   │   │   ├── ToBServiceHelper$18.smali
    │   │   │   ├── ToBServiceHelper$19.smali
    │   │   │   ├── ToBServiceHelper$2.smali
    │   │   │   ├── ToBServiceHelper$20.smali
    │   │   │   ├── ToBServiceHelper$21.smali
    │   │   │   ├── ToBServiceHelper$22.smali
    │   │   │   ├── ToBServiceHelper$23.smali
    │   │   │   ├── ToBServiceHelper$3.smali
    │   │   │   ├── ToBServiceHelper$4.smali
    │   │   │   ├── ToBServiceHelper$5.smali
    │   │   │   ├── ToBServiceHelper$6.smali
    │   │   │   ├── ToBServiceHelper$7.smali
    │   │   │   ├── ToBServiceHelper$8.smali
    │   │   │   ├── ToBServiceHelper$9.smali
    │   │   │   ├── ToBServiceHelper.smali
    │   │   │   ├── enums
    │   │   │   │   ├── PBS_ControllerKeyEnum$1.smali
    │   │   │   │   ├── PBS_ControllerKeyEnum.smali
    │   │   │   │   ├── PBS_DeviceControlEnum$1.smali
    │   │   │   │   ├── PBS_DeviceControlEnum.smali
    │   │   │   │   ├── PBS_HomeEventEnum$1.smali
    │   │   │   │   ├── PBS_HomeEventEnum.smali
    │   │   │   │   ├── PBS_HomeFunctionEnum$1.smali
    │   │   │   │   ├── PBS_HomeFunctionEnum.smali
    │   │   │   │   ├── PBS_PackageControlEnum$1.smali
    │   │   │   │   ├── PBS_PackageControlEnum.smali
    │   │   │   │   ├── PBS_PowerOnOffLogoEnum$1.smali
    │   │   │   │   ├── PBS_PowerOnOffLogoEnum.smali
    │   │   │   │   ├── PBS_ScreenOffDelayTimeEnum$1.smali
    │   │   │   │   ├── PBS_ScreenOffDelayTimeEnum.smali
    │   │   │   │   ├── PBS_SleepDelayTimeEnum$1.smali
    │   │   │   │   ├── PBS_SleepDelayTimeEnum.smali
    │   │   │   │   ├── PBS_StartVRSettingsEnum$1.smali
    │   │   │   │   ├── PBS_StartVRSettingsEnum.smali
    │   │   │   │   ├── PBS_SwitchEnum$1.smali
    │   │   │   │   ├── PBS_SwitchEnum.smali
    │   │   │   │   ├── PBS_SystemFunctionSwitchEnum$1.smali
    │   │   │   │   ├── PBS_SystemFunctionSwitchEnum.smali
    │   │   │   │   ├── PBS_SystemInfoEnum$1.smali
    │   │   │   │   ├── PBS_SystemInfoEnum.smali
    │   │   │   │   ├── PBS_USBConfigModeEnum$1.smali
    │   │   │   │   ├── PBS_USBConfigModeEnum.smali
    │   │   │   │   ├── PBS_VideoPlayMode$1.smali
    │   │   │   │   ├── PBS_VideoPlayMode.smali
    │   │   │   │   ├── PBS_WifiDisplayModel$1.smali
    │   │   │   │   └── PBS_WifiDisplayModel.smali
    │   │   │   └── interfaces
    │   │   │       ├── IBoolCallback$Stub$Proxy.smali
    │   │   │       ├── IBoolCallback$Stub.smali
    │   │   │       ├── IBoolCallback.smali
    │   │   │       ├── IIntCallback$Stub$Proxy.smali
    │   │   │       ├── IIntCallback$Stub.smali
    │   │   │       ├── IIntCallback.smali
    │   │   │       ├── ILongCallback$Stub$Proxy.smali
    │   │   │       ├── ILongCallback$Stub.smali
    │   │   │       ├── ILongCallback.smali
    │   │   │       ├── IPBS.smali
    │   │   │       ├── ISensorCallback$Stub$Proxy.smali
    │   │   │       ├── ISensorCallback$Stub.smali
    │   │   │       ├── ISensorCallback.smali
    │   │   │       ├── IStringCallback$Stub$Proxy.smali
    │   │   │       ├── IStringCallback$Stub.smali
    │   │   │       ├── IStringCallback.smali
    │   │   │       ├── IToBService$Stub$Proxy.smali
    │   │   │       ├── IToBService$Stub.smali
    │   │   │       ├── IToBService.smali
    │   │   │       ├── IWDJsonCallback$Stub$Proxy.smali
    │   │   │       ├── IWDJsonCallback$Stub.smali
    │   │   │       ├── IWDJsonCallback.smali
    │   │   │       ├── IWDModelsCallback$Stub$Proxy.smali
    │   │   │       ├── IWDModelsCallback$Stub.smali
    │   │   │       ├── IWDModelsCallback.smali
    │   │   │       ├── IWIFIManager$Stub$Proxy.smali
    │   │   │       ├── IWIFIManager$Stub.smali
    │   │   │       └── IWIFIManager.smali
    │   │   └── verify
    │   │       ├── ICallback$Default.smali
    │   │       ├── ICallback$Stub$Proxy.smali
    │   │       ├── ICallback$Stub.smali
    │   │       ├── ICallback.smali
    │   │       ├── IVerify$Default.smali
    │   │       ├── IVerify$Stub$Proxy.smali
    │   │       ├── IVerify$Stub.smali
    │   │       └── IVerify.smali
    │   ├── pxr
    │   │   └── xrlib
    │   │       ├── BuildConfig.smali
    │   │       ├── PicovrSDK.smali
    │   │       └── R.smali
    │   └── unity3d
    │       └── player
    │           ├── AudioVolumeHandler.smali
    │           ├── BuildConfig.smali
    │           ├── Camera2Wrapper.smali
    │           ├── GoogleARCoreApi.smali
    │           ├── GoogleVrApi.smali
    │           ├── GoogleVrProxy$1.smali
    │           ├── GoogleVrProxy$2.smali
    │           ├── GoogleVrProxy$3.smali
    │           ├── GoogleVrProxy$4.smali
    │           ├── GoogleVrProxy$a.smali
    │           ├── GoogleVrProxy.smali
    │           ├── GoogleVrVideo$GoogleVrVideoCallbacks.smali
    │           ├── GoogleVrVideo.smali
    │           ├── HFPStatus$1.smali
    │           ├── HFPStatus$a.smali
    │           ├── HFPStatus.smali
    │           ├── IAssetPackManagerDownloadStatusCallback.smali
    │           ├── IAssetPackManagerMobileDataConfirmationCallback.smali
    │           ├── IAssetPackManagerStatusQueryCallback.smali
    │           ├── IUnityPlayerLifecycleEvents.smali
    │           ├── MultiWindowSupport.smali
    │           ├── NativeLoader.smali
    │           ├── NetworkConnectivity$1.smali
    │           ├── NetworkConnectivity.smali
    │           ├── PlayAssetDeliveryUnityWrapper.smali
    │           ├── R$string.smali
    │           ├── R$style.smali
    │           ├── R.smali
    │           ├── ReflectionHelper$1.smali
    │           ├── ReflectionHelper$a.smali
    │           ├── ReflectionHelper$b.smali
    │           ├── ReflectionHelper.smali
    │           ├── UnityCoreAssetPacksStatusCallbacks.smali
    │           ├── UnityPlayer$1.smali
    │           ├── UnityPlayer$10.smali
    │           ├── UnityPlayer$11.smali
    │           ├── UnityPlayer$12.smali
    │           ├── UnityPlayer$13.smali
    │           ├── UnityPlayer$14.smali
    │           ├── UnityPlayer$15.smali
    │           ├── UnityPlayer$16.smali
    │           ├── UnityPlayer$17.smali
    │           ├── UnityPlayer$18.smali
    │           ├── UnityPlayer$19.smali
    │           ├── UnityPlayer$2.smali
    │           ├── UnityPlayer$20.smali
    │           ├── UnityPlayer$21.smali
    │           ├── UnityPlayer$22.smali
    │           ├── UnityPlayer$23.smali
    │           ├── UnityPlayer$24.smali
    │           ├── UnityPlayer$25.smali
    │           ├── UnityPlayer$26.smali
    │           ├── UnityPlayer$3$1.smali
    │           ├── UnityPlayer$3.smali
    │           ├── UnityPlayer$4$1.smali
    │           ├── UnityPlayer$4.smali
    │           ├── UnityPlayer$5.smali
    │           ├── UnityPlayer$6.smali
    │           ├── UnityPlayer$7.smali
    │           ├── UnityPlayer$8.smali
    │           ├── UnityPlayer$9.smali
    │           ├── UnityPlayer$a.smali
    │           ├── UnityPlayer$b.smali
    │           ├── UnityPlayer$c.smali
    │           ├── UnityPlayer$d.smali
    │           ├── UnityPlayer$e$1.smali
    │           ├── UnityPlayer$e.smali
    │           ├── UnityPlayer$f.smali
    │           ├── UnityPlayer.smali
    │           ├── UnityPlayerActivity.smali
    │           ├── a$a.smali
    │           ├── a$b.smali
    │           ├── a$c$a.smali
    │           ├── a$c.smali
    │           ├── a$d.smali
    │           ├── a$e$a.smali
    │           ├── a$e.smali
    │           ├── a.smali
    │           ├── b$a.smali
    │           ├── b$b.smali
    │           ├── b.smali
    │           ├── c$1.smali
    │           ├── c$2.smali
    │           ├── c$3.smali
    │           ├── c$4.smali
    │           ├── c$5.smali
    │           ├── c$a.smali
    │           ├── c.smali
    │           ├── d$1.smali
    │           ├── d.smali
    │           ├── e.smali
    │           ├── f.smali
    │           ├── g.smali
    │           ├── h.smali
    │           ├── i.smali
    │           ├── j.smali
    │           ├── k.smali
    │           ├── l$a.smali
    │           ├── l.smali
    │           ├── m.smali
    │           ├── n$1.smali
    │           ├── n$2.smali
    │           ├── n$3.smali
    │           ├── n$4.smali
    │           ├── n.smali
    │           ├── o$1.smali
    │           ├── o$a.smali
    │           ├── o.smali
    │           ├── p.smali
    │           ├── q.smali
    │           ├── r$a.smali
    │           ├── r.1.smali
    │           ├── s$a.smali
    │           ├── s$b.smali
    │           ├── s.smali
    │           ├── t$1$1$1.smali
    │           ├── t$1$1.smali
    │           ├── t$1.smali
    │           ├── t$2.smali
    │           ├── t$3.smali
    │           ├── t$4.smali
    │           ├── t$a.smali
    │           └── t.smali
    └── org
        └── fmod
            ├── FMODAudioDevice.smali
            └── a.smali

lib库

与Unity相关的库

  • libil2cpp.so:运行时
  • libmain.so:C#代码编译成了so
  • libunity.so:unity的库

其它的库是开发者自己依赖的库。

│   └── arm64-v8a
│       ├── libil2cpp.so
│       ├── libmain.so
│       ├── libpxr_api.so
│       ├── libpxrplatformloader.so
│       └── libunity.so

Unity的资源文件打包之后存储到了哪里?

Unity逆向 https://blog.csdn.net/e295166319/article/details/83902431

Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8

报错内容:

CommandInvokationFailure: Gradle build failed. 
/Users/bytedance/myUnity/2020.3.32f1/PlaybackEngines/AndroidPlayer/OpenJDK/bin/java -classpath "/Users/bytedance/myUnity/2020.3.32f1/PlaybackEngines/AndroidPlayer/Tools/gradle/lib/gradle-launcher-6.1.1.jar" org.gradle.launcher.GradleMain "-Dorg.gradle.jvmargs=-Xmx4096m" "assembleRelease"

stderr[
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':launcher:checkReleaseDuplicateClasses'.
> 1 exception was raised by workers:
  java.lang.RuntimeException: Duplicate class com.google.gson.DefaultDateTypeAdapter found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.ExclusionStrategy found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldAttributes found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingPolicy found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingPolicy$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingPolicy$2 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingPolicy$3 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingPolicy$4 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingPolicy$5 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.FieldNamingStrategy found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$2 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$3 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$4 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$5 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$6 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.Gson$FutureTypeAdapter found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.GsonBuilder found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.InstanceCreator found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonArray found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonDeserializationContext found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonDeserializer found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonElement found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonIOException found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonNull found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonObject found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonParseException found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonParser found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonPrimitive found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonSerializationContext found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonSerializer found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonStreamParser found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.JsonSyntaxException found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.LongSerializationPolicy found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.LongSerializationPolicy$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.LongSerializationPolicy$2 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.TypeAdapter found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.TypeAdapter$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.TypeAdapterFactory found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.annotations.Expose found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.annotations.JsonAdapter found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.annotations.SerializedName found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.annotations.Since found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.annotations.Until found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.$Gson$Preconditions found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.$Gson$Types found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.$Gson$Types$GenericArrayTypeImpl found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.$Gson$Types$ParameterizedTypeImpl found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.$Gson$Types$WildcardTypeImpl found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$10 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$11 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$12 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$13 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$14 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$2 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$3 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$4 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$5 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$6 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$7 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$8 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ConstructorConstructor$9 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.Excluder found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.Excluder$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.JsonReaderInternalAccess found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LazilyParsedNumber found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$AvlBuilder found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$AvlIterator found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$EntrySet found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$EntrySet$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$KeySet found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$KeySet$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$LinkedTreeMapIterator found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedHashTreeMap$Node found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$EntrySet found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$EntrySet$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$KeySet found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$KeySet$1 found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$LinkedTreeMapIterator found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.LinkedTreeMap$Node found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.ObjectConstructor found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release:)
  Duplicate class com.google.gson.internal.Primitives found in modules gson-2.6.2.jar (com.google.code.gson:gson:2.6.2) and pxr_api-release-runtime.jar (:pxr_api-release<message truncated>

分析报错内容可以看出是有重复jar包。
在LauncherTemplate.gradle里面添加,排除掉一个group

configurations.all {
    println("imczy exclude group: com.google.code.gson")
    // 删除 com.google.code.gson:gson:2.6.2 和 pxr_api-release.aar 有冲突
    exclude group: 'com.google.code.gson'
}

Unity的Android权限

权限有多种类型,有的权限只需要在AndroidManifest里面写明,而有的权限则需要使用Unity显式地弹窗申请权限。

首先在AndroidManifest中申请权限

<?xml version="1.0" encoding="utf-8"?>
<!-- GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN-->
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature
            android:name="android.hardware.vulkan.version"
            android:required="false" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />

    <uses-feature
            android:glEsVersion="0x00020000"
            android:required="true" />
    <uses-feature
            android:name="android.hardware.touchscreen"
            android:required="false" />
    <uses-feature
            android:name="android.hardware.touchscreen.multitouch"
            android:required="false" />
    <uses-feature
            android:name="android.hardware.touchscreen.multitouch.distinct"
            android:required="false" />

    <application>
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:theme="@style/UnityThemeSelector">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>
    </application>
</manifest>

其次,在Unity中申请权限

与权限有关的API:

  • HasUserAuthorizedPermission,判断是否有权限
  • RequestUserPermissions:申请多个权限
  • RequestUserPermission:申请一个权限

如果自定义PermissionManager,在运行到下面这句话的时候会崩溃。

    // private PermissionManager p = new PermissionManager(); //这句话会崩溃
using UnityEngine;
using UnityEngine.Android;

public class Main : MonoBehaviour
{
// Start is called before the first frame update
class PermissionManager : PermissionCallbacks
{
public PermissionManager()
{
// this.PermissionDenied += x => { Log("权限申请被拒绝"); };
// this.PermissionGranted += x => { Log("权限已授权"); };
// this.PermissionDeniedAndDontAskAgain += x => { Log("权限已被拒绝且不再提示"); };
}
}

    // private PermissionManager p = new PermissionManager(); //这句话会崩溃

    static void Log(string s)
    {
        Debug.Log(s);
    }


    void Start()
    {
        var permissions = new[]
        {
            Permission.Camera,
            Permission.Microphone,
            Permission.CoarseLocation,
            Permission.FineLocation,
            Permission.ExternalStorageRead,
            Permission.ExternalStorageWrite,
        };
        foreach (var i in permissions)
        {
            if (!Permission.HasUserAuthorizedPermission(i))
            {
                Permission.RequestUserPermission(i);
            }
        }

        Permission.RequestUserPermissions(permissions);
    }

    // Update is called once per frame
    void Update()
    {
    }
}

CLR:Common Language Runtime

中文叫公共语言运行时。是让 .NET 程序执行所需的外部服务的集合,.NET 平台的核心和最重要的组件,类似于 Java 的 JVM。

script backend

  • mono:只支持32位
  • il2cpp:支持32位和64位,跨平台性更好

il2cpp和mono相比的特点:

  • 可以调试生成的C ++代码。
  • 可以启用引擎代码剥离(Engine code stripping)来减少代码的大小。
  • 程序的运行效率比Mono高,运行速度快。
  • 多平台移植非常方便。
  • 相比Mono构建应用慢。

il2cpp

il2cpp是Unity开发的跨平台CLR解决方案。它产生的一个关键原因是Unity需要跨平台运行,但一些平台如iOS这种禁止了JIT,导致依赖了JIT的官方CLR虚拟机无法运行,必须使用AOT技术将mananged程序提前转化为目标平台的静态原生程序后再运行。而mono虽然也支持AOT,但性能较差以及跨平台支持不佳。il2cpp方案包含一套AOT运行时以及一套dll到C++代码及元数据的转换工具,使得原始的c#开发的代码最终能在iOS这样的平台运行起来。

il2cpp与热更新

很不幸,不像mono有Hybrid mode execution,支持动态加载dll,il2cpp是一个纯静态的AOT运行时,不支持运行时加载dll,因此不支持热更新。目前unity平台的主流热更新方案xlua、ILRuntime之类都是引入一个第三方vm(virtual machine),在vm中解释执行代码,来实现热更新。限于篇幅我们只分析使用c#为开发语言的热更新方案。这些热更新方案的vm与il2cpp是独立的,意味着它们的元数据系统是不相通的,在热更新里新增一个类型是无法被il2cpp所识别的(例如通过System.Activator.CreateInstance是不可能创建出这个热更新类型的实例),这种看起来像、实际上却又不是的伪CLR虚拟机,在与il2cpp这种复杂的CLR运行时交互时,产生极大量的兼容性问题,另外还有严重的性能问题。一个大胆的想法是,是否有可能对il2cpp运行时进行扩充,添加interpreter模块,进而实现mono hybrid mode execution 这样机制?这样一来就能彻底支持热更新了,并且兼容性极佳。对开发者来说,除了以解释模式运行的部分执行得比较慢,其他方面跟标准的运行时没有区别。对il2cpp加以了解并且深思熟虑后的答案是——确实是可行的!具体分析参见 关于HybridCLR可行性的思维实验 。这个想法诞生了HybridCLR,unity平台第一个支持ios的跨平台原生c#热更新方案!

unity的安卓配置

PlayerSettings中可以设置Manifest/LauncherManifest、GradleTemplate img_1.png

<?xml version="1.0" encoding="utf-8"?>
<manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.bytedance.platform4unity">

    <application>
        <activity android:name="com.unity3d.player.UnityPlayerActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

unity添加so包

直接添加so包,然后再属性里面更改so包的平台信息。

unity设置android 64bit

Unity要想使用arm64,就必须把Scripting Backend设置成IL2CPP。
在player settings里面设置: img_2.png

unity宏定义

UNITY_EDITOR 编辑器调用。
UNITY_STANDALONE_OSX 专门为Mac OS(包括 Universal,PPC和Intelarchitectures)平台的定义。 UNITY_DASHBOARD_WIDGET Mac OS Dashboard widget (Mac OS仪表板小部件)。 UNITY_STANDALONE_WIN Windows 操作系统。
UNITY_STANDALONE_LINUX Linux的独立的应用程序。
UNITY_STANDALONE 独立的平台(Mac,Windows或Linux )。 UNITY_WEBPLAYER 网页播放器(包括Windows和Mac Web 播放器可执行文件)。 UNITY_WII Wii游戏机平台。
UNITY_IPHONE iPhone平台。
UNITY_ANDROID Android平台。
UNITY_PS3 PlayStation 3。
UNITY_XBOX360 Xbox 360。
UNITY_NACL 谷歌原生客户端(使用这个必须另外使用 UNITY_WEBPLAYER)。 UNITY_FLASH Adobe Flash。

自定义宏在Player/ScriptCompilation中添加。 img_3.png

如果一个C#文件使用了宏,那么一定要注意:不能随便删除未使用的imports。因为未使用的imports可能在宏里面用到了。

使用宏的一个例子,在下面的示例中,在不同平台下加载不同的DLL名称。一般情况下,不同平台下的DLL名称保持一致即可,但是在同一种操作系统的32位和64位下所加载的dll不能重名。

#pragma warning disable 414
namespace Oculus.Platform
{
  public class CAPI
  {
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
  #if UNITY_64 || UNITY_EDITOR_64
    public const string DLL_NAME = "LibOVRPlatform64_1";
  #else
    public const string DLL_NAME = "LibOVRPlatform32_1";
  #endif
#elif UNITY_EDITOR || UNITY_EDITOR_64

判断UNITY版本

#if UNITY_EDITOR && UNITY_2021_1_OR_NEWER
using Screen = UnityEngine.Device.Screen; // To support Device Simulator on Unity 2021.1+
#endif

判断新老输入系统,新的输入系统叫InputSystem,旧的输入系统叫做InputManager。
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER

如何设置宏

PlayerSettings/OtherSettings/Scripting Define Symbols

设置IDE

img_4.png

开发unity多平台的方法

使用软链接的方式在多个项目中复用Assets ln -s SourceAssets DestAssets

unity使用软链接快速切换平台

我们的项目文件全部存放在Assets文件夹中,其中包含了我们整个项目运行所需的全部资产,如模型、贴图、材质、声音、视频等等,这些资产在导入时会生成对应的.meta文件,这些文件保存了资产之间的引用关系信息。
在Assets中所有资产在导入过程中,都经历了筛选和创建,会形成与之对应的设置,所有的设置都存放在ProjectSettings文件夹中。
如果使用Unity2018版本,我们项目中所有引用的包信息都存放在Packages文件夹中。 Library文件夹是Unity根据项目自动生成的,当我们的资源导入到Unity时,这些资源并没有被改变,而是根据我们的设置生成平台可以运行且处理后的文件放置到Library文件夹中,当我们的平台发生变化时,Library中的文件会根据变化重新编译生成与之相应的内容。 obj文件夹存放了Unity的部分缓存信息。
根据以上部分,当我们需要切换平台时,其改变的内容主要存在于Library文件夹中,我们可以根据不同的平台复制多个工程,在打包时把Assets、Packages和ProjectSettings文件夹放入其他平台的文件夹中进行编译即可。
把assets和package两个文件夹使用软链接,项目配置主要位于project settings目录。建立两个项目。unity设计的多平台方案切换起来过于耗时。

webgl编译产物部署

在Player Settings/Publish Settings可以设置压缩方式。默认使用brotli。
Brotli 是 google 开发的压缩算法,比起gzip可能会有高达37%的提升。目前主流浏览器都支持br算法。使用算法的前提是启用了 https,因为 http 请求中 request header 里的 Accept-Encoding: gzip, deflate 是没有 br 的。
使用brotli比较复杂,一是服务器需要进行相应配置;二是只能使用https。所以尝试使用gzip。
但是gzip的使用也建议不要在unity里面进行,而是让unity生成原始文件,然后在nginx里面配置静态资源的压缩,这种方式更为彻底,可以说是webgl部署的最佳实践。

    location {{locationName}}/ { 
      # access_log      /var/log/nginx/{{name}}.log;
      # 如果有公共目录,那么设置公共目录
        alias {{directory}} ;  #如果是root需要带着最后一个pathName
        index index.html Index.html ;
        gzip on;
        gzip_static on;
        gzip_min_length 1k;
        gzip_comp_level 5;
        gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png    application/json image/svg+xml  application/octet-stream;

    }

开发技巧

二进制文件没办法进行代码合并,只有文本文件可以进行代码合并。
Unity中的场景是一种yaml文件,这种文件执行代码合并之后可读性较差。

Unity提供了一个UnityYAMLMerge工具,该工具支持从命令行访问,可以用于合并场景、prefab等文件。

当使用Unity的Plastic时,在"编辑/项目设置/版本控制"中,会显示智能合并选项。

默认的路径:

  • windows:C:\Program Files\Unity\Editor\Data\Tools\UnityYAMLMerge.exe
  • mac:/Applications/Unity/Unity.app/Contents/Tools/UnityYAMLMerge

Unity可执行命令的路径

在 macOS 上,在终端中输入以下命令来启动 Unity:

/Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/MacOS/Unity -projectPath <project path>

On Linux, type the following into the Terminal to launch Unity:

/Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/Linux/Unity -projectPath <project path>

在 Windows 上,在命令提示符下输入以下命令来启动 Unity:

"C:\Program Files\Unity\Hub\Editor\<version>\Editor\Unity.exe" -projectPath "<project path>"

When you launch Unity like this, it receives commands and information on startup, which can be useful for test suites, automated builds and other production tasks.

Unity命令参数

-batchmode
//在 batch mode 下執行 Unity
//需要注意,Unity 只允許同時間存在一個執行程序
 
-quit
//在命令行結束執行時,關閉 Unity Editor
//需要注意使用這個功能,會導致無法在 Unity Editor 中查看錯誤訊息
 
-projectPath <pathname>
//Unity 專案路徑
 
-logFile <pathname>
//建置日誌路徑
 
-executeMethod <ClassName.MethodName>
//開啟 Unity 時,執行類別中的靜態方法
//可利用於 CI、Unit Tests、版本建置、資料處理...等。
//要注意類別腳本需要放置在 Editor 資料夾中

使用示例

$unity -projectPath xxxxx 从命令行打开Unity

Unity命令可以跟Unity的菜单配合使用

using UnityEditor;
using UnityEngine;
using System;
using System.IO;
using System.Collections.Generic;
 
public class BuildTool
{
    [MenuItem("BuildTool/Build")]
    private static void Build()
    {
        CustomizedCommandLine();
 
        string destinationPath = Path.Combine(_destinationPath, PlayerSettings.productName);
        destinationPath += GetExtension();
 
        BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, destinationPath, EditorUserBuildSettings.activeBuildTarget, BuildOptions.None);
    }
 
 
    private static string _destinationPath;
    private static void CustomizedCommandLine()
    {
        Dictionary<string, Action<string>> cmdActions = new Dictionary<string, Action<string>>
        {
            {
                "-destinationPath", delegate(string argument)
                {
                    _destinationPath = argument;
                }
            }
        };
 
        Action<string> actionCache;
        string[] cmdArguments = Environment.GetCommandLineArgs();
 
        for (int count = 0; count < cmdArguments.Length; count++)
        {
            if (cmdActions.ContainsKey(cmdArguments[count]))
            {
                actionCache = cmdActions[cmdArguments[count]];
                actionCache(cmdArguments[count + 1]);
            }
        }
 
        if (string.IsNullOrEmpty(_destinationPath))
        {
            _destinationPath = Path.GetDirectoryName(Application.dataPath);
        }
    }
 
 
    private static string GetExtension()
    {
        string extension = "";
 
        switch (EditorUserBuildSettings.activeBuildTarget)
        {
            case BuildTarget.StandaloneOSXIntel:
            case BuildTarget.StandaloneOSXIntel64:
            case BuildTarget.StandaloneOSXUniversal:
                extension = ".app";
                break;
            case BuildTarget.StandaloneWindows:
            case BuildTarget.StandaloneWindows64:
                extension = ".exe";
                break;
            case BuildTarget.Android:
                extension = ".apk";
                break;
        }
 
        return extension;
    }
}
#!/bin/bash
 
UNITY_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity
PROJECT_PATH=/Users/ted/SideProjects/UnityCommandLineBuild
BUILD_LOG_PATH=${PROJECT_PATH}/build.log
DESTINATION_PATH=/Users/ted/Desktop/

$UNITY_PATH -quit -batchmode -projectPath ${PROJECT_PATH} -executeMethod BuildTool.Build -logFile ${BUILD_LOG_PATH} -destinationPath ${DESTINATION_PATH}

调用构建API的最简方式

using UnityEditor;
class MyEditorScript
{
     static void PerformBuild ()
     {
        BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
        buildPlayerOptions.scenes = new[] { "Assets/Scene1.unity", "Assets/Scene2.unity" };
        BuildPipeline.BuildPlayer(buildPlayerOptions);
     }
}
/Applications/Unity/Unity.app/Contents/MacOS/Unity -quit -batchmode -projectPath ~/UnityProjects/MyProject -executeMethod MyEditorScript.PerformBuild

除了明确指定场景,也可以使用编辑器中的默认场景。

using UnityEditor;

public class MyEditorScript
{
    public static void PerformBuild()
    {
        BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, "a.apk", EditorUserBuildSettings.activeBuildTarget, BuildOptions.None);
    }
}

技巧

Unity对于一个项目只能打开一次,在Unity打开的情况下,上述编译命令就不可用了。报错一个项目只允许打开一次。

Aborting batchmode due to failure:
Fatal Error! It looks like another Unity instance is running with this project open.

Multiple Unity instances cannot open the same project.


如何解决这个问题呢?
把文件夹复制一份,然后执行上述命令。
注意:使用软链接是不行的,必须使用复制的方式。

获取当前活跃场景

EditorBuildSettingsScene.GetActiveSceneList()

var x=EditorSceneManager.GetActiveScene();

获取XR Settings

  var m=XRGeneralSettings.Instance.Manager;
        m.load
            XRGeneralSettings generalSettings = XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(BuildTargetGroup.Android);
            if (generalSettings == null) return false;
                var assignedSettings = generalSettings.AssignedSettings;
            if (assignedSettings == null) return false;
#if UNITY_2021_1_OR_NEWER
            foreach (XRLoader loader in assignedSettings.activeLoaders)
            {
                if (loader is PXR_Loader) return true;
            }
#else
            foreach (XRLoader loader in assignedSettings.loaders)
            {
                if (loader is PXR_Loader) return true;
            }

XR Editor配置 https://docs.unity3d.com/Packages/com.unity.xr.management@4.0/manual/EndUser.html

运行当前场景 BuildPipeline scenes传空即可。

参考资料

https://docs.unity3d.com/Manual/CommandLineArguments.html

查看全部快捷键

主菜单中的shortcuts

编辑gameObject快捷键

CommandShortCut
ViewQ
MoveW
RotateE
ScaleR
RectT
TransformY
Toggle Pivot PositionZ
Toggle Pivot OrientationX

视窗切换

scene=1

game=2

inspector=3

hierachy=4

project=5

console: Shift+Command+C

设置自动刷新

在settings里面把autoRefresh关掉,使用Command+R快捷键。

避免重复刷新。

Unity的三处设置

  1. Unity编辑器全局设置:在Settings里面

  2. Unity的BuildSettings:编译设置

  3. Unity的ProjectSettings:项目设置,内容最为丰富,包括PlayerSettings

插件

InGameDebugConsole如果使用新的输入系统,则需要做以下修改:

  • 在PackageManager中导入InputSystem
  • 在InGameDebugConsole这个插件的eventSystem中修改Input

修改InGameDebugConsole的asmdef

{
    "name": "IngameDebugConsole.Runtime",
    "references": ["Unity.InputSystem"]
}

为了简便期间,改完一次之后,可以将全部东西保存下来,下次可以直接导入,不用再改这么多地方了。

在DebugLogManager.cs文件头部有一个宏#define ENABLE_INPUT_SYSTEM。这个宏的作用是开启InputSystem。
一旦在InGameDebugConsole的references里面添加了Unity.InputSystem,这个宏就自动定义了。

using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
using UnityEngine.InputSystem;
#endif
#if UNITY_EDITOR && UNITY_2021_1_OR_NEWER
using Screen = UnityEngine.Device.Screen; // To support Device Simulator on Unity 2021.1+
#endif

#define ENABLE_INPUT_SYSTEM

unity官方资源商店

https://assetstore.unity.com/3d

Unity插件神器InGameDebugTool

首先从assets商店下载这个资源,然后再PackageManager中添加这个插件,就可以在Assets/Plugins目录中找到这个插件。使用Debug.Log记录日志的时候,可以看到日志统计的情况,点击则可以进入Console界面。Unity本身没有提供这样的Debug功能,但是社区造出了这样神奇的功能。

unity remote:移动端真机调试

unity remote的作用:程序依旧运行在电脑上,移动端只适用于显示和接收用户操作。 需要从google play 进行安装。 https://play.google.com/store/apps/details?id=com.unity3d.mobileremote 在project settings/Editor中进行设置。

我常用的插件

  • fontasy-monster skeleton
  • InGameDebugConsole
  • JSON.Net for Unity
  • Oculus Integration
  • Substance for unity

测试

Unity的TestRunner生成的包名:com.UnityTestRunner.UnityTestRunner

WaitForSeconds

一切yield的东西都继承自:YieldInstruction。 UnityEngine.AsyncOperation (derived) UnityEngine.Coroutine (derived) UnityEngine.Coroutine.Coroutine() UnityEngine.WaitForEndOfFrame (derived) UnityEngine.WaitForFixedUpdate (derived) UnityEngine.WaitForFixedUpdate.WaitForFixedUpdate() UnityEngine.WaitForSeconds (derived) UnityEngine.WaitForSeconds.WaitForSeconds(float seconds)

Unity的自定义YieldInstruction

  public sealed class WaitUntil : CustomYieldInstruction
  {
    private Func<bool> m_Predicate;

    public override bool keepWaiting => !this.m_Predicate();

    public WaitUntil(Func<bool> predicate) => this.m_Predicate = predicate;
  }
  public sealed class WaitWhile : CustomYieldInstruction
  {
    private Func<bool> m_Predicate;

    public override bool keepWaiting => this.m_Predicate();

    public WaitWhile(Func<bool> predicate) => this.m_Predicate = predicate;
  }

一个小问题:下面两个语句等价吗?基本上是等价的,但是第一种写法在超时之后有可能陷入死循环,而第二种方法则不会陷入死循环。 如果改写为while (tasks > 0 && !timeout) yield return null;会更安全一些。

while (tasks > 0) yield return null;

yield return new WaitWhile(()=>tasks > 0);

理解UniyTest协程

NUnity默认的Test属性会在一帧渲染里面执行完毕,UnityTest则能够让测试在多帧里面执行。UnityTest函数中的每一次yield return都会立即退出当前帧,等待下一帧再继续执行。

自定义Wait

public class Wait
{
    static public IEnumerator Until(Func<bool> condition, float timeout = 30f)
    {
        float timePassed = 0f;
        while (!condition() && timePassed < timeout) {
            yield return new WaitForEndOfFrame();
            timePassed += Time.deltaTime;
        }
        if (timePassed >= timeout) {
            throw new TimeoutException("Condition was not fulfilled for " + timeout + " seconds.");
        }
    }
}

[UnityTest]
public IEnumerator TestSkeletonFollowsPlayer()
{
    Vector3 playerPos = new Vector3(2f, 1f, -5f);
    Quaternion playerDir = Quaternion.identity;
    Vector3 skeletonPos = new Vector3(2f, 0f, 5f);
    Quaternion skeletonDir = Quaternion.LookRotation(new Vector3(0f, 0f, -1f), Vector3.up);

    GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
    GameObject skeleton = GameObject.Instantiate(skeletonPrefab, skeletonPos, skeletonDir);
    skeleton.GetComponent<Skeleton>().player = player.GetComponent();

    yield return Wait.Until(() => {
        float distance = Math.Abs((skeleton.transform.position - player.transform.position).magnitude);
        return distance <= 2f;
    }, timeout: 10f);
}

Unity测试用例的几个阶段

第一阶段:使用yield return new WaitForSeconds 缺点:时长不确定。

[UnityTest]
[UnityPlatform(RuntimePlatform.Android)]
[Timeout(10000)]
public IEnumerator UserGet()
{
    var hasCallback = false;
    UserService.GetLoggedInUser().OnComplete(u =>
    {
        Assert.IsFalse(u.IsError);
        ModelAssert.User(u.Data);
        UserService.Get(u.Data.ID).OnComplete(m =>
        {
            hasCallback = true;
            Assert.IsFalse(m.IsError);
            ModelAssert.User(m.Data);
        });
    });
    yield return new WaitForSeconds(5);
    Assert.IsTrue(hasCallback);
}

第二阶段:使用bool值+for循环 缺点:只能处理一个网络请求的情况,无法处理多个网络请求的情况。

[UnityTest]
[UnityPlatform(RuntimePlatform.Android)]
[Timeout(10000)]
public IEnumerator GetLoggedInUser()
{
    var hasCallback = false;
    UserService.GetLoggedInUser().OnComplete(m =>
    {
        hasCallback = true;
        Assert.IsFalse(m.IsError);
        ModelAssert.User(m.Data);
    });
    while (!hasCallback) yield return null;
}

第三阶段:使用int值 可以处理多个网络请求

Unity如何书写异步测试

Async unit test in Test Runner - Unity Answers

TestRunner会报错:Method has non-void return value, but no result is expectedNUnit实际上并没有报错,是Unity的TestRunner报了一个错,属于Unity TestRunner的bug。

        [Test]
        public async Task 测试异步expect()
        {
            Debug.Log("这是异步测试");
            return;
        }

解决方法就是绕过TestRunner,不要使用异步方法测试

    public static class UnityTestUtils {
      public static void RunAsyncMethodSync(t$$anonymous$$s Func < Task > asyncFunc) {
        Task.Run(async () => await asyncFunc()).GetAwaiter().GetResult();
      }
    }
     public class AsyncAwaitUnitTests {
      [Test]
      public void WithExt([Values(0, 500, 1000)] int delay) {
        UnityTestUtils.RunAsyncMethodSync(async () => {
          var sw = Stopwatch.StartNew();
 
          await Task.Delay(delay);
 
          Assert.AreEqual(delay, (int) sw.Elapsed.TotalMilliseconds, 300);
        });
      }
    }

有人提供了另一种工具方法封装:

         public static class UnityTestUtils {
      
             public static T RunAsyncMethodSync<T>(Func<Task<T>> asyncFunc) {
                 return Task.Run(async () => await asyncFunc()).GetAwaiter().GetResult();
             }
             public static void RunAsyncMethodSync(Func<Task> asyncFunc) {
                 Task.Run(async () => await asyncFunc()).GetAwaiter().GetResult();
             }
         }
         [Test]
         public void Test()
         {
             var result = RunAsyncMethodSync(() => GetTestTaskAsync(4));
             Assert.That(result, Is.EqualTo(4));
         }
     
         public async Task<int> GetTestTaskAsync(int a) {
             await Task.Delay(TimeSpan.FromMilliseconds(200));
             return a;
         }
     
         [Test]
         public void Testthrow() {
             Assert.Throws<InvalidOperationException>(
                            ()=> RunAsyncMethodSync(() => ThrowTaskAsync(4)));
         }
     
         public async Task<int> ThrowTaskAsync(int a) {
             await Task.Delay(TimeSpan.FromMilliseconds(200));
             throw new InvalidOperationException();
         }

参考资料

Unity Test Framework NUnit官网

学习NUIT学习什么

NUnit是C#语言中最重要的测试框架,对标Java中的JUnit。

  1. Assert系列函数,有两套Assert体系,一套是NUnit自己的,一套是Unity的。
  2. case上的注解,SetUp,TearDown,ExpectedException,Timeout等。

不轻易修改测试

测试一旦完成,就不再修改,除非功能发生变化。

尽量避免测试中的复杂逻辑

单元测试中,最好保持逻辑的简单,避免逻辑太重。因为逻辑如果复杂,测试结果不符合预期的时候就需要判断是测试写的不对还是功能不对。

一个case只测一个点

避免在一个case里面包含大量的测试点。

保持测试case的可读性

测试代码的特点应该是冗长、简单。就像文档一样,虽然很长,但是每一个点都很简单。

Android系统上 Unity的TestRunner的原理

创建一个虚拟场景,这个虚拟场景挂上测试脚本,在Android机器上运行这个场景。

使用

在Window/General/TestRunner中, 点击TestRunner窗口的右上角:RunSelectedTests进行运行。也可以选择RunAllTests。这个测试过程也是需要编译的,比较慢。
TestRunner有两种模式:

  • Editor:编辑器模式
  • Play:真机模式

创建测试用例

直接右键创建,选择Testing,可以选择EditorMode和PlayMode的C#脚本。
在Window/General/TestRunner窗口中,底部也有创建测试用例的按钮。

其实 [Test] 的注解就是普通的测试标签,[UnityTest]标签才是 PlayMode 测试用例的标签,同时该注解下的函数返回类型是个迭代器 IEnumerator,我们注意到该函数内部还有一条语句 yield return null。其实还有类似的写法,如 yield return new WaitForSeconds()、 yield return new WaitForEndOfFrame()、 yield return new WaitForFixedUpdate() 等。

在EditorMode下,一般使用[Test]属性较多。

PlayMode下的测试用例为什么返回值是IEnumerator?因为Unity的测试用例是一帧一帧执行的。一旦执行yield return null;则后面的代码在下一帧的时候才会执行。这种执行机制与Unity游戏刷新机制相似,从而便于测试。

从命令行运行单元测试

https://docs.unity.cn/Packages/com.unity.test-framework@1.1/manual/reference-command-line.html

Unity.exe -runTests -batchmode -projectPath PATH_TO_YOUR_PROJECT -testResults C:\temp\results.xml -testPlatform PS4

NUnit的使用

NUnit是C#中最著名的测试框架。编写好测试用例之后,在Window/General/TestRunner中可以看见测试用例。测试用例应该放在Editor文件夹下(因为测试case与Unity Editor相关,所以应该放在Editor目录下面)。 如果不把测试用例放在Editor文件夹下,也可以随意一个目录,然后添加以下内容:xxx.asmdef

{
    "name": "Tests",
    "optionalUnityReferences": [
        "TestAssemblies"
    ]
}

写一段代码:

using UnityEngine;
using System.Collections;
using NUnit.Framework;

[TestFixture]
public class HealthComponentTests
{
  //测试伤害之后,血的值是否比0大
  [Test]
  public void TakeDamage_BeAttacked_BiggerZero()
    {
      //Arrange 
      UnMonoHealthClass health = new UnMonoHealthClass();
      health.healthAmount = 50f;

      //Act
      health.TakeDamage(60f);

      //Assert
        Assert.GreaterOrEqual(health.healthAmount, 0);
    }
}

常见的NUnit属性如下,对应的有UnitySetUp、UnityTearDown等为Unity进行特殊定制的属性。

[SetUp]
[TearDown]  
[TestFixture]
[Test]
[TestCase]
[Category]
[Ignore]

在游戏中,UI、交互方面的测试比较难以实现,只能在逻辑部分进行一些单元测试。

单元测试有两类:Edit模式和play模式 在Edit模式下,是本地测试。 在Play模式下,是运行在Android系统。 如果创建的是Edit模式的测试,则应该放在Editor目录下面。 如果创建的是Play模式的测试,则可以放在任意一个目录下面,并且添加xxxx.asdef文件。这里面的关键点是optionalUnityReferences设置为TestAssemblies。只有这样才能够引用到一些测试库。 如果要测试的是另一个assembly,则应该添加references才能引用到另外一个单元。

{
  "name": "Tests",
  "references": [
    "XXXX.Platform"
  ],
  "optionalUnityReferences": [
    "TestAssemblies"
  ]
}

asmdef.json

asmdef.json是unity特有的程序集描述文件。许多工程下面都有xxxx.asmdef文件。

{
    "schema": "http://json-schema.org/draft-06/schema#",
    "title": "Unity Assembly Definition",
    "description": "Defines an assembly in the Unity compilation pipeline",
    "type": "object",
    "properties": {
        "name": {
            "description": "The name of the assembly being defined",
            "type": "string",
            "minLength": 1
        },
        "rootNamespace": {
            "description": "The root namespace of the assembly. Requires Unity 2020.2",
            "type": "string",
            "minLength": 1
        },
        "references": {
            "description": "A list of assembly names or assembly definition asset GUIDs to reference",
            "type": "array",
            "items": {
                "type": "string",
                "minLength": 1
            },
            "uniqueItems": true
        },
        "includePlatforms": {
            "description": "Platforms to target",
            "$ref": "#/definitions/platformValues"
        },
        "excludePlatforms": {
            "description": "Platforms that are explicitly not targeted",
            "$ref": "#/definitions/platformValues"
        },
        "allowUnsafeCode": {
            "description": "Allow unsafe code",
            "type": "boolean",
            "default": false
        },
        "autoReferenced": {
            "description": "When true, this assembly definition is automatically referenced by predefined assemblies (Assembly-CSharp, Assembly-CSharp-firstpass, etc.)",
            "type": "boolean",
            "default": true
        },
        "noEngineReferences": {
            "description": "When true, no references to UnityEngine or UnityEditor will be added to this assembly project",
            "type": "boolean",
            "default": false
        },
        "overrideReferences": {
            "description": "When true, any references to precompiled assembly assets are manually chosen. When false, all precompiled assembly assets are automatically referenced",
            "type": "boolean",
            "default": "false"
        },
        "precompiledReferences": {
            "description": "A list of precompiled assembly assets that will be referenced. Only valid when overrideReferences is true",
            "type": "array",
            "uniqueItems": true
        },
        "defineConstraints": {
            "description": "A list of the C# compiler define symbols that must evaluate to true in order for the assembly to be compiled or referenced. Absence of a symbol can be checked with a bang symbol (!DEFINE)",
            "type": "array",
            "items": {
                "type": "string"
            },
            "uniqueItems": true
        },
        "optionalUnityReferences": {
            "description": "Additional optional Unity features to reference. Not supported since 2019.3",
            "type": "array",
            "items": {
                "enum": [
                    "TestAssemblies"
                ]
            },
            "uniqueItems": true
        },
        "versionDefines": {
            "description": "A set of expressions that will define a symbol in the C# project if a package or module version matches the given expression",
            "type": "array",
            "uniqueItems": true,
            "items": {
                "type": "object",
                "properties": {
                    "name": {
                        "description": "The package or module that will provide the version to be checked in the expression",
                        "type": "string",
                        "minLength": 1
                    },
                    "expression": {
                        "description": "The semantic version range of the chosen package or module",
                        "type": "string"
                    },
                    "define": {
                        "description": "The name of the define that is added to the project file when expression evaluates to true",
                        "type": "string"
                    }
                },
                "required": [ "name", "expression", "define" ],
                "minLength": 1
            }
        }
    },
    "definitions": {
        "platformValues": {
            "type": "array",
            "uniqueItems": true,
            "items": {
                "enum": [
                    "Android",
                    "CloudRendering",
                    "Editor",
                    "GameCoreXboxOne",
                    "iOS",
                    "LinuxStandalone64",
                    "Lumin",
                    "macOSStandalone",
                    "PS4",
                    "PS5",
                    "Stadia",
                    "Switch",
                    "tvOS",
                    "WSA",
                    "WebGL",
                    "WindowsStandalone32",
                    "WindowsStandalone64",
                    "XboxOne",

                    "GameCoreScarlett",
                    "LinuxStandalone32",
                    "LinuxStandaloneUniversal",
                    "Nintendo3DS",
                    "PSMobile",
                    "PSVita",
                    "Tizen",
                    "WiiU"
                ]
            }
        }
    },
    "required": ["name"],
    "anyOf": [
        {
            "properties": {
                "includePlatforms": {
                    "minItems": 1
                },
                "excludePlatforms": {
                    "maxItems": 0
                }
            }
        },
        {
            "properties": {
                "includePlatforms": {
                    "maxItems": 0
                },
                "excludePlatforms": {
                    "minItems": 1
                }
            }
        },
        {
            "properties": {
                "includePlatforms": {
                    "maxItems": 0
                },
                "excludePlatforms": {
                    "maxItems": 0
                }
            }
        }
    ]
}

测试的asm

{
  "name": "Tests",
  "references": [
    "Pico.Platform"
  ],
  "optionalUnityReferences": [
    "TestAssemblies"
  ]
}

Mock和Stub有何区别?

Mock与Stub的区别:
Mock:关注行为验证。细粒度的测试,即代码的逻辑,多数情况下用于单元测试。
Stub:关注状态验证。粗粒度的测试,在某个依赖系统不存在或者还没实现或者难以测试的情况下使用,例如访问文件系统,数据库连接,远程协议等。

使用注解

断言抛出某种类型的异常

[ExpectedException(typeof(NegativeHealthException))]

设定运行时长

使用命令运行单元测试

Unity程序如果实现自动化测试,例如CI,在代码提交之后自动执行,那么就需要使用命令行运行测试,而不是使用TestRunner窗口。

Unity运行时支持以下参数:

runEditorTests
editorTestsResultFile
editorTestsFilter
editorTestsCategories
editorTestsVerboseLog

写法实例

OneTimeSetUp:全局执行一次 SetUp、TearDown:每个用例都要执行一次

using UnityEngine; //基于 Unity 引擎,必须引用
using NUnit.Framework; //引用NUnit测试框架

[TestFixture, Description("测试套")] //一个类对应一个测试套,通常一个测试特性对应一个测试套。
public class UnitTestDemoTest
{
    [OneTimeSetUp] //在执行该测试套时首先会执行该函数,在整个测试套中只执行一次。
    public void OneTimeSetUp()
    {
        Debug.Log("OneTimeSetUp"); 
    }

    [OneTimeTearDown] //在执行该测试套时最后会执行该函数,在整个测试套中只执行一次。
    public void OneTimeTearDown()
    {
        Debug.Log("OneTimeTearDown");
    }

    [SetUp]
    public void SetUp() //在执行每个用例之前都会执行一次该函数
    {
        Debug.Log("SetUp");
    }

    [TearDown] //在执行完每个用例之后都会执行一次该函数
    public void TearDown()
    {
        Debug.Log("TearDown");
    }

    [TestCase, Description("测试用例1")] //这个函数内部写测试用例
    public void TestCase1()
    {
        Debug.Log("TestCase1"); 
    }

    [TestCase, Description("测试用例2")] //这个函数内部写测试用例
    public void TestCase2()
    {
        Debug.Log("TestCase2");
    }
}

TestCase和Test的区别

TestCase可以接收参数。并且可以指定多组输入数据。

    // 多测试数据的GetTextLength测试
    [TestCase("", 0)]
    [TestCase("Hello World", 11)]
    public void GetTextLength_MultiTestData(string data, int exResult)
    {
        int result = GameUtils.GetTextLength(data);
        Assert.AreEqual(exResult, result);
    }

Unity Test Framework是Unity的测试模块,简称UTF。

UTF提供了一些定制能力,可以在模块介绍中找到。

获取测试用例的列表

var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.RetrieveTestList(TestMode.EditMode, (testRoot) =>
{
    Debug.Log(string.Format("Tree contains {0} tests.", testRoot.TestCaseCount));
});

获取测试结果

创建一个TestRunnerApi对象,然后给这个对象注册一个Callback,在用例运行的时候处理这些回调。 例如在测试结束的时候可以获取测试结果。

public void SetupListeners()
{
   var api = ScriptableObject.CreateInstance<TestRunnerApi>();
   api.RegisterCallbacks(new MyCallbacks());
}

private class MyCallbacks : ICallbacks
{
    public void RunStarted(ITestAdaptor testsToRun)
    {

    }

    public void RunFinished(ITestResultAdaptor result)
    {

    }

    public void TestStarted(ITestAdaptor test)
    {

    }

    public void TestFinished(ITestResultAdaptor result)
    {
        if (!result.HasChildren && result.ResultState != "Passed")
        {
            Debug.Log(string.Format("Test {0} {1}", result.Test.Name, result.ResultState));
        }
    }
}

通过程序运行测试用例而不是通过TestRunner窗口

只运行Play模式下的用例

var testRunnerApi = ScriptableObject.CreateInstance<TestRunnerApi>();
var filter = new Filter()
{
    testMode = TestMode.PlayMode
};
testRunnerApi.Execute(new ExecutionSettings(filter));

指定用例名称

var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.Execute(new ExecutionSettings(new Filter()
{
    testNames = new[] {"MyTestClass.NameOfMyTest", "SpecificTestFixture.NameOfAnotherTest"}
}));

通过Assembly和用例名称同时过滤测试用例。在此例中,选择满足assemby和testNames两个条件的用例来运行。

var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.Execute(new ExecutionSettings(new Filter()
{
    assemblyNames = new [] {"MyTestAssembly"},
    testNames = new [] {"MyTestClass.NameOfMyTest", "MyTestClass.AnotherNameOfATest"}
}));

ExecutionSettings可以指定多个Filter,多个Filter之间满足任何一个就能够执行。

var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.Execute(new ExecutionSettings(
    new Filter()
    {
        assemblyNames = new[] {"MyTestAssembly"},
    },
    new Filter()
    {
        testNames = new[] {"MyTestClass.NameOfMyTest", "MyTestClass.AnotherNameOfATest"}
    }
));

修改编译选项

实现ITestPlayerBuildModifier接口。

using UnityEditor;
using UnityEditor.TestTools;

[assembly:TestPlayerBuildModifier(typeof(BuildModifier))]
public class BuildModifier : ITestPlayerBuildModifier
{
    public BuildPlayerOptions ModifyOptions(BuildPlayerOptions playerOptions)
    {
        if (playerOptions.target == BuildTarget.iOS)
        {
            playerOptions.options |= BuildOptions.SymlinkLibraries; // Enable symlink libraries when running on iOS
        }

        playerOptions.options |= BuildOptions.AllowDebugging; // Enable allow Debugging flag on the test Player.
        return playerOptions;
    }
}

只进行编译生成apk,不运行

using System;
using System.IO;
using System.Linq;
using Tests;
using UnityEditor;
using UnityEditor.TestTools;
using UnityEngine;
using UnityEngine.TestTools;

[assembly:TestPlayerBuildModifier(typeof(HeadlessPlayModeSetup))]
[assembly:PostBuildCleanup(typeof(HeadlessPlayModeSetup))]

namespace Tests
{
    public class HeadlessPlayModeSetup : ITestPlayerBuildModifier, IPostBuildCleanup
    {
        private static bool s_RunningPlayerTests;
        public BuildPlayerOptions ModifyOptions(BuildPlayerOptions playerOptions)
        {
            // Do not launch the player after the build completes.
            playerOptions.options &= ~BuildOptions.AutoRunPlayer;

            // Set the headlessBuildLocation to the output directory you desire. It does not need to be inside the project.
            var headlessBuildLocation = Path.GetFullPath(Path.Combine(Application.dataPath, ".//..//PlayModeTestPlayer"));
            var fileName = Path.GetFileName(playerOptions.locationPathName);
            if (!string.IsNullOrEmpty(fileName))
            {
                headlessBuildLocation = Path.Combine(headlessBuildLocation, fileName);
            }
            playerOptions.locationPathName = headlessBuildLocation;

            // Instruct the cleanup to exit the Editor if the run came from the command line. 
            // The variable is static because the cleanup is being invoked in a new instance of the class.
            s_RunningPlayerTests = true;
            return playerOptions;
        }

        public void Cleanup()
        {
            if (s_RunningPlayerTests && IsRunningTestsFromCommandLine())
            {
                // Exit the Editor on the next update, allowing for other PostBuildCleanup steps to run.
                EditorApplication.update += () => { EditorApplication.Exit(0); };
            }
        }

        private static bool IsRunningTestsFromCommandLine()
        {
            var commandLineArgs = Environment.GetCommandLineArgs();
            return commandLineArgs.Any(value => value == "-runTests");
        }
    }
}

https://docs.nunit.org/articles/nunit/writing-tests/attributes.html

Apartment Attribute Indicates that the test should run in a particular apartment. Author Attribute Provides the name of the test author. Category Attribute Specifies one or more categories for the test. Combinatorial Attribute Generates test cases for all possible combinations of the values provided. Culture Attribute Specifies cultures for which a test or fixture should be run. Datapoint Attribute Provides data for Theories. DatapointSource Attribute Provides data for Theories. DefaultFloatingPointTolerance Attribute Indicates that the test should use the specified tolerance as default for float and double comparisons. Description Attribute Applies descriptive text to a Test, TestFixture or Assembly. Explicit Attribute Indicates that a test should be skipped unless explicitly run. FixtureLifeCycle Attribute Specifies the lifecycle of a fixture allowing a new instance of a test fixture to be constructed for each test case. Useful in situations where test case parallelism is important. Ignore Attribute Indicates that a test shouldn't be run for some reason. LevelOfParallelism Attribute Specifies the level of parallelism at assembly level. MaxTime Attribute Specifies the maximum time in milliseconds for a test case to succeed. NonParallelizable Attribute Specifies that the test and its descendants may not be run in parallel. NonTestAssembly Attribute Specifies that the assembly references the NUnit framework, but that it does not contain tests. OneTimeSetUp Attribute Identifies methods to be called once prior to any child tests. OneTimeTearDown Attribute Identifies methods to be called once after all child tests. Order Attribute Specifies the order in which decorated test should be run within the containing fixture or suite. Pairwise Attribute Generate test cases for all possible pairs of the values provided. Parallelizable Attribute Indicates whether test and/or its descendants can be run in parallel. Platform Attribute Specifies platforms for which a test or fixture should be run. Property Attribute Allows setting named properties on any test case or fixture. Random Attribute Specifies generation of random values as arguments to a parameterized test. Range Attribute Specifies a range of values as arguments to a parameterized test. Repeat Attribute Specifies that the decorated method should be executed multiple times. RequiresThread Attribute Indicates that a test method, class or assembly should be run on a separate thread. Retry Attribute Causes a test to be rerun if it fails, up to a maximum number of times. Sequential Attribute Generates test cases using values in the order provided, without additional combinations. SetCulture Attribute Sets the current Culture for the duration of a test. SetUICulture Attribute Sets the current UI Culture for the duration of a test. SetUp Attribute Indicates a method of a TestFixture called just before each test method. SetUpFixture Attribute Marks a class with one-time setup or teardown methods for all the test fixtures in a namespace. SingleThreaded Attribute Marks a fixture that requires all its tests to run on the same thread. TearDown Attribute Indicates a method of a TestFixture called just after each test method. Test Attribute Marks a method of a TestFixture that represents a test. TestCase Attribute Marks a method with parameters as a test and provides inline arguments. TestCaseSource Attribute Marks a method with parameters as a test and provides a source of arguments. TestFixture Attribute Marks a class as a test fixture and may provide inline constructor arguments. TestFixtureSetup Attribute Deprecated synonym for OneTimeSetUp Attribute. TestFixtureSource Attribute Marks a class as a test fixture and provides a source for constructor arguments. TestFixtureTeardown Attribute Deprecated synonym for OneTimeTearDown Attribute. TestOf Attribute Indicates the name or Type of the class being tested. Theory Attribute Marks a test method as a Theory, a special kind of test in NUnit. Timeout Attribute Provides a timeout value in milliseconds for test cases. Values Attribute Provides a set of inline values for a parameter of a test method. ValueSource Attribute Provides a source of values for a parameter of a test method.

NUnit有两种模式的Assert,一种模式是That模式,这种模式的特点是非常灵活;另一种模式是函数式,有很多个小函数,这些小函数的实现其实都是基于That模式。小函数模式使用起来更加直观简洁。

有一些断言只能使用That实现。

int[] array = { 1, 2, 3 };
Assert.That(array, Has.Exactly(1).EqualTo(3));
Assert.That(array, Has.Exactly(2).GreaterThan(1));
Assert.That(array, Has.Exactly(3).LessThan(100));

有一些断言使用小函数和That都能实现,That的功能是小函数功能的超集。

Assert.AreEqual(4, 2 + 2);
Assert.That(2 + 2, Is.EqualTo(4));

常用的断言类

  • Assert:最基础的断言
  • StringAssert:与字符串这种类型相关的断言
  • CollectionAssert:集合断言
  • FileAssert:文件断言
  • DirectoryAssert:目录断言

如果自定义的类想要实现统一的断言,类的命名上可以使用XXXAssert的形式。

渲染和图形

透视相机和正交相机

Unity支持两种摄像机 透视相机和正交相机
透视相机:符合一般视觉规律,即近大远小、近清晰远模糊。相机像一个点一样观察世界。
正交相机:显示的对象不随距离变远而缩小的摄像机称为正交摄像机

获取camera

  • 使用Camera.main;
  • 使用名称获取Camera
    Camera mainCamera;//使用public字段,在Unity Editor里面绑定
    GameObject gameObject=GameObject.Find("Main Camera");
    mainCamera=gameObject.GetComponent<Camera>();
    
  • 使用ObjectType获取Camera(推荐)
    Camera mainCamera = FindObjectOfType<Camera>();
    

相机的关键参数

camera的位置和尺寸

  • camera.transform.posision//获取位置
  • camera.rect//视区的尺寸

相机的大小有两种设置方式,这两种方式其实是等价的。一种方式是设置相机到画布中心的距离,另一种方式是设置视区的高度。

  • 视区:viewport,表示相机能够看到的最远的那个面,一个四面椎体内的东西都是可见的。如果是2d应用,则视区是一个平面矩形。
  • aspect:宽高比,宽度除以高度。宽高比是系统参数,程序无法改变这个参数。
  • orthographicSize:视区的高度。程序可以改变这个参数。

屏幕的大小:

  • Screen.width
  • Screen.height

设置完orthographicSize之后,视区是一个坐标系。水平方向为x轴,竖直方向为y轴。x的范围是[-width,width],其中width=orthographicSize*aspect

画布字太小

更改画布的Canvas Scaler:设置为Scale With Screen Size。随着屏幕大小缩放画布。

Unity提供了几种光源,分别是什么

答: 四种。
平行光:Directional Light
点光源:Point Light
聚光灯:Spot Light
区域光源:Area Light

使用RayCast找到游戏对象

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit2D hit = Physics2D.Raycast (ray.origin, ray.direction, Mathf.Infinity);
if (hit.collider !=null) {
    Debug.Log (hit.collider.gameObject.name);

坐标转换

Camera下面有8个与点和坐标转换相关的函数:

  • Vector3 WorldToScreenPoint(Vector3 position)
  • Vector3 WorldToViewportPoint(Vector3 position)
  • Vector3 ViewportToWorldPoint(Vector3 position)
  • Vector3 ViewportToScreenPoint(Vector3 position)
  • Vector3 ScreenToWorldPoint(Vector3 position)
  • Vector3 ScreenToViewportPoint(Vector3 position)
  • Ray ViewportPointToRay(Vector3 pos)
  • Ray ScreenPointToRay(Vector3 pos, Camera.MonoOrStereoscopicEye eye)

其中涉及四个概念:

  • ScreenPoint:是一个二维点,表示屏幕上的坐标
  • ViewportPoint:是一个二维点,与摄像机的ViewportRect有关,是一个相对于摄像机的坐标系
  • WorldPoint:一个二维点,与摄像机有关。
  • Ray:一束光线

ViewportPoint是一个以左下角为原点的一个矩形,ViewportPoint的x和y的坐标取值都是0到1之间。
WorldPoint是一个以中心为原点的坐标系,在2d中,z的取值为相机的z坐标,xy为相机的视区坐标。

ClearFlags:重绘标志

什么叫重绘标志?相机所看到的是画布,画布每帧重绘的时候需要执行Clear()清空上一帧的东西。
ClearFlags表示重绘之前应该如何处理画布。

  • Skybox:天空盒,每帧重绘的时候,首先把天空盒画上去。
  • SolidColor:纯色,每帧重绘的时候,使用纯色绘制。
  • DepthOnly:仅深度
  • Don't Clear:不清除,表示每帧绘制的时候不执行Canvas.Clear()操作。如果有一个旋转的物体,则会看到这个物体的重影。

Culling Mask:

Culling:宰杀,部分捕杀
Culling Mask:选择性让相机渲染哪些Layer的物体

Layer:层级

Unity的场景是由GameObject组成的,每个GameObject可以指定Layer。其中前七个Layer是Unity内置的。

  • nothing:什么都没有,这个层空空如也
  • everything:什么都有,这个层包括全部的GameObject
  • default
  • transparent FX
  • ignore raycast
  • water
  • UI

一个相机的CullingMask是一个int数字,所以Unity最多支持32个层。

1.用于只渲染某一层
_camera.cullingMask = 1<<8; //cube 只渲染第八层
_camera.cullingMask = 1<<9; //sphere 只渲染第九层
_camera.cullingMask = 1<<10; //capsule 只渲染第十层
只渲染第8、9、10层
_camera.cullingMask = (1 << 10) + (1<<9) +(1<<8);

2.渲染所有层
_camera.cullingMask = -1; //对应 everything

3.任何层都不渲染
_camera.cullingMask = 0; //对应 nothing

4.在原来基础添加某一层
_camera.cullingMask |= (1 << 10); //在原来的基础上增加第10层

5.在原来基础减去某一层
_camera.cullingMask &= ~(1 << 10); //在原来的基础上减掉第10层
6.渲染除了某一层外的所有层
_camera.cullingMask = ~(1 << 10); //渲染除第10层之外的其他所有层
**

场景设置天空盒

在Window/Render/Lightning打开窗口,设置Scene/Environment,选择SkyboxMaterial。
创建Material,设置Shader为Skybox下面的6面体模式,使用6张图片作为背景图。

九宫格图片可以实现一张图片铺满整个平面。
它有left、right、top、bottom四个参数 。

img.png

计算机图形学 三十九:写出光照计算中的diffuse的计算公式
答:diffuse = Kd x colorLight x max(N*L,0);Kd 漫反射系数、colorLight 光的颜色、N 单位法线向量、L 由点指向光源的单位向量、其中N与L点乘,如果结果小于等于0,则漫反射为0。

四十:LOD是什么,优缺点是什么?
答:LOD(Level of detail)多层次细节,是最常用的游戏优化技术。它按照模型的位置和重要程度决定物体渲染的资源分配,降低非重要物体的面数和细节度,从而获得高效率的渲染运算。

四十一:两种阴影判断的方法、工作原理。
本影和半影:
本影:景物表面上那些没有被光源直接照射的区域(全黑的轮廓分明的区域)。
半影:景物表面上那些被某些特定光源直接照射但并非被所有特定光源直接照射的区域(半明半暗区域)
工作原理:从光源处向物体的所有可见面投射光线,将这些面投影到场景中得到投影面,再将这些投影面与场景中的其他平面求交得出阴影多边形,保存这些阴影多边形信息,然后再按视点位置对场景进行相应处理得到所要求的视图(利用空间换时间,每次只需依据视点位置进行一次阴影计算即可,省去了一次消隐过程)

四十二:Vertex Shader是什么,怎么计算?
答:顶点着色器是一段执行在GPU上的程序,用来取代fixed pipeline中的transformation和lighting,Vertex Shader主要操作顶点。 Vertex Shader对输入顶点完成了从local space到homogeneous space(齐次空间)的变换过程,homogeneous space即projection space的下一个space。在这其间共有world transformation, view transformation和projection transformation及lighting几个过程。

四十三:MipMap是什么,作用?
答:MipMapping:在三维计算机图形的贴图渲染中有常用的技术,为加快渲染进度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为MipMap。

什么是LightMap?
答:LightMap:就是指在三维软件里实现打好光,然后渲染把场景各表面的光照输出到贴图上,最后又通过引擎贴到场景上,这样就使物体有了光照的感觉。

简述水面倒影的渲染原理
答: 原理就是对水面的贴图纹理进行扰动,以产生波光玲玲的效果。用shader可以通过GPU在像素级别作扰动,效果细腻,需要的顶点少,速度快

更改正方体的颜色

添加一个3D Cube,如何设置这个Cube的颜色。
创建一个Material,把Material放到Cube上面替换它的默认的Material。这个Material实际上与prefab语义相同,每个GameObject都持有一个新的Material。GameObject上的所有的组件实际上都是实例,修改一个GameObject的Material不会影响其它使用这个Material的GameObject。

如何获取Material?
Material本身并不是一个组件, MeshRenderer是一个MonoBehavior,Material是MeshRenderer的一个成员变量。

更改Canvas/Panel的透明度

在Unity中,一切颜色都是RGBA。Panel有一个Image组件,只需要设置Panel的Image的颜色透明度即可。

模型Model

模型是包含有关 3D 对象(如角色、地形或环境对象)的形状和外观的数据的文件。模型文件可包含各种数据,包括网格、材质和纹理。对于动画角色,它们还会包含动画数据。您通常在外部应用程序中创建模型,然后将它们导入到 Unity 中。

模型=网格+材质+纹理。

网格:3D物体的骨骼。描述了3D物体的形状。
材质:3D物体的皮肉。
纹理:3D物体的纹身。例如斑马身上的花纹。

常见的模型文件的格式:pbx文件。

PBR

Physically Based Render:基于物理的渲染。过去有过一些基于经验的渲染。

网格Mesh

包含了顶点,面,法线,UV ,切线等数据,构成了物体的骨架。
其中面是顶点三元组。

UV

UV就是二维,UV决定了texture如何包裹在三维Mesh上。

纹理Texture

就是图形学中的纹理,主要是凹凸的花纹和光滑面上彩色的图像,在UI中通常指的是2维图片 Unity中自带有纹理导入器,通过选中要修改的纹理,会自动在Inspector中显示纹理属性,更改TextureType来创建Sprite

要从数据的角度看待贴图(texture),而不仅仅把它当做皮肤。 贴图=纹理=texture。贴图应该从数据维度去看待,但是通常意义上的贴图指的是baseColor维度的材质。 PBR:physical based rendering.

材质Material

它是表面各可视属性的结合,这些可视属性是指表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等,通过纹理和shader 以及属性的组合形成一种,正是由于材质的定义才让3D世界的物体更具有真实世界的样子。

Material是材质,shader(材质,光照)=>最终渲染。 shader是算法,材质是数据。shader决定了如何把材质数据利用起来。 材质,就是一堆texture。texture不仅仅描述一个图片,而是表示数据。baseColor(也称颜色贴图)、法线map(normal)、粗糙程度(roughness)、金属度(metalic)、发光(emitness)等每一个维度都可以看做是一个texture。

常见的Material的分量:

  • Diffuse/Albedo/BaseColor:漫反射
  • Reflection/Specular:反射
  • Metalness:金属度
  • Glossiness:光泽度
  • Roughness:粗糙度
  • Normal:法线
  • Displacement/Height:置换
  • Bump:凹凸
  • Ambient Occlusion:环境光遮蔽

着色器shader

在 Unity 中,着色器分为三大类。每个类别的用途不同,使用方式也不同。

  • 作为图形管线一部分的着色器是最常见的着色器类型。它们执行一些计算来确定屏幕上像素的颜色。在 Unity 中,通常是表面着色器和顶点片段着色器。
  • 计算器在常规图形管线之外,在 GPU 上执行计算。
  • 光线追踪着色器执行与光线追踪相关的计算。

shadergraph、vfx

现在的shadergraph和vfx都是以可视化的形式去编辑shader,不需要写代码。

SBSAR文件

.SBSAR 文件是.SBS 文件的发布版本。它被优化为只存储渲染它存储的物质所需的信息,并且不能被编辑。要在substance Designer中发布物质(创建.SBSAR 文件),请执行以下步骤: 在资源管理器中选择SBS文件。 单击“发布”图标或右键单击并选择导出。 命名存档。 选择压缩和参数曝光选项。 单击“确定”。 .SBSAR 文件可以由多种程序打开,包括Substance Designer、Substance Painter、Unity、Unreal Engine 4、玛雅、3ds Max、Cinema 4D、CATIA和TouchDesigner。 您可以在Substance Designer中打开一个.SBSAR 文件,方法是选择文件→打开。。。。您可以通过选择文件→导入资源…,将文件拖放到Substance Painter中的“工具架”窗格中,或将其放置在以下目录中的“材料”文件夹中,将.SBSAR 文件添加到Substance Painter中: 文件/物质油漆工/架子/ 新添加的.SBSAR 文件中存储的材料将在Substance Painter的“工具架”窗格中可用。

常见3d模型格式

  • obj:只包含3d模型数据,不包括动画、材质、贴图等数据。
  • fbx:obj的升级版,包含动画、材质、贴图等信息。
  • usdz:压缩率较高
  • glTF:web上的3d对象标准,包含动画、材质、灯光等属性。分为glb和gltf两种,glb全部数据都在二进制文件中,文件小;gltf有一个额外的bin文件,它相当于一个meta文本文件。
  • GLB
  • maya

常用软件

  • maya
  • 3dmax
  • blender

常用素材网站

爱给网:收费 https://www.aigei.com/home/mark free3D:免费 强烈推荐 https://free3d.com/zh/3d-models/%E9%BE%99 sketchFab: https://sketchfab.com/tags/unity3d open3dmodels :https://open3dmodel.com/3d-models/unity 模型网:https://www.cgmodel.com/model/all_1_0_0_0_0_0_0_0_0_0_0_1_0_0_0_0_0_0_0_0_0/?order=rmdAsc CG小霸王:http://www.cgbawang.com/member.php?mod=logging&action=login

SubstanceDesigner

一个用来编辑材质的工具,adobe出品。

在Unity中如何导入sbsar文件? 点击Window,选择Asset Store, 在商店中搜索Substance in Unity。

学习资料

https://github.com/cymheart/3DMahjong

显示

人脑的信息输入主要靠视觉、听觉。视觉输入的信息占比更大。视觉像面条、像馒头一样,是主食;而听觉像调味剂,可以让视觉更有意思。
计算机显示出东西来,这个过程涉及到很多层。最底层是显示器,显示器就是一块固定大小的内存,这块内存空间在不停地刷新,就形成了我们看到的屏幕。那么是谁在往这块内存里面不停地送数据呢?是显卡,显卡通过计算得到一堆内存数据交给显示器去展示。显卡依旧是硬件层面。
是谁告诉显卡应该计算那些东西呢?是图形库,图形库对各个厂家的显卡做了屏蔽,这是第一层软件层。几乎所有的操作系统都需要依赖某种图形库。

显卡

GPU,Graphic Processing Unit.
计算机中用于渲染图像的处理器,分为集成显卡和独立显卡,二者的区别就是独立显卡不会占用CPU性能,目前大部分机器都是独立显卡。
显卡厂商:

  • 英伟达Nvidia
  • Intel

图形API

  • OpenGL:老牌开源图形API
  • DirectX:微软出品,Windows平台使用广泛。
  • Vulkan:比较新的图形API
  • Skia:Google开源图形库,底层基于Vlukan和OpenGL。flutter就是基于Skia图形库实现的。skia是一个2d图形库,用于绘制文本、几何图形、图片等。Jetbrain退出了skia的Java封装: https://github.com/JetBrains/skija
  • WebGl:web端的图形API,底层一般使用OpenGL等最底层的库。
  • OpengIES
  • Metal

其中,图形API也分底层和高层,底层的图形API被称为显卡驱动,例如:

  • OpenGL
  • DirectX
  • Vulkan

GPU流水线顺序

GPU的图形(处理)流水线完成如下的工作:(并不一定是按照如下顺序)
顶点处理:这阶段GPU读取描述3D图形外观的顶点数据并根据顶点数据确定3D图形的形状及位置关系,建立起3D图形的骨架。在支持DX8和DX9规格的GPU中,这些工作由硬件实现的Vertex Shader(定点着色器)完成。
光栅化计算:显示器实际显示的图像是由像素组成的,我们需要将上面生成的图形上的点和线通过一定的算法转换到相应的像素点。把一个矢量图形转换为一系列像素点的过程就称为光栅化。例如,一条数学表示的斜线段,最终被转化成阶梯状的连续像素点。
纹理帖图:顶点单元生成的多边形只构成了3D物体的轮廓,而纹理映射(texture mapping)工作完成对多变形表面的贴图,通俗的说,就是将多边形的表面贴上相应的图片,从而生成“真实”的图形。TMU(Texture mapping unit)即是用来完成此项工作。
像素处理:这阶段(在对每个像素进行光栅化处理期间)GPU完成对像素的计算和处理,从而确定每个像素的最终属性。在支持DX8和DX9规格的GPU中,这些工作由硬件实现的Pixel Shader(像素着色器)完成。
最终输出:由ROP(光栅化引擎)最终完成像素的输出,1帧渲染完毕后,被送到显存帧缓冲区。
总结:GPU的工作通俗的来说就是完成3D图形的生成,将图形映射到相应的像素点上,对每个像素进行计算确定最终颜色并完成输出。

什么是渲染管道?

答:是指在显示器上为了显示出图像而经过的一系列必要操作。 渲染管道中的很多步骤,都要将几何物体从一个坐标系中变换到另一个坐标系中去。
主要步骤有:
本地坐标->视图坐标->背面裁剪->光照->裁剪->投影->视图变换->光栅化。

渲染管线

RP:render pipeline
SRP:scriptable render pipeline
HDRP:高精度渲染管线,high definition
URP:universal渲染管线。 universal render pipeline。

GPU和CPU

GPU和CPU各自具有优势和缺点:

优点缺点指令
GPU只能做一些简单操作一些简单操作单指令多数据
CPU运行缓慢可以做一些复杂操作单指令但数据

GPU就像写numpy,CPU就像用Python标准库写循环。

什么是着色器?

着色器是用来渲染图形的一种技术,通过shader可以控制每一个像素的渲染方式,从而实现自定义显卡渲染画面的算法。

drawCall是CPU调用GPU执行一次绘制的命令。
shader就是GPU流水线上一些高度可编程的阶段,我们可以通过Shader控制流水线中的渲染细节。

shader怎么用

shader及其配置打包形成Material材质。材质赋予到三维模型上就可以输出了。

三维模型等于形状+材质。材质的实现方式之一就是shader。贴图可以作为shader的输入,shader能够把贴图等一些原始资源整合起来。

着色器分类

着色器有两种:顶点Shader和像素Shader。顶点着色器确定了多面体的顶点,像素着色器决定了多面体每个面的颜色。

  • 顶点着色器:VertexShader。3D图形是由很多个三角面片组成的,顶点Shader就是计算每个三角面片上的顶点,为像素渲染做准备。
  • 像素着色器(也叫片元shader、片段shader):PixelShader、FragmentShader。以像素为单位,计算光照、颜色。

常用的着色语言

  • openCV:GLSL,GL shading language
  • directX:微软出品,HLSL,Hight Level Shading Language,高级着色语言
  • nvidia:CG,C for Graphics,用于图形的C语言,兼容directX和openCV,它对GLSL和HLSL做了进一步封装,是微软和英伟达互协作在标准硬件光照语言的语法和语义上达成的一种一致性协议。unity就采用这种语言。

ShaderLab

为了自定义渲染效果,我们往往需要和大量的文件和设置打交道,而Unity Shader为我们提供了一层抽象来封装这些打交道的细节,即Unity Shader。而我们和这层抽象打交道的语言就是ShaderLab。它使用一系列的语义块(比如Property, SubShader)来描述一个Unity Shader文件,Unity在背后会根据使用的平台来把这些结构编译成真正的代码和Shader文件,而开发者只需要和Unity打交道即可。 作者:2025AT https://www.bilibili.com/read/cv14408919 出处:bilibili

表面着色器:以牺牲性能的前提下方便了渲染编写,是对顶点/片元着色器的一种封装。

表面着色器的代码直接定义在了SubShader模块下。

顶点/片元着色器:顶点/片元着色器的代码需要定义在Pass语义块内,灵活性高,性能好,但代价是编写麻烦。

Unity Shader是用ShaderLab语言编写,但是在SubShader内部,我们需要嵌套CG/HLSL语言去定义渲染内容的细节。

Unity的shader

Unity中支持三种Shader,分别是:

  • Standard Surface Shader:标准表面着色器
  • Unlit Shader:无光照着色器
  • Image Effect Shader:图片效果着色器

unity的着色器语言叫做shaderlab,其实就是CG(C for Graphics)。

  • surface shader:是对vertexFragment Shader的封装,它实际上是对vertex/fragment shader的封装,只要学会了vertex/fragment shader就能够掌握serface shader。
  • vertex 和fragment shader
  • fixed function shader:是一种比较比较底层的shader,已经被淘汰了,完全没有学习的必要。

综上,unity的shader虽然多,但是真正需要了解得只有vertex/fragment shader。

Unity3D Shader分哪几种,有什么区别?

答:表面着色器的抽象层次比较高,它可以轻松地以简洁方式实现复杂着色。表面着色器可同时在前向渲染及延迟渲染模式下正常工作。 顶点片段着色器可以非常灵活地实现需要的效果,但是需要编写更多的代码,并且很难与Unity的渲染管线完美集成。 固定功能管线着色器可以作为前两种着色器的备用选择,当硬件无法运行那些酷炫Shader的时,还可以通过固定功能管线着色器来绘制出一些基本的内容。

写法

Shader的名称

可以通过在Shader名字中加("/")来控制Unity Shader出现的位置: 比如:"Custom/MyShader"在材质面板的位置就是Shader-Custom-MyShader

Properties

Properties语义块中声明的属性是在Inspector面板可以直接调节的属性 声明格式:Name("Display name", PropertyType) = DefaultValue

SubShader

一个Unity Shader文件中可以包含多个SubShader文件,但至少要有一个。Unity会选择第一个能够在目标平台上运行的SubShader,如果都不支持,会调用Fallback语义指定的SubShader。 SubShader中包含RenderSetup,Tags和Pass通道。

  • RenderSetup用于设置逐片元操作中的流程,比如关闭深度写入,开启混合模式等。

  • Tags则用于设置渲染方式:

subshader是啥呢,算法,就是写给GPU渲染的shader片段了,这里记住,一个shader当中至少有一个subshader。每一次显卡进行处理的时候呢,只能选择其中一个subshader去执行。那为什么会有多个subshader呢?这和硬件有关。

在读取shader的时候,会先从第一个subshader读取,如果第一个能适配当前硬件,就不会往下读了;如果硬件太老跟不上,第一个读取不了,就会读取第二个看能不能与我适配。也就是说,subshader的所有方案会向下简化。如果这些列举的subshader都用不了怎么办?那就是第三个Fallback了。

漫反射贴图,diffuse map,albedo map
法线贴图,normal map
高度贴图,height map
光泽贴图,gloss map 环境贴图,environment map
光照贴图,lighting map

法线贴图是一种在低多边形网格下实现更精细的凹凸表面渲染的技术。一张法线贴图与漫反射贴图不同之处在于,法线贴图的三个通道上分别存储的是该点法线的 X, Y, Z 坐标。通过存储的法线向量改变光照计算时反射方向,从而产生凹凸不平的视错觉。

高度贴图是一种顶点位移技术,网格根据高度图的信息进行相应的位移,从而产生了大幅度的凸起或凹陷,通常与法线贴图一起使用,可以产生更逼真的渲染效果。

光泽贴图用于定义不规则的高光区域,可用于展示更逼真的物体表面。

光照贴图定义了每个纹素处的光照幅度信息。可以在光源静止的前提下计算出来,以节省动态光照计算的开销。光照贴图配合法线、高度、与漫反射贴图,提供了更逼真的物体表面渲染表现。不过光照贴图无法应对来自环境的光与遮挡。

环境贴图用于展示户外环境。将游戏世界包围起来的假想立方体通常给了游戏世界塑造了天空或地下世界的穹顶,也即游戏中常说的天空盒(Sky Box)。这种贴图包含六个立面,我们在游戏世界中看到的美丽天空实际上是立方体的六个绘面。

材质(material)则是对一个物体表面的视觉特性的完整描述,包括了网格表面的各项纹理设置、选用的着色器(Shader)以及着色器的输入参数。光照是渲染的中心,也是俗称画面“质感”的核心因素。着色(Shading)通常是光照加上其他视觉效果的泛称。因此着色还包含了以过程式(Procedural)顶点变形表现的水面动态、随风摆动的草丛、高次曲面(high order surface)镶嵌以及许多渲染场景的计算。材质直接影响了物体表面的画面表现。

粒子系统(Particle System)则用分散在一定立体或平面区域内的带材质的细小物体,表现诸如火焰、火花、爆炸、浪花、烟雾、云朵、雪花、荧光的不规则细小运动体。

物理

物理引擎、渲染引擎是游戏引擎的重要组成部分。

Unity内置的物理系统:

  • 3d物理系统是Nvidia PhysX
  • 2d物理系统是Box2D引擎,box2d引擎是2D物理引擎中使用最广泛的,JS侧也有这样的box2d封装,跟PIXIJS搭配使用可以实现很炫酷的效果。

2d虽然是3d的子集,如果使用3d引擎去处理2d,会带来一些性能的损失。

物理引擎的内容

Unity3D的物理引擎封装了Nvidia PhysX引擎。

物理引擎包含的主题有:

  1. 角色控制:主要用于第三人称玩家控制或者不使用刚体物理组件的第一人称玩家控制。
  2. 刚体物理
  3. 碰撞
  4. 连接
  5. 关节
  6. 布娃娃系统
  7. 布料
  8. 多场景物理

torque:力矩

链条关节

hinge joint:可以模拟两个物体间用一根链条连接在一起的情况,能保持两个物体在一个固定距离内部相互移动而不产生作用力,但是达到固定距离后就会产生拉力。

Physic Material

DynamicFriction 滑动摩擦 Static Friction 摩擦力 Bounciness 弹力 Friction Combine 相互摩擦计算 Bounce Combine 碰撞弹力

角色控制器的适用范围:

  1. 第三人称
  2. 不使用刚体的第一人称

角色控制器是什么?角色控制器是一个胶囊,也就是一个圆柱+球体。

通过角色控制器的属性,很容易理解角色控制器的用途。
形状相关的属性:

  1. center:中心
  2. radius:半径
  3. height:高度

碰撞相关

  1. skin width:皮肤厚度,两个碰撞体碰撞的时候可以陷入进对方的皮肤。

运动相关:

  1. slope limit:碰撞体爬坡的斜率
  2. step offset
  3. min move distance:最小移动距离,如果让角色移动的距离太小,则不执行这个动作。这种方式可以减少抖动。

如果要通过角色控制器来推动刚体或对象,可以编写脚本通过 OnControllerColliderHit() 函数对与控制器碰撞的任何对象施力。

刚体的重要属性

刚体和质点是物理学中的重要概念,质点把物体抽象为一个点,忽略了物体的自身形状。
刚体则具体考虑物体的形状,它包括运动、重力、碰撞等属性。

如果启用了刚体的质量、dynamic等属性,则刚体的位置和旋转都是由物理计算出来的,开发者不应该直接设置刚体的物理和旋转。
开发者可以给刚体施加力。一句话总结就是,改变游戏物体的Transform这一动作,在每一个阶段都应该由特定控制器负责,如果多个控制器同时修改物体的Transform,会造成游戏物体的瞬移、抖动等不可控情况。

如果不想使用重力

  1. 直接把物体的重力系数设置为0即可。
  2. 把物体设置为kinematic,只有运动,没有质量。物体的Transform属性不会被物理引擎所驱动,只能通过代码修改Transform属性对其进行操作。
  3. 把物体的UseGravity设置成0

给物体施加力

rigidbody.AddForce rigidbody.AddForceAtPosition

刚体(RegidBody)的三种类型

  • dynamic:有质量、有速度
  • kinematic(运动学的):只有速度没有质量
  • static:只有质量没有速度,永远静止的物体。

只有有质量的两个物体才有可能相撞,没有质量的两个物体无法相撞。

Dynamic:这种刚体类型具有可用的全套属性(例如有限质量和阻力),并受重力和作用力的影响。Dynamic刚体类型将与每个其它刚体类型碰撞,是最具互动性的刚体类型。这是需要移动的对象的最常见刚体类型,因此是2D刚体的默认刚体类型。此外,由于具有动态性并与周围所有对象互动,因此也是性能成本最高的刚体类型。选择此刚体类型时,所以2D刚体属性均可用。 Kinematic:这种类型的2D刚体仅在非常明确的用户控制下进行移动。虽然Dynamic2D刚体受重力和作用力的影响,但Kinematic2D刚体并不会受此影响。因此,Kinematic2D刚体的速度很快,与Dynamic比,对系统资源的需求更低。 Kinematic2D刚体仍然通过速度移动,但是此速度不受作用力和重力的影响。Kinematic2D刚体不会与其它Kinematic2D刚体和Static2D刚体碰撞,只会与Dynamic2D刚体碰撞。与Static2D刚体(见下文)相似,Kinematic2D刚体在碰撞期间的行为类似于不可移动的对象(就像具有无限质量)。选择此刚体类型时,与质量相关的属性将不可用。 (简单来说,Kinematic只能通过更改刚体的速度属性来改变位置,不会受到其它物理效果的影响。) Static2D刚体设计为在模拟条件下完全不动(Play模式);如果任何对象与Static2D刚体碰撞,此刚体类型的行为类似于不可移动的对象(就像具有无限质量)。此刚体类型也是使用资源最少的刚体类型。Static刚体只能与Dynamic2D刚体碰撞。不可支持两个Static2D刚体进行碰撞,因为这种刚体不是为了移动而设计的。

Rigidbody组件的属性

Mass 质量:用来模拟物体的质量,单位为千克。质量会影响惯性大小,而且质量不同的两个刚体相互碰撞时,它们各自的反应也不同。要注意物体的质量大小不会影响自由落体时的速度,想想伽利略的两个铁球同时落地。

Drag 空气阻力:物体受力移动时受到的空气阻力大小,0 表示没有空气阻力。如果想让两个物体自由落体时速度不同,就给它们设置不同的空气阻力。空气阻力越大,物体感觉起来就越轻,比如铁块的空气阻力可以设置为 0.001,羽毛的空气阻力可以设置为 10。这个值一般不需要设置,如有特殊需要的话,就参考铁块和羽毛的阻力范围进行设置。Drag可以理解为"拉风",风对它的阻力。

Angular Drag 角阻力:物体受力旋转时受到的空气阻力大小。因为物体旋转时的速度叫做角速度,所以旋转时受到的阻力就叫做角阻力。这个属性和 Drag 空气阻力属性类似, Drag 空气阻力属性会影响物体在空气中移动时的速度,Angular Drag 角阻力会影响物体在空气中旋转时的速度。

Use Gravity 启用重力:物体是否受重力影响。受重力影响的物体会进行自由落体。

Is Kinematic 运动学:如果我们只是需要让物理系统进行碰撞检测,不需要使用物理系统控制游戏对象,而是在脚本中使用 Transform 控制物体,此时我们可以勾选刚体组件中的 Is Kinematic 属性。

Interpolate 插值:物体在进行物理运动时的插值方式,默认是 None 不使用插值。

  • None 不使用插值:这个选项是默认值,一般情况下此选项不需要设置。如果物体在物理系统控制下,会发生轻微的抖动,可以尝试使用另外两种选项。
  • Interpolate 插值:这个选项会根据上一帧中物体的 Transform 来计算当前帧的插值。
  • Extrapolate 向后推断:这个选项会推断下一帧中物体的 Transform 来计算当前帧的插值。

Collision Detection 碰撞检测:碰撞检测模式,默认是 Discrete 离散检测。

  • Discrete 离散检测:对场景中的其它碰撞体使用离散碰撞检测。一般情况下我们不需要修改此属性。物理系统默认的碰撞检测是离散的,所以有时高速移动的刚体会穿透障碍物,此时我们才需要使用另外两种碰撞检测模式。
  • Continuous 连续检测:刚体对其它没有刚体的碰撞器采用连续检测,防止刚体速度过快穿透障碍物。比如快速飞行的子弹碰撞静止的墙壁,此时如果采用 Dscrete 离散检测,子弹不会打在墙壁上反弹,而是会穿透墙壁。但该模式下并不能正确检测两颗高速飞行的子弹相互碰撞,因为它们两个都带有刚体,此时请使用第三种模式。要注意连续检测会影响物理性能,非必要情况请使用默认的 Discrete 离散碰撞检测。
  • Continuous Dynamic 连续动态检测:会对其它刚体(碰撞检测模式不能是 Discrete 离散碰撞检测)进行连续检测,防止互相穿透。对没有刚体的碰撞器也会采用连续碰撞检测

Constraints 限制:对刚体的运动进行限制。 Freeze Position 冻结位移:当刚体移动时,可选地忽略在某个轴上的位移。有 3 个可勾选属性 X、Y、Z,分别表示在 X 轴、Y 轴和 Z 轴方向上的位移。如果勾选了 Y 则表示,刚体受力运动后,不会在 Y 轴方向上发生位移。 Freeze Rotation 冻结旋转:当刚体旋转时,可选地忽略绕某个轴的旋转。也有 3 个可勾选属性,分别表示绕 X 轴旋转、绕 Y 轴旋转和绕 Z 轴旋转。如果勾选了 X 则表示,刚体受力运动后,不会绕 X 轴发生旋转。

刚体的睡眠

当刚体移动速度低于规定的最小线性速度或转速时,物理引擎会认为刚体已经停止。发生这种情况时,游戏对象在受到碰撞或力之前不会再次移动,因此将其设置为“睡眠”模式。这种优化意味着,在刚体下一次被“唤醒”(即再次进入运动状态)之前,不会花费处理器时间来更新刚体。

在大多数情况下,刚体组件的睡眠和唤醒是透明发生的。但是,如果通过修改__变换__位置将静态碰撞体(即,没有刚体的碰撞体)移入游戏对象或远离游戏对象,则可能无法唤醒游戏对象。这种情况下可能会导致问题,例如,已经从刚体游戏对象下面移走地板时,刚体游戏对象会悬在空中。在这种情况下,可以使用 WakeUp 函数显式唤醒游戏对象。有关睡眠的更多信息,请参阅刚体和 2D 刚体组件页面。

恒力组件

Unity提供了一个Constant Force 组件,这个组件如果开发者自己实现也用不了几行代码。
它的属性包括:力、相对力、力矩、相对力矩。

碰撞体的形状

  1. 立方体
  2. 胶囊:椭球
  3. 网格
  4. 车轮
  5. 地形碰撞体

三维弹性碰撞

private Vector3 m_preVelocity = Vector3.zero;//上一帧速度

public void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.name == "wall")
    {
        ContactPoint contactPoint = collision.contacts[0];
        Vector3 newDir = Vector3.zero;
        Vector3 curDir = transform.TransformDirection(Vector3.forward);
        newDir = Vector3.Reflect(curDir, contactPoint.normal);
        Quaternion rotation = Quaternion.FromToRotation(Vector3.forward, newDir);
        transform.rotation = rotation;
        rigidbody.velocity = newDir.normalized * m_preVelocity.x / m_preVelocity.normalized.x;
    }
}

物体碰撞

更改transform.position而不给物体添加速度,物体是不会碰撞的。 unity的component是有顺序的。 刚开始学习unity的时候,老师说translate的移动方式不会受到碰撞影响,最近有个项目,要让碰撞起作用,测试之后,发现用translate移动会受到碰撞限制。 移动的物体上有Collider和Rigidbody组件,Rigidbody组件中 Collision Detection最好选择Continuous Dynamic模式。被碰撞的物体具有Collider组件。

Unity3d中的碰撞器(Collider)和触发器()的区别?

答:碰撞器是触发器的载体,而触发器只是碰撞器身上的一个属性。 当Is Trigger=false时,碰撞器根据物理引擎引发碰撞,产生碰撞的效果,可以调用OnCollisionEnter/Stay/Exit函数; 当Is Trigger=true时,碰撞器被物理引擎所忽略,没有碰撞效果,可以调用OnTriggerEnter/Stay/Exit函数。 如果既要检测到物体的接触又不想让碰撞检测影响物体移动或要检测一个物件是否经过空间中的某个区域这时就可以用到触发器

物体发生碰撞的必要条件

答:两个物体都必须带有碰撞器Collider,其中一个物体还必须带有Rigidbody刚体。

碰撞发生的几个阶段

  • OnCollisionEnter
  • OnCollisionStay
  • OnCollisionExit

Unity为什么没有提供圆柱体的collider?

https://forum.unity.com/threads/why-cylinder-collider-doesnt-exist.63967/

核心原因:圆柱体的碰撞检测实现困难。

正方体、球体、椭球体可以非常简便地判断是否相交,而圆柱体判断相交比较复杂。
算法题:

  • 给定空间中两个长方体,判断它们是否存在公共部分。
  • 给定空间中两个球体,判断它们是否存在公共部分。
  • 给定空间中两个椭球体,判断它们是否存在公共部分。
  • 给定空间中两个圆柱体,判断它们是否存在公共部分。

fixed joint:固定连接

hinge joint:合页连接

门,可以旋转某个角度。

关节的类型

  • 固定关节:fixed joint
  • 弹性关节:spring joint
  • 铰链关节:hinge joint,举例:门,链条,钟摆
  • 角色关节:character joint,举例:用于布娃娃系统

布娃娃系统

ragdoll system 通过设置刚体的关节,让人物可以实现走路。

官方文档:https://docs.unity3d.com/cn/current/Manual/RagdollStability.html

布娃娃系统是游戏中模拟人体运动的一种仿真机制。因为整个设计酷似布娃娃而得名。

动画的实现方式有很多种,而布娃娃系统则是基于物理实现动画,这是一种非常真实的动画生成方式。

布娃娃系统的13块骨头

Unity 3D右键创建3D Object,可以直接创建一个布娃娃系统。布娃娃系统创建完成之后,会为指定的13块骨头添加一些刚体属性,布娃娃系统本身并不是一个GameObject,只是一些关系的集合。如果对于刚体比较精通,开发者自己也可以写一个脚本为一堆物体设置刚体属性,这样得到的效果与布娃娃系统是一致的。

如果修改了布娃娃系统的材质,一般需要重新设置布娃娃系统的关系。例如修改了各个骨头的位置、尺寸,都需要重新设置布娃娃系统。

一个布娃娃系统一共有3+2*5=13块大骨头。

pelvis:骨盆

middle spine:脊椎

head:头

四肢:

(left+right)*(hips+knee+foot+arm+elbow)

hips:大腿

knee:小腿

foot:脚丫子

arm:上臂

elbow:下臂

汉语优于英语的地方就在于汉语能够用最少的字描述尽量丰富的意思,使用上下、大小就足以描述一切,而英语却为每一个事物单独起一个名字。

注意事项

布娃娃系统的坑

播放布娃娃的时候要把animator勾掉,不然不生效

部位穿插拉伸,要把部位Character Joint组件上面的Enable Projection勾上

激活布娃娃弹得很高,就是刚体的速度,把rigidbody的速度置为0

如果布娃娃动作僵硬,肯定用错骨骼来绑定布娃娃导致的

布娃娃穿透场景碰撞的问题,其实这个由于速度太快引起的,可以设置rigidbody的碰撞检测模式collisionDetectionMode

受力不要全部部位都给力,一般都是给一个部位就够了,不然各种力作用引起怪异表现

关于倒地之后抖动问题,这个只能加个检测,如果所有刚体速度小于某个数则关闭刚体功能

布娃娃系统的开启和关闭

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RangdollController : MonoBehaviour {

private Animator Anim;
// Use this for initialization
void Start () {
    Anim = GetComponent<Animator>();
    InitRagdoll();
}

// Update is called once per frame
void Update () {
    
}

public List<Rigidbody> RagdollRigidbodys = new List<Rigidbody>();
public List<Collider> RagdollColliders = new List<Collider>();
/// <summary>
/// 初始化,遍历角色身体上的所有ragdoll并存储 此时关闭布娃娃系统
/// </summary>
void InitRagdoll()
{
    Rigidbody[] Rigidbodys = GetComponentsInChildren<Rigidbody>();
    for (int i = 0; i < Rigidbodys.Length; i++)
    {
        if (Rigidbodys[i] == GetComponent<Rigidbody>())
        {
            //排除正常状态的Rigidbody
            continue;
        }
        //添加Rigidbody和Collider到List
        RagdollRigidbodys.Add(Rigidbodys[i]);
        Rigidbodys[i].isKinematic = true;
        Collider RagdollCollider = Rigidbodys[i].gameObject.GetComponent<Collider>();
        RagdollCollider.isTrigger = true;
        RagdollColliders.Add(RagdollCollider);
    }
}
/// <summary>
/// 启动布娃娃系统
/// </summary>
public void EnableRagdoll()
{
    //开启布娃娃状态的所有Rigidbody和Collider
    for (int i = 0; i < RagdollRigidbodys.Count; i++)
    {
        RagdollRigidbodys[i].isKinematic = false;
        RagdollColliders[i].isTrigger = false;
    }
    //关闭正常状态的Collider
    GetComponent<Collider>().enabled = false;
    //下一帧关闭正常状态的动画系统
    StartCoroutine(SetAnimatorEnable(false));
}
/// <summary>
/// 关闭布娃娃系统
/// </summary>
public void DisableRagdoll()
{
    //关闭布娃娃状态的所有Rigidbody和Collider
    for (int i = 0; i < RagdollRigidbodys.Count; i++)
    {
        RagdollRigidbodys[i].isKinematic = true;
        RagdollColliders[i].isTrigger = true;
    }
    //开启正常状态的Collider
    GetComponent<Collider>().enabled = true;
    //下一帧开启正常状态的动画系统
    StartCoroutine(SetAnimatorEnable(true));
}
IEnumerator SetAnimatorEnable(bool Enable)
{
    yield return new WaitForEndOfFrame();
    Anim.enabled = Enable;
}

布娃娃系统没有Animator组件

布娃娃系统的使用场景一般只在角色死亡时候有用。

没有Animator组件,意味着该Ragdoll系统只能单纯受物理驱动,无法与动画融合(因此Unity官方也建议仅将该系统用于角色死亡时表现)

不要大量使用布娃娃系统

游戏相比普通UI最大的特点就是耗费性能较多,每一帧都需要刷新。

游戏耗费性能体现在渲染上,渲染上耗费性能的两大来源:光照和物理。

物理也是耗费性能的重要来源。物理涉及到刚体计算,如果游戏中刚体特别多,计算量就比较大。

通过设置isKinemetic来控制布娃娃的存活和死亡

一般布娃娃需要设置isKinemetic为True,避免让重力打摊布娃娃。

最简单的布娃娃系统

最简单的布娃娃系统实际上并不是13块骨头。而是6块骨头。

头、左手、右手、左腿、右腿、躯干。

这是非常自然的、我最初设计的布娃娃系统。

问题

布娃娃系统一下子就瘫倒在地

把HEAD的isKinemetic设置为True,则头不受重力作用,布娃娃就会站立着。

更专业的ragdoll插件:Ragdoll Mecanim Mixer

Ragdoll Mecanim Mixer 插件正是为了解决上面的这些痛点而出现的,对于这些问题,该插件都实现了很好的解决方案(该插件可在Asset Store购买,目前Unity最好的Ragdoll插件之一)

  • Ragdoll碰撞体与骨骼分离,不直接关联,降低耦合度

  • 基于分离结构,可以分别计算物理变换与动画变换,并实现融合

  • 基于一套Ragdoll碰撞体可以预设多种状态,切换简单且可控制过渡平滑度

官方文档:https://assetstore.altinqiran.kz/ramecan-mixer/files/GUIDE_ENG.pdf

该插件主要由两个脚本组成

  • Ragdoll Constructor

  • Ramecan Mixer

网络和多人游戏

创建Get请求

UnityWebRequest wr = new UnityWebRequest(); // 完全为空
UnityWebRequest wr2 = new UnityWebRequest("http://www.mysite.com"); // 设置目标 URL

// 必须提供以下两项才能让 Web 请求正常工作
wr.url = "http://www.mysite.com";
wr.method = UnityWebRequest.kHttpVerbGET;   // 可设置为任何自定义方法,提供了公共常量

wr.useHttpContinue = false;
wr.chunkedTransfer = false;
wr.redirectLimit = 0;  // 禁用重定向
wr.timeout = 60;       // 此设置不要太小,Web 请求需要一些时间

创建UploadHandler

byte[] payload = new byte[1024];
// ...使用数据填充有效负载 ...

UnityWebRequest wr = new UnityWebRequest("http://www.mysite.com/data-upload");
UploadHandler uploader = new UploadHandlerRaw(payload);

// 发送标头:"Content-Type: custom/content-type";
uploader.contentType = "custom/content-type";

wr.uploadHandler = uploader;

创建DownloadHandler

DownloadHandlers 有多种类型:

  • DownloadHandlerBuffer 用于简单的数据存储,下载结果为二进制。
  • DownloadHandlerFile 用于下载文件并将文件保存到磁盘(内存占用少)。
  • DownloadHandlerTexture 用于下载图像。
  • DownloadHandlerAssetBundle 用于提取 AssetBundle。
  • DownloadHandlerAudioClip 用于下载音频文件。
  • DownloadHandlerMovieTexture 用于下载视频文件。由于 MovieTexture 已被弃用,因此建议您使用 VideoPlayer 进行视频下载和电影播放。
  • DownloadHandlerScript 是一个特殊类。就其本身而言,不会执行任何操作。但是,此类可由用户定义的类继承。此类接收来自 UnityWebRequest 系统的回调,然后可以使用这些回调在数据从网络到达时执行完全自定义的数据处理。

Buffer

using UnityEngine;
using UnityEngine.Networking; 
using System.Collections;

 
public class MyBehaviour : MonoBehaviour {
    void Start() {
        StartCoroutine(GetText());
    }
 
    IEnumerator GetText() {
        UnityWebRequest www = new UnityWebRequest("http://www.my-server.com");
        www.downloadHandler = new DownloadHandlerBuffer();
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            // 将结果显示为文本
            Debug.Log(www.downloadHandler.text);
 
            // 或者以二进制数据格式检索结果
            byte[] results = www.downloadHandler.data;
        }
    }
}

文件

using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class FileDownloader : MonoBehaviour {

    void Start () {
        StartCoroutine(DownloadFile());
    }
    
    IEnumerator DownloadFile() {
        var uwr = new UnityWebRequest("https://unity3d.com/", UnityWebRequest.kHttpVerbGET);
        string path = Path.Combine(Application.persistentDataPath, "unity3d.html");
        uwr.downloadHandler = new DownloadHandlerFile(path);
        yield return uwr.SendWebRequest();
        if (uwr.isNetworkError || uwr.isHttpError)
            Debug.LogError(uwr.error);
        else
            Debug.Log("File successfully downloaded and saved to " + path);
    }
}

纹理

DownloadHandlerTexture下载一个纹理, 等价于:使用DownloadHandlerBuffer+Texture.LoadImage(从原始字节创建纹理)。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking; 
using System.Collections;

[RequireComponent(typeof(UnityEngine.UI.Image))]
public class ImageDownloader : MonoBehaviour {
    UnityEngine.UI.Image _img;
 
    void Start () {
        _img = GetComponent<UnityEngine.UI.Image>();
        Download("http://www.mysite.com/myimage.png");
    }
 
    public void Download(string url) {
        StartCoroutine(LoadFromWeb(url));
    }
 
    IEnumerator LoadFromWeb(string url)
    {
        UnityWebRequest wr = new UnityWebRequest(url);
        DownloadHandlerTexture texDl = new DownloadHandlerTexture(true);
        wr.downloadHandler = texDl;
        yield return wr.SendWebRequest();
        if(!(wr.isNetworkError || wr.isHttpError)) {
            Texture2D t = texDl.texture;
            Sprite s = Sprite.Create(t, new Rect(0, 0, t.width, t.height),
                                     Vector2.zero, 1f);
            _img.sprite = s;
        }
    }
}

AssetBundle

using UnityEngine;
using UnityEngine.Networking; 
using System.Collections;
 
public class MyBehaviour : MonoBehaviour {
    void Start() {
        StartCoroutine(GetAssetBundle());
    }
 
    IEnumerator GetAssetBundle() {
        UnityWebRequest www = new UnityWebRequest("http://www.my-server.com");
        DownloadHandlerAssetBundle handler = new DownloadHandlerAssetBundle(www.url, uint.MaxValue);
        www.downloadHandler = handler;
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            // 提取 AssetBundle
            AssetBundle bundle = handler.assetBundle;
        }
    }
}

audioClip

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class AudioDownloader : MonoBehaviour {

    void Start () {
        StartCoroutine(GetAudioClip());
    }

    IEnumerator GetAudioClip() {
        using (var uwr = UnityWebRequestMultimedia.GetAudioClip("http://myserver.com/mysound.ogg", AudioType.OGGVORBIS)) {
            yield return uwr.SendWebRequest();
            if (uwr.isNetworkError || uwr.isHttpError) {
                Debug.LogError(uwr.error);
                yield break;
            }

            AudioClip clip = DownloadHandlerAudioClip.GetContent(uwr);
            // 使用音频剪辑
        }
    }   
}

Get请求

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
 
public class MyBehaviour : MonoBehaviour {
    void Start() {
        StartCoroutine(GetText());
    }
 
    IEnumerator GetText() {
        UnityWebRequest www = UnityWebRequest.Get("http://www.my-server.com");
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            // 将结果显示为文本
            Debug.Log(www.downloadHandler.text);
 
            // 或者以二进制数据格式检索结果
            byte[] results = www.downloadHandler.data;
        }
    }
}

GetTexture

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
 
public class MyBehaviour : MonoBehaviour {
    void Start() {
        StartCoroutine(GetTexture());
    }
 
    IEnumerator GetTexture() {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture("http://www.my-server.com/image.png");
        yield return www.SendWebRequest();

        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            Texture myTexture = ((DownloadHandlerTexture)www.downloadHandler).texture;
        }
    }
}

Texture的获取也可以通过DownloadHandler

    IEnumerator GetTexture() {
            UnityWebRequest www = UnityWebRequestTexture.GetTexture("http://www.my-server.com/image.png");
            yield return www.SendWebRequest();

            Texture myTexture = DownloadHandlerTexture.GetContent(www);
        }

从服务器下载AssetBundle

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
 
public class MyBehaviour : MonoBehaviour {
    void Start() {
        StartCoroutine(GetAssetBundle());
    }
 
    IEnumerator GetAssetBundle() {
        UnityWebRequest www = UnityWebRequest.GetAssetBundle("http://www.my-server.com/myData.unity3d");
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(www);
        }
    }
}

POST请求提交表单

表单鼓励使用 List<IMultipartFormSection> formData,旧版的UnityWebRequest.Post(string url, WWWForm formData);已经不鼓励使用了。

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Collections.Generic;

public class MyBehavior : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Upload());
    }

    IEnumerator Upload()
    {
        List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
        formData.Add(new MultipartFormDataSection("field1=foo&field2=bar"));
        formData.Add(new MultipartFormFileSection("my file data", "myfile.txt"));

        UnityWebRequest www = UnityWebRequest.Post("http://www.my-server.com/myform", formData);
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
        }
        else
        {
            Debug.Log("Form upload complete!");
        }
    }
}

PUT请求

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
 
public class MyBehavior : MonoBehaviour {
    void Start() {
        StartCoroutine(Upload());
    }
 
    IEnumerator Upload() {
        byte[] myData = System.Text.Encoding.UTF8.GetBytes("This is some test data");
        UnityWebRequest www = UnityWebRequest.Put("http://www.my-server.com/upload", myData);
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            Debug.Log("Upload complete!");
        }
    }
}

使用Unity开发多人游戏,有两套API:

  1. 偏底层一些,直接使用NetworkManager或者高级API(HL API)

  2. 使用NetworkTransport,与Unity 结合更紧密

参考资料

多人游戏是Unity的重要内容,在功能介绍中有非常详细的介绍。https://docs.unity3d.com/Manual/UNet.html

此外,Unity专门开辟了一个多人游戏域名存放多人游戏相关的文档:https://docs-multiplayer.unity3d.com/tools/current/install-tools

多人游戏网站介绍了三个组件,这三个组件分别具有自己的版本号:

  1. Netcode for GameObjects 1.2.0

  2. Transport 2.0.0

  3. Multiplayer Tools 1.1.0

tutorials:

https://docs-multiplayer.unity3d.com/netcode/current/tutorials/goldenpath_series/goldenpath_one/index.html

Unity多人游戏提供的服务

  • 匹配服务

  • 创建比赛和通告比赛

  • 列出可用的比赛、加入比赛

  • 中继服务器(Relay Server)

HLAPI的功能

  • 使用 Network Manager 来控制游戏的联网状态。

  • 操作“客户端托管的”游戏,这种情况下的主机也是玩家客户端。

  • 使用通用序列化程序来序列化数据。

  • 发送和接收网络消息。

  • 将联网命令从客户端发送到服务器。

  • 执行从服务器到客户端的远程过程调用 (RPC)。

  • 将联网事件从服务器发送到客户端。

NetworkManager

三种模式:

  • Server:

  • Client:

  • Host:主机模式

NetworkManager HUD

多人游戏的简单UI,主要用于测试多人游戏,它支持两种模式:

  • LAN模式

  • Matchmaker模式

什么是UnityWebRequest

Unity库中提供了基本的HTTP请求库,这个库可以做到跨平台,并且封装了AssetBundle、Texture等常用加载资源的方法。
UnityWebRequest提供了两套API,一类叫做HL API(高级API),一类叫做LL API (低级API)。
HLAPI意思是开发者可以直接调用Get、Post请求。
LLAPI意思是开发者需要手动写UploadHandler、DownloadHandler。

使用UnityWebRequest

  • www是旧版用法
  • 判断请求是否成功,使用req.result == UnityWebRequest.Result.Success

GET请求

UnityWebRequest req = new UnityWebRequest($"https://baidu.com");
//绕过SSL证书验证
req.certificateHandler = new BypassCertificate();
req.downloadHandler = new DownloadHandlerBuffer();
var asyncOp = req.SendWebRequest();
while (asyncOp.isDone == false)
{
    await Task.Delay(1000 / 30); //30 hertz
}
var s = req.downloadHandler.text;

POST请求

internal class BypassCertificate : CertificateHandler
    {
        protected override bool ValidateCertificate(byte[] certificateData)
        {
            return true;
        }
    }

    [Test]
    public static void testRequest()
    {
        var url = "https://weiyinfu.cn/jiahe/";
        UnityWebRequest req = new UnityWebRequest(url);
//绕过SSL证书验证
        req.certificateHandler = new BypassCertificate();
        req.downloadHandler = new DownloadHandlerBuffer();
        req.method = "POST";
        req.SetRequestHeader("Content-Type", "application/json");
        var data = new JObject();
        data["content"] = "hello";
        Debug.Log(data.ToString());
        var d = Encoding.UTF8.GetBytes(data.ToString());
        req.uploadHandler = new UploadHandlerRaw(d);
        var asyncOp = req.SendWebRequest();
        while (asyncOp.isDone == false)
        {
            Thread.Sleep(300);
            // await Task.Delay(1000 / 30); //30 hertz
        }

        var s = req.downloadHandler.text;
        Debug.Log(s);
    }

C#的HttpClient

public static void testHttpClient()
{
    HttpClient cli = new HttpClient();
    var data = new JObject();
    data["content"] = "hello";
    Debug.Log(data.ToString());
    var content = new StringContent(data.ToString(), Encoding.UTF8);
    content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
    var resp = cli.PostAsync("https://weiyinfu.cn/jiahe/", content);
    resp.Wait();
    var sTask = resp.Result.Content.ReadAsStringAsync();
    sTask.Wait();
    var s = sTask.Result;
    Debug.Log($"result= {s}");
}

Unity WebRequest特点

  1. https的端口号默认也是80端口,因此在URL中必须指明端口号
  2. 下面这种发送方式,ContentType默认是:

*application/x-www-form-urlencoded*

JObject obj = new JObject();
obj["content"] = DateTime.Now.ToString();
var ss = JsonConvert.SerializeObject(obj);
UnityWebRequest www = UnityWebRequest.Post("http://localhost:9001/why", ss);

排查simpleget为啥请求80端口。

  1. 端口号最好带着,即便是https接口也要带着端口号,因为有一些http库并不会实现https就请求443端口。
  2. 请求路径长一点,如果是请求weiyinfu.cn/jiahe则会进行301重定向,就变成了get请求。所以应该是weiyinfu.cn/jiahe/

脚本

https://docs.unity3d.com/cn/2022.1/Manual/roslyn-analyzers.html

https://learn.microsoft.com/zh-cn/dotnet/csharp/roslyn-sdk/get-started/syntax-analysis

awemesome.net

https://dotnet.libhunt.com/categories

C#代码

C#字节数组

MemoryStream stream = new MemoryStream();
using (BinaryWriter writer = new BinaryWriter(stream))
{
    writer.Write(myByte);
    writer.Write(myInt32);
    writer.Write("Hello");
}
byte[] bytes = stream.ToArray();

C#创建随机数组

int Min = 0;
int Max = 20;
Random randNum = new Random();
int[] test2 = Enumerable
    .Repeat(0, 5)
    .Select(i => randNum.Next(Min, Max))
    .ToArray();

读写文件

            if (! Directory.Exists(C:\Users\Administrator\123))     // 返回bool类型,存在返回true,不存在返回false
            {
                Directory.CreateDirectory(C:\Users\Administrator\123);      //不存在则创建路径
            }

           if (! File.Exists(C:\Users\Administrator\123\1.txt))        // 返回bool类型,存在返回true,不存在返回false                                     
            {
                 File.Create(C:\Users\Administrator\123\1.txt);         //不存在则创建文件
            }

IntPtr运算符

/// For passing to native C
public static explicit operator IntPtr(RtcGetTokenOptions options)
{
    return options != null ? options.Handle : IntPtr.Zero;
}

静态类的优点

静态类只能包含静态成员。您不能为静态类创建对象。

  1. 如果将任何成员声明为非静态成员,则会收到错误消息。

  2. 当您尝试为静态类创建实例时,它会再次生成编译时错误,因为可以使用其类名直接访问静态成员。

  3. 在类定义中,在 class 关键字之前使用 static 关键字来声明静态类。

  4. 静态类成员通过类名后跟成员名来访问。

unity多线程插件loom

一个队列,用于普通线程和UI线程之间的通信。

unity使用toml

https://github.com/dezhidki/Tommy/blob/master/Tommy/Tommy.cs

https://lab.uwa4d.com/lab/62839f03a8103dabd055548d

https://github.com/toml-lang/toml

parent

dont use xx.parent=xxx,use setParent

userObj.transform.SetParent(friendContent.transform, true); }

UI操作只能在协程进行

11-17 10:54:24.090 28748 28909 E Unity : Trying to add RawImageHeadImage (UnityEngine.UI.RawImage) for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported.

使用HTTP请求ContinueWith会有另一个协程,这个协程与主线程还是有区别的。

创建一个MonoBehavior,它有一个队列,把需要执行的函数放到这个队列里面。

LineRenderer

Matrix4x4

unity竟然不支持cylinder collider

圆柱体在三维空间中就会不停地滚动,根本停不下来

每帧检查

如以下代码所示,每一秒钟执行一次。
因为Update()不够准确,所以这种定时方式时间长了就会不准。

public float timer = 1.0f;
// Update is called once per frame
void Update() {
    timer -= Time.deltaTime;
    if (timer <= 0) {
        Debug.Log(string.Format("Timer1 is up !!! time=${0}", Time.time));
        timer = 1.0f;
    }
}

使用协程

void Start() {
    StartCoroutine(Timer());
}
IEnumerator Timer() {
    while (true) {
        yield return new WaitForSeconds(1.0f);
        Debug.Log(string.Format("Timer2 is up !!! time=${0}", Time.time));
    }
}

使用协程的延时调用

void Start() {
    Invoke("Timer", 1.0f);
}
void Timer() {
    Debug.Log(string.Format("Timer3 is up !!! time=${0}", Time.time));
    Invoke("Timer", 1.0f);
}

协程

所谓协程,就是return一个IEnumerator的一个函数
这个函数之后每次都会返回Update都会被调用知道返回,也就是在一个函数里面安排下未来的很多个动作,这种方式可以实现位移动画

协程等待一段时间的实现方案

参考资料:https://stackoverflow.com/questions/30056471/how-to-make-the-script-wait-sleep-in-a-simple-way-in-unity

Wait系列:

  • WaitForEndOfFrame
  • WaitForFixedUpdate
  • WaitForSeconds
  • WaitForSecondsRealtime
  • WaitUntil
  • WaitWhile

一、使用WaitForSeconds类

yield return 一个WaitForSeconds类,这样调度的时候就会等待一段时间。
让物体旋转90度,然后休息4秒钟。接着再旋转,再休息。

void Start()
{
    StartCoroutine(waiter());
}
IEnumerator waiter()
{
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    yield return new WaitForSeconds(4);

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    yield return new WaitForSeconds(2);

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

二、使用WaitForSecondsRealtime

这个类与WaitForSeconds的唯一区别就是这个类使用了无所方的时间来等待,这就意味着当游戏挂起Time.timeScale的时候,WaitForSecondsRealtime函数并不会受到Time.timeScale的影响,而WaitForSecond会受到影响。

示例代码如下:

void Start()
{
    StartCoroutine(waiter());
}

IEnumerator waiter()
{
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    yield return new WaitForSecondsRealtime(4);

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    yield return new WaitForSecondsRealtime(2);

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

三、使用deltaTime

相当于自己写一个wait函数,使用float变量记录已经过去的时间,累加Time.deltaTime。

也可以使用Time.realTimeSinceStartup来表示,这样就不用累加了。

这种方式依赖Update的频率,因此准确性可能相比上面那种方式差一些。

bool quit = false;

void Start()
{
    StartCoroutine(waiter());
}

IEnumerator waiter()
{
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    float waitTime = 4;
    yield return wait(waitTime);

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    waitTime = 2;
    yield return wait(waitTime);

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

IEnumerator wait(float waitTime)
{
    float counter = 0;

    while (counter < waitTime)
    {
        //Increment Timer until counter >= waitTime
        counter += Time.deltaTime;
        Debug.Log("We have waited for: " + counter + " seconds");
        if (quit)
        {
            //Quit function
            yield break;
        }
        //Wait for a frame so that Unity doesn't freeze
        yield return null;
    }
}

四、使用WaitUtil等待某个条件的满足

float playerScore = 0;
int nextScene = 0;

void Start()
{
    StartCoroutine(sceneLoader());
}

IEnumerator sceneLoader()
{
    Debug.Log("Waiting for Player score to be >=100 ");
    yield return new WaitUntil(() => playerScore >= 10);
    Debug.Log("Player score is >=100. Loading next Level");

    //Increment and Load next scene
    nextScene++;
    SceneManager.LoadScene(nextScene);
}

五、使用WaitWhile

WaitWhile语义上与WaitUtil恰好相反。

void Start()
{
    StartCoroutine(inputWaiter());
}

IEnumerator inputWaiter()
{
    Debug.Log("Waiting for the Exit button to be pressed");
    yield return new WaitWhile(() => !Input.GetKeyDown(KeyCode.Escape));
    Debug.Log("Exit button has been pressed. Leaving Application");

    //Exit program
    Quit();
}

void Quit()
{
    #if UNITY_EDITOR
    UnityEditor.EditorApplication.isPlaying = false;
    #else
    Application.Quit();
    #endif
}

六、使用Invoke函数

Invoke(函数名称,等待的秒数)

void Start()
{
    Invoke("feedDog", 5);
    Debug.Log("Will feed dog after 5 seconds");
}

void feedDog()
{
    Debug.Log("Now feeding Dog");
}

在total帧内把一个游戏物体直线匀速移动到某个位置

这种写法存在的问题:移动的快慢依赖于帧数,如果帧率高,物体移动得就很快;如果帧率低,物体移动得就很慢。因此,最好是依赖于时间去移动物体。

//在total帧内移动到位置p
public static IEnumerator moveTo(GameObject gameObject, Vector3 p, int total)
{
    var o = gameObject.transform.position;
    for (var i = 1; i < total; i++)
    {
        var ratio = i * 1.0f / total;
        gameObject.transform.position = p * ratio + o * (1 - ratio);
        yield return null;
    }

    gameObject.transform.position = p;
}

在一个duration时间段内,插入frameCount帧把物体移动到某个位置

public static IEnumerator moveTo(GameObject gameObject, Vector3 p, float duration, int frameCount)
{
    var o = gameObject.transform.position;
    int total = frameCount;
    var startTime = Time.realtimeSinceStartup;
    for (var i = 1; i < total; i++)
    {
        while (true)
        {
            var ratio = (Time.realtimeSinceStartup - startTime) / duration;
            if (ratio > 1.0f * i / total) break;
            yield return null;
        }

        {
            var ratio = (Time.realtimeSinceStartup - startTime) / duration;
            if (ratio < 0) ratio = 0f;
            if (ratio > 1) break;
            gameObject.transform.position = p * ratio + o * (1 - ratio);
            yield return null;
        }
    }

    gameObject.transform.position = p;
}

MonoBehaviour的两个关键函数

  • Invoke:在一段时间后定时执行
  • StartCoroutine:开启一个协程执行任务

拼接IEnumerator

以下这种写法,使用while()遍历IEnumerator。

public void mergeGrow()
{
    //发生合并的时候,先变大再变小
    StartCoroutine("mergeGrowCoroutine");
}

private IEnumerator mergeGrowCoroutine()
{
    //a是一个IEnumerator类型的对象
    var a = AnimateUtil.growAndShrink(gameObject, defaultScale * 1.2f, defaultScale, 0.4f, 25);
    while (a.MoveNext()) yield return null;
}

实际上,可以简写为yield return <一个IEnumerator对象的形式>

private IEnumerator mergeGrowCoroutine()
{
    yield return AnimateUtil.growAndShrink(gameObject, defaultScale * 1.2f, defaultScale, 0.4f, 25);
}

不管什么工具,它的国际化都是非常相似的。

生成代码,便于直接引用一个stringId。如果没有生成代码,则引用字符串的时候就只能手拼字符串了,非常容易出错。

在配置文件里面写各种语言。

每一种配置语言最好是一个单独的文件,这样做的好处是便于把语言包作为一个插件。

一个比较好用的框架:loxodon-framework

i18n.cs:一个简单的CS脚本。

https://gist.github.com/ditzel/2546768f28df7ca664de4a8dfbbfc778

unity创建object的三种方法

//1.第一种方法

GameObject go = new GameObject("name");  //name 为名字

//2.第二种方法

public GameObject prefab;

GameObject.Instantiate(prefab);//可以很具prefab 或者另外一个物体克隆

//3.第三种方法

//创建基本图形

GameObject.CreatePrimitive(PrimitiveType.Cube);

unity使用prefab的两种方式

加载prefab有两种方式

  • 方法一:设置脚本的public字段 ,然后再界面上传参。这种方式比较死板。
  • 方法二:使用Resource.Load() 在代码里用Resource.Load()来加载实例化预设,前提是预设预提必须放在Assets/Resources的目录下。当然你也可以在 Assets/Resources/MyPrefabs。
// Use this for initialization
void Start () {
//参数一:是预设 参数二:实例化预设的坐标  参数三:实例化预设的旋转角度
GameObject instance = (GameObject)Instantiate(Resources.Load("Cubeprefab"), transform.position)

find an inactive object

Unity的对象树许多关键功能是缺失的,需要开发者自己实现。对于寻找inactive的物体这一简单功能,unity没有提供便捷的函数,使用Resources.FindObjectsTypeAll能够实现。

 var fooGroup = Resources.FindObjectsOfTypeAll<AnUniqueClass>();
 if (fooGroup.Length > 0) {
   var foo = fooGroup[0];
 }

下面是网上的一些讨论

Oh my god, I can't believe they didn't support this simple, essential and frequent feature even in 2018...?!?!?!

That's got to be a real wrong name choice here. I would never expect to look into that class. Resources tells me it deals with resources folder, so how come this find them in the scene??!! Is that another intern job at Unity? Anyway, it does the job indeed. don't use this method. it has permanent effect on your assets. find another solution.

SpriteRenderer[] onlyActive = GameObject.FindObjectsOfType<SpriteRenderer>();

SpriteRenderer[] activeAndInactive = GameObject.FindObjectsOfType<SpriteRenderer>(true);

// requires "using System.Linq;"
SpriteRenderer[] onlyInactive = GameObject.FindObjectsOfType<SpriteRenderer>(true).Where(sr => !sr.gameObject.activeInHierarchy).ToArray();

寻找一个对象下的某个对象

实现一个扩展方法:

 public static GameObject FindObject(this GameObject parent, string name)
 {
     Transform[] trs= parent.GetComponentsInChildren<Transform>(true);
     foreach(Transform t in trs){
         if(t.name == name){
              return t.gameObject;
         }
     }
     return null;
 }

使用时:

GameObject obj = parentObject.FindObject("MyObject");

寻找一个对象下的子组件

 Transform childTransform = gameObject.transform.GetComponentsInChildren<Transform>(true).FirstOrDefault(t => t.name == "Name Of Child Object");

找到所有的游戏对象

 public static List<GameObject> FindAllObjectsInScene()
     {
         UnityEngine.SceneManagement.Scene activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
 
         GameObject[] rootObjects = activeScene.GetRootGameObjects();
 
         GameObject[] allObjects = Resources.FindObjectsOfTypeAll<GameObject>();
 
         List<GameObject> objectsInScene = new List<GameObject>();
 
         for (int i = 0; i < rootObjects.Length; i++)
         {
             objectsInScene.Add(rootObjects[i]);
         }
 
         for (int i = 0; i < allObjects.Length; i++)
         {
             if (allObjects[i].transform.root)
             {
                 for (int i2 = 0; i2 < rootObjects.Length; i2++)
                 {
                     if (allObjects[i].transform.root == rootObjects[i2].transform && allObjects[i] != rootObjects[i2])
                     {
                         objectsInScene.Add(allObjects[i]);
                         break;
                     }
                 }
             }
         }
         return objectsInScene;
     }

删除游戏对象

  • Destroy(this.gameObject);
  • DestroyImmediate(this.gameObject);

如果在一个Update中,先执行删除操作再执行添加操作,则应该使用DestroyImmediate。

update和fixedUpdate的区别

Update是每一帧执行之前都要执行,Update的执行频率取决于系统硬件;fixedUpdate是单位时间内执行一次。 一般物理模拟放在FixedUpdate中。

unity线程

Unity3D是否支持写成多线程程序?如果支持的话需要注意什么? 答:仅能从主线程中访问Unity3D的组件,对象和Unity3D系统调用。 Unity所有的Update操作都是串行执行的,由主线程负责执行。Update阻塞会导致UI卡顿。 Unity一般不使用多线程,而是使用协程。 Unity3d没有多线程的概念,不过unity也给我们提供了StartCoroutine(协同程序)和LoadLevelAsync(异步加载关卡)后台加载场景的方法。 StartCoroutine为什么叫协同程序呢,所谓协同,就是当你在StartCoroutine的函数体里处理一段代码时,利用yield语句等待执行结果,这期间不影响主程序的继续执行,可以协同工作。

unity的MonoBehavior的运行机制

多个GameObject的Update函数是串行执行的,它们都处于同一个线程里面。 Unity会维护一个GameObjectList,在每一帧中,写一个for循环串行执行。因此无论如何都不应该阻塞Monobehavior。 for obj in GameObjectList: for behavior in obj.BehaviorList: behavior.Update()

mono behavior的回调

  • Awake:当一个脚本实例被载入时Awake被调用。我们大多在这个类中完成成员变量的初始化
  • Start:仅在Update函数第一次被调用前调用。因为它是在Awake之后被调用的,我们可以把一些需要依赖Awake的变量放在Start里面初始化。 同时我们还大多在这个类中执行StartCoroutine进行一些协程的触发。要注意在用C#写脚本时,必须使用StartCoroutine开始一个协程,但是如果使用的是JavaScript,则不需要这么做。
  • Update:当MonoBehaviour启用时,其Update在每一帧被调用。
  • FixedUpdate:当MonoBehaviour启用时,其 FixedUpdate 在每一固定帧被调用。
  • OnEnable:当对象变为可用或激活状态时此函数被调用。
  • OnDisable:当对象变为不可用或非激活状态时此函数被调用。
  • OnDestroy:当MonoBehaviour将被销毁时,这个函数被调用。

Unity Fps

第一步,在PlayerSettings中关掉VSyncCount [图片] 第二步,在脚本中设置帧率

using UnityEngine;
using System.Collections;

/// <summary>
/// 功能:修改游戏FPS
/// </summary>
public class UpdateFrame : MonoBehaviour
{
    //游戏的FPS,可在属性窗口中修改
    public int targetFrameRate = 300;

    //当程序唤醒时
    void Awake ()
    {
        //修改当前的FPS
        Application.targetFrameRate = targetFrameRate;
    }

}

第三步,把显示帧率的脚本挂载某个游戏对象上

@script ExecuteInEditMode

private var gui : GUIText;

private var updateInterval = 1.0;
private var lastInterval : double; // Last interval end time
private var frames = 0; // Frames over current interval

function Start()
{
    lastInterval = Time.realtimeSinceStartup;
    frames = 0;
}

function OnDisable ()
{
    if (gui)
    DestroyImmediate (gui.gameObject);
}

function Update()
{
#if !UNITY_FLASH
    ++frames;
    var timeNow = Time.realtimeSinceStartup;
    if (timeNow > lastInterval + updateInterval)
    {
        if (!gui)
        {
            var go : GameObject = new GameObject("FPS Display", GUIText);
            go.hideFlags = HideFlags.HideAndDontSave;
            go.transform.position = Vector3(0,0,0);
            gui = go.guiText;
            gui.pixelOffset = Vector2(5,55);
        }
        var fps : float = frames / (timeNow - lastInterval);
        var ms : float = 1000.0f / Mathf.Max (fps, 0.00001);
        gui.text = ms.ToString("f1") + "ms " + fps.ToString("f2") + "FPS";
        frames = 0;
        lastInterval = timeNow;
    }
#endif
}

移动相机动作在哪个函数里,为什么在这个函数里?

LateUpdate,是在所有的Update结束后才调用,比较适合用于命令脚本的执行。官网上例子是摄像机的跟随,都是所有的Update操作完才进行摄像机的跟进,不然就有可能出现摄像机已经推进了,但是视角里还未有角色的空帧出现。

脚本的执行顺序

往一个GameObject上挂载多个MonoBahavior,它们的执行顺序是怎样的? 执行顺序与添加顺序一致。 如果往不同GameObject上挂载多个Monobehavior,它们的执行顺序是怎样的? 不确定。可以在项目配置中设置脚本执行命令。

一个MonoBehavior的执行顺序

img.png

img_1.png

第一阶段:初始化

Awake、onEnable、Start

第二阶段:物理层

FixedUpdate、OnTriggerXXX、OnCollisionXXX

第三阶段:输入事件

OnMounseXXX

第四阶段:游戏逻辑

Update 调用过去的协程yield LateUpdate

第五阶段:场景渲染

  • OnWillRenderObject
  • OnPreCull
  • OnBecameVisisible
  • OnBecameInvisible
  • OnPreRender
  • OnRenderObject
  • OnPOstRender
  • OnRenderImage

OnApplicationFocus

unity从后台切换到前台的时候调用

FixedUpdate和Update的区别

FixedUpdate比Update调用频率更高。

Unity PlayMode下场景加载之后

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    static void Initializing()
    {
        ShootsStub.GetInstance();
    }

Transform和RectTransform

RectTransform继承自Transform,如果使用图片作为精灵,则一定要设置成RectTransform,这样RectTransform的初始值就是图片的大小。在脚本中可以通过RectTransform的Rect字段获取宽度和高度。

可以给任意一个物体设置RectTransform 设置RectTransform的大小使用size属性,它的rect属性是只读的。

position和localPosition的区别

  1. position是根据世界原点为中心
  2. localPosition是根据父节点为中心,如果没有父节点,localpositon和position是没有区别的
  3. 选中一个物体左上角Global和Local切换看物体世界坐标轴和本地坐标轴

transform组件下的Position属性是一个相对于父对象的局部坐标,当然了,如果其没有父对象,其属性框里显示的自然而然也就是世界坐标。如果有父对象,那么在unity编辑器显示的值是这个transform以上一级父对象为基准的坐标。

资源和包

有一个EditorWindow类型,使用了新版的UnityGUI。需要加载UXML和CSS,UXML和CSS的路径与C#脚本在同一目录下面,如何不管脚本的位置,动态加载呢? 关键在于获取C#脚本文件所在的位置。

解决方案如下:

  • 传入fileName(不带后缀名)。
  • 在AssetDatabase中搜索fileName,得到GUID列表
  • 如果找到的paths个数为0,表示没有找到;如果大于1表示找到了多个,文件名称起得不够特殊。
  • 根据GUID寻找资源的详细路径

    string getScriptPath(string fileName)
    {
        string[] paths = AssetDatabase.FindAssets(fileName);
        if (paths.Length > 1)
        {
            Debug.LogError("multi file is found");
            return null;
        }

        if (paths.Length == 0)
        {
            Debug.LogError($"cannot find {fileName}");
            return null;
        }

        string realPath = AssetDatabase.GUIDToAssetPath(paths[0]);
        Debug.Log($"getScriptPath {fileName} {realPath}");
        return Path.GetDirectoryName(realPath);
    }

预设和Prefab是Unity中简化操作的两大利器,它们本质上就是一套模板。

Prefab=GameObject的模板。 预设=MonoBehavior的模板。

Unity的GameObject包含很多个组件,每个组件都有一些public的属性,可以公开设置。
当一个组件的属性非常多的时候,设置起来就很麻烦,这时候可以使用Prefab。
除了Prefab,也可以使用Preset(预设)。预设就是组件的一组配置方式。
如下图所示,点击XR Controller组件的问号后面的那个预设按钮,可以选择预设。也可以将当前配置保存为预设。
img.png

预设相比Prefab更为轻量,prefab一定会引入新的GameObject,而预设则只改一个组件的成员值而不改变GameObject。

每个预设相当于一个工厂方法

MonoBehavior preset1(){
    ...
}
MonoBehavior preset2(){
    ...
}

预设相当于Vue中的Mixin,可以将预设文件设置为某个MonoBehavior的默认设置。 一个预设只能更改一个MonoBehavior。

如何保存Preset

在一个MonoBehavior上面点击右上角的Preset按钮,可以为该组件导入预设。在该浮窗底部有一个Save Current Preset,点击可以将当前MonoBehavior保存在一个预设。

Unity的AssetDatabase是资源管理数据库,在其中扮演重要角色的就是.asset文件,这种文件的格式是YAML。
Unity使用了一种定制化、优化的YAML库,称之为UnityYAML,UnityYAML不支持完整的YAML规范。
Unity内部有YAML解析库,但是Unity并不准备把它开源。

Unity检测Assets目录下面的一些特殊文件夹名称,这些文件夹都有各自用途。文件夹不一定直接放在Assets目录下面,可以嵌套多层。

Editor文件夹

  1. Editor测试脚本
  2. Editor菜单、窗口定制脚本

Plugins

C/C++的插件目录。

Resources

资源文件。可以使用Resources.Load()直接加载。
这个文件夹下的内容会加密打包到应用里面。

  • Resources文件夹下的资源无论使用与否都会被打包
  • 资源会被压缩,转化成二进制
  • 打包后文件夹下的资源只读
  • 无法动态更改,无法做热更新
  • 使用Resources.Load加载

StreamingAssets

同样是资源文件。但是这些资源不会被加密,解压apk之后能够找到这些资源,这些资源文件以独立文件的形式存在。

  • 流数据的缓存目录
  • 文件夹下的资源无论使用与否都会被打包
  • 资源不会被压缩和加密
  • 打包后文件夹下的资源只读,主要存放二进制文件
  • 无法做热更新
  • WWW类加载(一般用CreateFromFile ,若资源是AssetBundle,依据其打包方式看是否是压缩的来决定)
  • 相对路径,具体路径依赖于实际平台,Android:Application.streamingAssetsPath;IOS: Application.dataPath + “/Raw” 或Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxx.app/Data/Raw

Application.dataPath

  • 游戏的数据文件夹的路径(例如在Editor中的Assets)
  • 很少用到
  • 无法做热更新
  • IOS路径: Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxx.app/Data

Application.persistentDataPath

  • 持久化数据存储目录的路径( 沙盒目录,打包之前不存在 )
  • 文件夹下的资源无论使用与否都会被打包
  • 运行时有效,可读写
  • 无内容限制,从StreamingAsset中读取二进制文件或从AssetBundle读取文件来写入PersistentDataPath中
  • 适合热更新
  • IOS路径: Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Documents

隐藏的资源

在导入package、unitypackage的过程中,Unity自动忽略一些特殊文件和文件夹,这些包括:

  • 隐藏文件夹,即以.开头,包括.git
  • 以~开头的文件夹,Unity把它们当做Sample
  • 名为cvs的文件和文件夹,这是一个CVS这种版本管理工具
  • 临时文件,即以.tmp结尾的文件

参考资料:https://docs.unity3d.com/cn/current/Manual/upm-scoped.html

注册表服务器

开发者可以自己搭建包注册表服务器,这是一个中心化的包存储服务器,可以使用npm管理。

如下例所示,在scopedRegistries里面,定义了General和Tools两个服务器,PackageManager去指定的URL处下载包,将包复制到Library目录下面。 每个服务器有一个scopes,它是一个数组,存储一些包名的前缀。PackageManager会选择匹配度最高的服务器去下载包。

{
    "scopedRegistries": [
        {
            "name": "General",
            "url": "https://example.com/registry",
            "scopes": [
                "com.example", "com.example.tools.physics"
            ]
        },
        {
            "name": "Tools",
            "url": "https://mycompany.example.com/tools-registry",
            "scopes": [
                "com.example.mycompany.tools"
            ]
        }
    ],
    "dependencies": {
        "com.unity.animation": "1.0.0",
        "com.example.mycompany.tools.animation": "1.0.0",
        "com.example.tools.physics": "1.0.0",
        "com.example.animation": "1.0.0"
    }
}

manifest.json一般是不需要手动编辑的,可以在UI里面进行设置。 打开ProjectSettings,选择PackageManager即可。

包的三种特殊形式

https://docs.unity3d.com/cn/current/Manual/upm-git.html 除了可以通过制定包名+版本号的方式指定依赖,还可以通过以下三种方式指定依赖:

  1. git仓库:"com.mycompany.mypackage": "git@mycompany.github.com:gitproject/com.mycompany.mypackage.git"
  2. 本地文件夹:"my_package_a": "file:../github/my_package_folder"
  3. 嵌入式依赖项:直接把包文件夹放在packages目录下面。

包的脚本API

https://docs.unity3d.com/cn/current/Manual/upm-api.html

常见用途:

  1. 获取已经安装的包的列表
  2. 将包添加到项目
  3. PackageManager事件

unitypackage

unitypackage可以用于打包一些资源、脚本等。unitypackage导入之后就会导入到assets里面,如果assets有依赖的话,也会相应的导入依赖。在导出unitypackage的时候,可以取消勾选"include dependencies"

unity导出资源的正确方法

unity的文件分为两类:Assets和Packages
Pacakges大多数情况下只是维护一个包的引用,但是也可以直接把包拷贝到Pacakges目录下面,这样分享给别人的时候就不会出现找不到包的问题了。 如果对Packages进行修改,则会永远进行修改,可能会影响到其它的Unity项目,因为Package只是引用。
导出资源的时候使用ExportAssets进行导出。

assets目录

  • Resources:需要动态加载的文件放入,打包时,在这个文件夹里的文件不管有没有被使用,都会被打包出来。
  • Plugins:插件目录,该目录编译时会优先编译,以便项目中调用。
    这里我们在使用Android项目的时候会将.jar .aar文件放入Plugins\Android\libs中,而将.so 64位文件Plugins\Android\libs\arm64-v8a,.so 32位放入Plugins\Android\libs\armeabi-v7a中
  • Editor: 该目录下的代码可调用Unity Editor 的API,存放扩展编辑器的代码。编译时不会被打包到游戏中。
  • Standard Assets:该文件夹下的文件会优先被编译,以便项目调用,它与Plugins一样,打包时会被编译到同一个.sln文件里。
  • SteamingsAssets:该目录下的文件会在打包时打包到项目中去,与Resources一样不管有没有用到的文件,在打包时都会被打包出来。

Resources与SteamingsAssets的区别
Resources下 文件在打时会进行压缩与加密,但是StremingsAssets下的文件是直接被打包出来。所以SteamingsAssets中主要存放2进制文件
Resources中的材质球、预制体等资源,会在打包时自动寻找引用资源,打包到Resurce中。

package原理

unity的package是在电脑上共用的,项目A和项目B依赖同一个package,则这个package在磁盘上位于同一个位置,在项目A里面更改package的内容会影响到项目B。
把包直接放在Packages目录下面,则能够解决package位置问题。
从外部引入的packages是soft link,而不是复制一份。

使用Resource加载一个asset文件

Resources目录下面有一个PlatformSetting.asset文件 var x=UnityEngine.Resources.Load<PXR_PlatformSetting>("PlatformSetting"); 在PlatformSetting.asset文件中有m_Script这一行,其中guid表示PXR_PlatformSetting.cs.meta中的guid,这个字段表示了这个meta文件所对应的类。

MonoBehaviour:
    m_ObjectHideFlags: 0
    m_CorrespondingSourceObject: {fileID: 0}
    m_PrefabInstance: {fileID: 0}
    m_PrefabAsset: {fileID: 0}
    m_GameObject: {fileID: 0}
    m_Enabled: 1
    m_EditorHideFlags: 0
    m_Script: {fileID: 11500000, guid: 4a400eaa24c042bdb05671fd4bf94c1d, type: 3}

Unity中与资源有关的几个ID

Unity会为Assets目录中的每一个资源都创建一个meta文件,该文件中就有GUID,GUID表示这个资源的ID。GUID可以用于表示资源之间的引用关系。

  • GUID:对象ID
  • fileID:文件ID,又叫本地ID,用于表示资源内部的资源。
  • InstanceID:实例ID。在Unity运行时会维护一个缓存表,用于将GUID和fileID映射成一个InstanceID。便于运行时管理资源。

资源之间的依赖关系使用GUID来确定;资源内部的依赖关系使用fileID来确定。

Standard Assets和StreamingAssets

Standard Assets 该文件夹下的文件会优先被编译,以便项目调用,它与Plugins一样,打包时会被编译到同一个.sln文件里。 StreamingAssets 该目录下的文件会在打包时打包到项目中去,与Resources一样不管有没有用到的文件,在打包时都会被打包出来。

Resources与SteamingsAssets的区别

Resources下文件在打包时会进行压缩与加密,但是StreamingAssets下的文件是直接被打包出来。所以SteamingsAssets中主要存放二进制文件 Resources中的材质球、预制体等资源,会在打包时自动寻找引用资源,打包到Resource中。

在Unity中如果文件夹以~结尾,则该文件夹不会被assetDatabase打包,因此最终也不会被打包到编译产物中。
例如:

  • Documentation~
  • Samples~

PacakgeManager导入包的时候可以选择性地导入Samples。只需要在JSON中写上Sample列表即可。

{
  "name": "com.unity.inputsystem",
  "displayName": "Input System",
  "version": "1.3.0",
  "unity": "2019.4",
  "description": "A new input system which can be used as a more extensible and customizable alternative to Unity's classic input system in UnityEngine.Input.",
  "keywords": [
    "input",
    "events",
    "keyboard",
    "mouse",
    "gamepad",
    "touch",
    "vr",
    "xr"
  ],
  "dependencies": {
    "com.unity.modules.uielements": "1.0.0"
  },
  "upmCi": {
    "footprint": "9b3507e700a475d3fc7c6406be352fd3f6b2e8e9"
  },
  "repository": {
    "url": "https://github.com/Unity-Technologies/InputSystem.git",
    "type": "git",
    "revision": "76d3fd182183dff9e9793431de614fd1e69e363c"
  },
  "samples": [
    {
      "displayName": "Custom Binding Composite",
      "description": "Shows how to implement a custom composite binding.",
      "path": "Samples~/CustomComposite"
    },
    {
      "displayName": "Custom Device",
      "description": "Shows how to implement a custom input device.",
      "path": "Samples~/CustomDevice"
    },
    {
      "displayName": "Custom Device Usages",
      "description": "Shows how to tag devices with custom usage strings that can be used, for example, to distinguish multiple instances of the same type of device (e.g. 'Gamepad') based on how the device is used (e.g. 'Player1' vs 'Player2' or 'LeftHand' vs 'RightHand').",
      "path": "Samples~/CustomDeviceUsages"
    },
    {
      "displayName": "Gamepad Mouse Cursor",
      "description": "An example that shows how to use the gamepad for driving a mouse cursor for use with UIs.",
      "path": "Samples~/GamepadMouseCursor"
    },
    {
      "displayName": "In-Game Hints",
      "description": "Demonstrates how to create in-game hints in the UI which reflect current bindings and active control schemes.",
      "path": "Samples~/InGameHints"
    },
    {
      "displayName": "InputDeviceTester",
      "description": "A scene containing UI to visualize the controls on various supported input devices.",
      "path": "Samples~/InputDeviceTester"
    },
    {
      "displayName": "Input Recorder",
      "description": "Shows how to capture and replay input events. Also useful by itself to debug input event sequences.",
      "path": "Samples~/InputRecorder"
    },
    {
      "displayName": "On-Screen Controls",
      "description": "Demonstrates a simple setup for an on-screen joystick.",
      "path": "Samples~/OnScreenControls"
    },
    {
      "displayName": "Rebinding UI",
      "description": "An example UI component that demonstrates how to create UI for rebinding actions.",
      "path": "Samples~/RebindingUI"
    },
    {
      "displayName": "Simple Demo",
      "description": "A walkthrough of a simple character controller that demonstrates several techniques for working with the input system. See the README.md file in the sample for details.",
      "path": "Samples~/SimpleDemo"
    },
    {
      "displayName": "Simple Multiplayer",
      "description": "Demonstrates how to set up a simple local multiplayer scenario.",
      "path": "Samples~/SimpleMultiplayer"
    },
    {
      "displayName": "Touch Samples",
      "description": "A series of sample scenes for using touch input with the Input System package. This sample is not actually part of the package, but needs to be downloaded.",
      "path": "Samples~/TouchSamples"
    },
    {
      "displayName": "UI vs. Game Input",
      "description": "An example that shows how to deal with ambiguities that may arrise when overlaying interactive UI elements on top of a game scene.",
      "path": "Samples~/UIvsGameInput"
    },
    {
      "displayName": "Visualizers",
      "description": "Several example visualizations of input controls/devices and input actions.",
      "path": "Samples~/Visualizers"
    }
  ]
}

Unity包的Samples

{
  "name": "com.unity.timeline",
  "displayName": "Timeline",
  "version": "1.6.4",
  "unity": "2019.3",
  "keywords": [
    "unity",
    "animation",
    "editor",
    "timeline",
    "tools"
  ],
  "description": "Use Unity Timeline to create cinematic content, game-play sequences, audio sequences, and complex particle effects.",
  "dependencies": {
    "com.unity.modules.director": "1.0.0",
    "com.unity.modules.animation": "1.0.0",
    "com.unity.modules.audio": "1.0.0",
    "com.unity.modules.particlesystem": "1.0.0"
  },
  "relatedPackages": {
    "com.unity.timeline.tests": "1.6.4"
  },
  "upmCi": {
    "footprint": "dcac7462836a5fa5813cc1e2c6080df1da992327"
  },
  "repository": {
    "url": "https://github.cds.internal.unity3d.com/unity/com.unity.timeline.git",
    "type": "git",
    "revision": "d7e1eb6805737974459309b7d6e7db58635dd167"
  },
  "samples": [
    {
      "displayName": "Customization Samples",
      "description": "This sample demonstrates how to create custom timeline tracks, clips, markers and actions.",
      "path": "Samples~/Customization"
    },
    {
      "displayName": "Gameplay Sequence Demo",
      "description": "This sample demonstrates how Timeline can be used to create a small in-game moment, using built-in Timeline tracks.",
      "path": "Samples~/GameplaySequenceDemo"
    }
  ]
}

Prefab有三种加载方式
一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate
二是Resource.Load,Load之后instantiate
三是AssetBundle.Load,Load之后instantiate

使用WWW方式加载

        private IEnumerator LoadImage(string path, MeshRenderer output)
        {
            var url = "file://" + path;
            var www = new WWW(url);
            yield return www;

            var texture = www.texture;
            if (texture == null)
            {
                Debug.LogError("Failed to load texture url:" + url);
            }

            output.material.mainTexture = texture;
        }

使用Texture2D加载

        if (!File.Exists(filepath))
        {
            Debug.LogError($"File not exists {filepath}");
            return;
        }

        var x = new Texture2D(200, 200);
        if (!x.LoadImage(File.ReadAllBytes(filepath)))
        {
            Debug.LogError($"Load image failed");
            return;
        }

        rawImage.texture = x;

在Editor中异步获取依赖版本

        public static void GetPackageVersionAsync(string packageName, Action<string> callback)
        {
            ListRequest Request = Client.List(); // List packages installed for the project
            EditorApplication.CallbackFunction progress = null;
            progress = delegate()
            {
                if (!Request.IsCompleted)
                {
                    return;
                }

                EditorApplication.update -= progress;

                if (Request.Status == StatusCode.Success)
                {
                    foreach (var package in Request.Result)
                    {
                        if (package.name == packageName)
                        {
                            callback(package.version);
                            return;
                        }
                    }
                }
                else if (Request.Status >= StatusCode.Failure)
                {
                    Debug.Log(Request.Error.message);
                }
            };
            EditorApplication.update += progress;
        }

同步获取依赖版本

        public static string GetPackageVersion(string packageName)
        {
            ListRequest Request = Client.List(); // List packages installed for the project

            while (!Request.IsCompleted)
            {
                Thread.Sleep(100);
            }

            if (Request.Status == StatusCode.Success)
            {
                foreach (var package in Request.Result)
                {
                    if (package.name == packageName)
                    {
                        return package.version;
                    }
                }
            }
            else if (Request.Status >= StatusCode.Failure)
            {
                Debug.Log(Request.Error.message);
            }

            return "";
        }

UPM

Unity Package Management,是Unity官方推出的一种包管理模式。

Unity导入外部资源有两种方式,一种是通过.unitypackage名称的文件把资源导入到Assets目录,一种是把资源放在Pacakges目录。

Pacakges方式支持:内嵌、git仓库、网址三种形式。

UPM就是Packages目录方式,它可以指定依赖的名称、版本等信息。

Unity支持在UnityEditor中配置Registry,从而可以从私服上下载资源。

使用内嵌的最大好处就是:开发者可以修改Packages里面的内容。

使用git仓库的好处是:可以省掉与registry打交道的过程,同时享受网址模式的好处。

使用网址的好处:正规,当遇到升级的时候,可以显示可用的升级。

OpenUPM

OpenUPM是一个非官方的UPM工具,包括一个nodejs命令行和一个网站。

https://openupm.com/

国服:https://openupm.cn/

竞品:

使用方式与普通的Unity使用方式类似,可以手动添加registry,也可以使用openupm的命令行工具。

创建包的方式

openupm中管理了多少个包?这一切信息都在一个github仓库中:https://github.com/openupm/openupm

创建github仓库其实就是创建一个yaml文件,描述清楚包的仓库地址、license等信息。

这个yaml文件的创建可以使用一个网页工具创建,也可以直接编辑一个yaml文件提交上去。在这个仓库的PullRequest里面可以找到很多例子:https://github.com/openupm/openupm/pull/4015

使用国内版openupm访问github会报错,所以创建包的时候需要使用国际版:https://openupm.com/packages/add/

openupm严重依赖github,需要发布的包必须放在github上,并且使用tag管理版本。

https://github.com/openupm/openupm/compare/master...weiyinfu:openupm:patch-1

data/packages/weiyinfu.pico.integrationsdk.yml

使用包

https://openupm.cn/packages/weiyinfu.pico.integrationsdk/#close

方法一:直接改manifest

编辑Packages/manifest.json,添加scopeRegistries和dependency,然后保存

{    "scopedRegistries": [        {            "name": "package.openupm.cn",            "url": "https://package.openupm.cn",            "scopes": [                "weiyinfu.pico.integrationsdk"            ]        }    ],    "dependencies": {        "weiyinfu.pico.integrationsdk": "2.1.6"    }}

方法二:使用openupm命令

openupm-cn add weiyinfu.pico.integrationsdk

与热更新相关的文件夹

需要更新的代码、资源,都必须打包成AssetBundle(建议使用未压缩的格式打包),需要熟悉Unity的几个重要的路径 • Resources(只读) • StreamingAssets(只读) • Application.dataPath(只读) • Application.persistentDataPath(可读写)

其中,只有Application.persistentDataPath适合做热更新。

关于热更新

苹果iOS系统一律禁止热更新。
Android支持热更新,Unity的热更新方案包括:

  1. AssetBundle (1)项目开发中,可以将部分逻辑提取至一个单独的代码库工程中,打包为DLL; (2)将DLL打包为AssetBundle; (3)Unity程序动态加载AssetBundle中的DLL文件,使用Reflection机制来调用代码。 assetbundle是Unity支持的一种文件储存格式,也是Unity官方推荐的资源存储与更新方式,它可以对资源(Asset)进行压缩,分组打包,动态加载,以及实现热更新,但是AssetBundle无法对Unity脚本进行热更新,因为其需要在打包时进行编译。
  2. 使用Lua进行热更新。

Sprite Atlas,精灵图集是一种性能优化方式,它把多个图片放在一个图片里面。

类型:master和variant。master类型的图集可以设置Objects for packing,variant依赖master,在master的基础上可以设置缩放比例。

master类型的精灵图集打包之后会产生.spriteatlas资源。

资源加载

方式一:使用全局变量,以拖动的方式赋值

public class Main : MonoBehaviour
{
    public TextAsset indexJson;
    ...

方式二:新建Resources文件夹

把资源文件放在该文件夹下,例如index.json这个文件,加载的时候只需要写成index,不需要带着后缀名。 Resources文件夹可以位于Assets文件夹的任意位置。

    var x = Resources.Load<TextAsset>("index");
    Debug.Log($"resource .length={x.text.Length}");

方式三:网络加载资源

        var t = await Util.GetRemoteTexture("https://forum.unity.com/data/avatars/m/628/628421.jpg?1441745369");
        var obj = new GameObject();
        obj.AddComponent<RawImage>();
        var rawImage = obj.GetComponent<RawImage>();
        rawImage.texture = t;

使用网络请求获取资源


    public static async Task<Texture2D> GetRemoteTexture(string url)
    {
        using (UnityWebRequest req = UnityWebRequestTexture.GetTexture(url))
        {
            req.certificateHandler = new BypassCertificate();
            // begin request:
            var asyncOp = req.SendWebRequest();

            // await until it's done:
            while (asyncOp.isDone == false)
                await Task.Delay(1000 / 30); //30 hertz

            // read results:
            if (req.result == UnityWebRequest.Result.ConnectionError || req.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.Log($"{req.error}, URL:{req.url}");
                return null;
            }
            else
            {
                // return valid results:
                return DownloadHandlerTexture.GetContent(req);
            }
        }
    }

方式四:通过AssetDatabases加载资源

        {
            //使用AssetDatabase加载图片,图片的位置可以比较随意
            var x = AssetDatabase.FindAssets("PICODeveloper");
            Debug.Log($"assets:{string.Join(",", x)}");
            if (x.Length > 0)
            {
                var assetId = x[0];
                var filepath = AssetDatabase.GUIDToAssetPath(assetId);
                texture = AssetDatabase.LoadAssetAtPath<Texture>(filepath);
            }
        }

输入系统

Unity通过两个独立的系统提供输入支持:

  • 输入管理器InputManager是Unity核心平台的一部分,默认情况下就能使用。
  • 输入系统InputSystem,必须使用PackageManager安装才能使用,是Unity新版的主推的输入系统,它需要.net4 运行时。

在 Unity 设计之初,并没有预见到现在如此丰富的平台和设备支持规模,所以一开始设计的输入系统 UnityEngine.Input 使用起来并不舒适,在多设备和多平台输入处理时显得力不从心且不够优雅,甚至连游戏中热插拔手柄这种操作都显得十分臃肿和复杂。Unity Tec 从 2016 年起开始逐步开发新一代输入系统 UnityEngine.InputSystem ,到今天(Unity 2019.3.0f6),已经迭代到了 1.0.0-preview.4 版本。官方表示大概在 Unity 2020 版本推出新输入系统的正式版,但是旧输入系统的下架时间并没有被明确。

新输入系统基于事件,输入设备和动作逻辑互相分离,通过配置映射来处理输入信息。具有易用,多设备多平台协调一致的特点。现在已经可以在 Package Manager 中安装使用了。

Unity的这两种输入系统是互斥的,要想更改只能重启。Unity的XR Interaction Toolkit也有最新版,新版的特点就是鼓励使用基于Action的输入,不鼓励直接访问设备的输入,这种理念与InputSystem是相似的。我认为InputManager和Devices-Based XR输入系统更具灵活性并且容易上手。

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.4/manual/Migration.html

如何避免InputSystem频繁弹窗?

新版的输入系统InputSystem需要通过PackageManager安装,只有在安装了InputSystem的情况下才会弹窗,要想避免弹窗,直接在Pacakge/manifest里面把InputSystem删掉即可。

如何切换到InputSystem?

  1. 打开PacakgeManager添加InputSystem包,一旦导入这个包,之后每次打开Unity如果没有启用新输入系统都会提示是否启用新的输入系统。
  2. 在PlayerSettings里面设置输入为新的InputSystem
  3. 把各个场景中的EventySystem按照提示启用新输入系统的MonoBehavior。
  4. 对于类似InGameDebugConsole等一些库,需要手动添加引用,解决编译报错。

输入涉及到的API

  • Input的静态方法系列

  • 移动设备使用Input.touches

    foreach(Touch touch in Input.touches)
    {
        if (touch.phase == TouchPhase.Began)
        {
            // 从当前触摸坐标构造一条射线
            Ray ray = Camera.main.ScreenPointToRay(touch.position);
            if (Physics.Raycast(ray))
            {
                // 如果命中,则创建一个粒子
                Instantiate(particle, transform.position, transform.rotation);
            }
        }
    } 
    
  • VR、XR的InputDevices系列

Unity定义了一个输入系统,好处:

  • 可以为每个输入起一个名字
  • 不同设备上的输入不需要修改代码,只需要修改配置即可。

在Edit/Project Settings/InputManager下面,可以添加或者删除Axes(在Axes上右键)。

if (Input.GetButtonDown("Left"))
{
    if (MoveTilesLeft())
    {
        state = State.CheckingMatches;
    }
}
else if (Input.GetButtonDown("Right"))
{
    if (MoveTilesRight())
    {
        state = State.CheckingMatches;
    }
}
else if (Input.GetButtonDown("Up"))
{
    if (MoveTilesUp())
    {
        state = State.CheckingMatches;
    }
}
else if (Input.GetButtonDown("Down"))
{
    if (MoveTilesDown())
    {
        state = State.CheckingMatches;
    }
}
else if (Input.GetButtonDown("Reset"))
{
    Reset();
}
else if (Input.GetButtonDown("Quit"))
{
    Application.Quit();
}

InputSystem

InputSystem API文档

学习资料,在PackageManager中添加InputSystem的Samples。

如果启用了新版输入系统,还使用旧版输入系统就会报错

InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings.
02-28 16:20:22.460 14806 31977 E Unity   :   at (wrapper managed-to-native) UnityEngine.Input.GetKeyDownInt(UnityEngine.KeyCode)

关键类

  • InputAction
  • InputBindings
  • InputActionPhase表示InputAction的五种阶段。分别为:Canceled, Disabled, Performed, Started, Waiting
  • InputActionAssets:是一种存储动作绑定关系的资源文件,扩展名为.InputActions,数据格式为 JSON。可以通过 Project 页面中 Create -> Input Actions 来创建一个新的 Input Action Assets。
  • PlayerInput

使用示例

直接获取当前输入设备的状态

public void Update()
{
    var gp = Gamepad.current;
    if (gp == null) return;
    
    Vector2 leftStick = gp.leftStick.ReadValue(),
            rightStick = gp.rightStick.ReadValue();
    
    if (gp.buttonSouth.wasPressedThisFrame) Debug.Log("Pressed");
    if (gp.buttonSouth.wasReleasedThisFrame) Debug.Log("Released");
    
    //...
}

使用PlayerInput组件

public class Player : MonoBehaviour
{
    // Unity Event的情况,需要在Inspector中进行本函数的订阅操作
    public void OnAttack(InputAction.CallbackContext callback)
    {
        switch (callback.phase)
        {
            case InputActionPhase.Performed:
                Debug.Log("Attacking!");
        }
        move = callback.ReadValue<Vector2>();
    }
    
    // C# 事件的情况,与上方情况不共存
    private PlayerInput input;
    private Vector2 move;
    private void Awake()
    {
        // 添加订阅者函数, 当然也可以写成独立的函数
        input = GetComponent<PlayerInput>().onActionTriggered += 
            callback =>
        {
            if (callback.action.name == "Move")
            {
                move = callback.ReadValue<Vector2>();
            }
        };
        
        // ...
    }
}

直接使用InputAction,不让PlayerInput赚差价

public class Player : MonoBehaviour
{
    public InputAction moveAction;
    
    public void OnEnable()
    {
        moveAction.Enable();
    }
    
    public void OnDisable()
    {
        moveAction.Disable();
    }
    
     // 此时我们自定义的InputAction即可正常使用
    public void Update()
    {
        var move = moveAction.ReadValue<Vector2>();
        
        //...
    }
}

代码示例

            if (Input.GetKeyDown(KeyCode.UpArrow))
            {
                keydown = true;
                Up();
            }
            else if (Input.GetKeyDown(KeyCode.DownArrow))
            {
                keydown = true;
                Down();
            }
            else if (Input.GetKeyDown(KeyCode.LeftArrow))
            {
                keydown = true;
                Left();
            }
            else if (Input.GetKeyDown(KeyCode.RightArrow))
            {
                keydown = true;
                Right();
            }

使用宏处理新旧输入系统

 #if ENABLE_INPUT_SYSTEM
     // New input system backends are enabled.
 #endif
 
 #if ENABLE_LEGACY_INPUT_MANAGER
     // Old input backends are enabled.
 #endif

官方文档

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.4/manual/UISupport.html

作用

两套输入系统

  • 旧版:InputManager

  • 新版:InputSystem

在2022 editor中已经将新版的InputSystem作为默认配置。如果想在2019的editor中使用,则需要在PackageManager中手动添加InputSystem这个依赖。

目前来看,InputSystem是未来,尽量使用InputSystem。

在代码中使用的时候,

替换InputManager,对应UnityEngine.Input类。

三种用法

通过输入设备直接读取输入

读取gamepad。其它的可以使用:Keyboard.current,Mouse.current.

using UnityEngine;
using UnityEngine.InputSystem;

public class MyPlayerScript : MonoBehaviour
{
    void Update()
    {
        var gamepad = Gamepad.current;
        if (gamepad == null)
            return; // No gamepad connected.if (gamepad.rightTrigger.wasPressedThisFrame)
        {
            // 'Use' code here
        }

        Vector2 move = gamepad.leftStick.ReadValue();
        // 'Move' code here
    }
}

通过InputAction读取输入

  1. 添加一个PlayerInput组件。每个PlayerInput代表游戏中的一个玩家。

  2. 为PlayerInput创建Actions,创建名为.inputactions的文件,将其关联到PlayerInput组件。

  3. 编辑Action的Response

    1. SendMessages

    2. Broadcast Messages

    3. Invoke Unity Events

    4. Invoke C Sharp Events

常用技巧

当前帧是否按下了空格键

Keyboard.current.space.wasPressedThisFrame

找到所有的游戏手柄

    var allGamepads = Gamepad.all;

    // Or more specific versions.var allPS4Gamepads = DualShockGamepadPS4.all;
    
    //方法二
    // Go through all devices and select gamepads.
    InputSystem.devices.Select(x => x is Gamepad);

    // Query everything that is using the gamepad layout or based on that layout.// NOTE: Don't forget to Dispose() the result.
    InputSystem.FindControls("<gamepad>");

找到玩家当前使用的手柄

var gamepad = Gamepad.current;

    // This works for other types of devices, too.
    var keyboard = Keyboard.current;
    var mouse = Mouse.current;

监控设备变更

InputSystem.onDeviceChange +=
        (device, change) =>
        {
            switch (change)
            {
                case InputDeviceChange.Added:
                    // New Device.break;
                case InputDeviceChange.Disconnected:
                    // Device got unplugged.break;
                case InputDeviceChange.Connected:
                    // Plugged back in.break;
                case InputDeviceChange.Removed:
                    // Remove from Input System entirely; by default, Devices stay in the system once discovered.break;
                default:
                    // See InputDeviceChange reference for other event types.break;
            }
        }

获取触摸屏的输入

    using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;

    public void Update(){
        foreach (var touch in Touch.activeTouches)
            Debug.Log($"{touch.touchId}: {touch.screenPosition},{touch.phase}");
    }

InputAction

var action=new InputAction("设备名称/设备按钮");

一个action有许多状态:

  • performed

  • triggered

UI系统与输入系统的交互器

EventSystem是一个GameObject,它包含UI与输入的事件转发。

旧版的InputManager的事件处理是StandaloneInputModule。

新版的InputSystem的事件处理是InputSystemUIInputModule。

如果使用XR,需要添加XR UI InputModule。

UI Toolkit+XR+InputSystem=暂不支持

XR与UI Toolkit的结合使用上不支持,这意味着无法通过VR控制器设别操作使用UI Toolkit创建的界面。

参考资料

https://docs.unity3d.com/cn/2022.2/Manual/class-InputManager.html

学习路线

  1. 了解Unity在PC端如何获取鼠标键盘的输入事件,了解Input

  2. 了解Unity如何获取XR的输入事件,了解InputDevices

  3. 了解XR Interaction Toolkit,了解Unity的输入的高阶用法

名词解释

HMD:头戴,Head Mounted Display

Unity的两套独立的输入系统

InputManager:旧版的输入系统

InputSystem:新版的输入系统

输入系统不影响API的使用。

Unity 支持来自多种输入设备的输入,包括:

  • 键盘和鼠标

  • 游戏杆

  • 控制器

  • 触摸屏

  • 加速度计或陀螺仪等移动设备的运动感应功能

  • VR 和 AR 控制器

float horizontalInput = Input.GetAxis ("Horizontal");
float moveSpeed = 10;
//定义对象移动的速度。

float horizontalInput = Input.GetAxis("Horizontal");
//获取水平输入轴的数值。

float verticalInput = Input.GetAxis("Vertical");
//获取垂直输入轴的数值。

transform.Translate(new Vector3(horizontalInput, verticalInput, 0) * moveSpeed * Time.deltaTime);
//将对象移动到 XYZ 坐标,分别定义为 horizontalInput、0 以及 verticalInput。

移动端输入:触碰点

    void Update()
    {
        foreach(Touch touch in Input.touches)
        {
            if (touch.phase == TouchPhase.Began)
            {
                // 从当前触摸坐标构造一条射线
                Ray ray = Camera.main.ScreenPointToRay(touch.position);
                if (Physics.Raycast(ray))
                {
                    // 如果命中,则创建一个粒子
                    Instantiate(particle, transform.position, transform.rotation);
                }
            }
        }
    }

加速度计

using UnityEngine;

public class Accelerometer : MonoBehaviour
{
    float speed = 10.0f;

    void Update()
    {
        Vector3 dir = Vector3.zero;
        // 我们假设设备与地面平行,
        // 主屏幕按钮位于右侧

        // 将设备的加速度轴重新映射到游戏坐标:
        // 1) 设备的 XY 平面映射到 XZ 平面
        // 2) 绕 Y 轴旋转 90 度

        dir.x = -Input.acceleration.y;
        dir.z = Input.acceleration.x;

        // 将加速度矢量限制为单位球体
        if (dir.sqrMagnitude > 1)
            dir.Normalize();

        // 使其每秒移动 10 米而不是每帧 10 米...
        dir *= Time.deltaTime;

        // 移动对象
        transform.Translate(dir * speed);
    }
}

XR输入系统

Unity的输入系统一般只通过Input这个类就能够实现。

但是对于XR,目前推荐使用InputDevices这个类作为入口类。

Unity的Input、XR.InputTracking这两个类也能够获取XR输入,但是不鼓励使用,尽量使用InputDevices,这样语义更明确、语法更简洁。Input的缺点在于没法以向量的形式获取数据。

以Input为例,要想获取primaryAxis,就需要Input.GetAxis(1),Input.GetAxis(2),这样才能获取左手的摇杆位置;Input.GetAxis(4),Input.GetAxis(5)才能够获取右手的摇杆位置。

primary2DAxis [(1,2)/(4,5)]

InputDevices

InputDevices

静态

InputDevices的静态函数只有一个目标:获取InputDevice。它提供了几种检索InputDevice的方式

  • 通过XRNode获取,这也是最常用的方式

  • 通过Role获取,对应枚举InputDeviceRole

  • 通过Characteristics获取,对应枚举InputDeviceCharacteristics

  • GetDeviceAtXRNode:根据XRNode获取InputDevice

  • GetDevicesAtRole:根据Role获取设备

  • GetDevicesWithCharacteristics:根据设别的特性获取设备。

成员

  • DeviceRole:设备的角色,InputDeviceRole

  • TryGet+(InputFeatureType中的数据类型):从设备读取数据

事件:

  • 设备连接

  • 设备断连

  • 设备配置发生变化

获取全部的可用设备

var inputDevices = new List<UnityEngine.XR.InputDevice>();
UnityEngine.XR.InputDevices.GetDevices(inputDevices);

foreach (var device in inputDevices)
{
    Debug.Log(string.Format("Device found with name '{0}' and role '{1}'", device.name, device.role.ToString()));
}

XRNode

  • 左眼

  • 右眼

  • 中心眼

  • 左手

  • 右手

  • 游戏控制器

  • 跟踪参照物

  • 硬件跟踪器

XRNode跟InputDeviceRole基本上是同一类东西。

InputDeviceCharacteristics

一个设备可能包含多种特性,使用一个int值表示一个设备的特性,例如HeldInHand|Left表示左右持有的设备。就像使用rwx表示一个文件的特性一样。

  • HeadMounted

  • Camera

  • HeldInHand

  • HandTracking

  • EyeTracking

  • TrackedDevice

  • Controller

  • TrackingReference

  • Left

  • Right

InputDeviceRole

输入设备的角色,包括:

  • 左手持有

  • 右手持有

  • 游戏控制器

  • 跟踪参照物

  • 硬件跟踪器

public enum InputDeviceRole : uint
{
  /// <summary>
  ///   <para>This device does not have a known role.</para>
  /// </summary>
  Unknown,
  /// <summary>
  ///   <para>This device is typically a HMD or Camera.</para>
  /// </summary>
  Generic,
  /// <summary>
  ///   <para>This device is a controller that represents the left hand.</para>
  /// </summary>
  LeftHanded,
  /// <summary>
  ///   <para>This device is a controller that represents the right hand.</para>
  /// </summary>
  RightHanded,
  /// <summary>
  ///   <para>This device is a game controller.</para>
  /// </summary>
  GameController,
  /// <summary>
  ///   <para>This device is a tracking reference used to track other devices in 3D.</para>
  /// </summary>
  TrackingReference,
  /// <summary>
  ///   <para>This device is a hardware tracker.</para>
  /// </summary>
  HardwareTracker,
  /// <summary>
  ///   <para>This device is a legacy controller.</para>
  /// </summary>
  LegacyController,
}

触觉

Haptic是触觉的意思,有些游戏手柄支持触觉。

List<UnityEngine.XR.InputDevice> devices = new List<UnityEngine.XR.InputDevice>(); 

UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.RightHanded, devices);

foreach (var device in devices)
{
    UnityEngine.XR.HapticCapabilities capabilities;
    if (device.TryGetHapticCapabilities(out capabilities))
    {
            if (capabilities.supportsImpulse)
            {
                uint channel = 0;
                float amplitude = 0.5f;
                float duration = 1.0f;
                device.SendHapticImpulse(channel, amplitude, duration);
            }
    }
}

XR Input Tracking:Unity旧版XR输入接口

InputTracking

Unity.Xr.InputTracking是Unity旧版的输入入口类。

设备事件

输入设备的跟踪有四个事件

  • trackingAcquired:获取设备跟踪

  • trackingLost:设备丢失

  • nodeAdded:设备添加

  • nodeRemoved:设备移除

读取数据

  • 获取XRNode的位置

  • 获取XRNode的Rotation

  • 获取XRNode的DeviceId

从InputTracking中读取数据已经不鼓励使用了,请使用InputDevices。例如获取设备地址。

[NativeConditional("ENABLE_VR", "Vector3f::zero")]
[Obsolete("This API is obsolete, and should no longer be used. Please use InputDevice.TryGetFeatureValue with the CommonUsages.devicePosition usage instead.")]
public static Vector3 GetLocalPosition(XRNode node)
{
  Vector3 ret;
  InputTracking.GetLocalPosition_Injected(node, out ret);
  return ret;
}

XRNodeState

XNode:结点的类型

AwailableTrackingData:可用的数据列表

位置

角度

速度

角速度

加速度

角加速度

tracked:是否正在跟踪

AwailableTrackingData

枚举,描述一个XRNodeState可用的数据列表可以组合这个枚举,例如位置|旋转,表示这个XRNodeState可用的数据是位置和旋转。

  • 位置

  • 旋转

  • 速度

  • 角速度

  • 加速度

  • 角加速度

InputFeatureUsage

InputFeatureUsage

Unity引擎里面的类,定义了一个输入类型,可以描述所有的输入。它包含的字段:

  • name:输入的名称

  • UsageType:输入的类型

InputFeatureType

internal enum InputFeatureType : uint
{
  Custom = 0,//Custom类型,使用byte数组表示
  Binary = 1,//bool
  DiscreteStates = 2,//uint,枚举状态
  Axis1D = 3,//一维浮点
  Axis2D = 4,//二维浮点:例如鼠标
  Axis3D = 5,//三维浮点
  Rotation = 6,//角度,Quaternion,四元数
  Hand = 7,//Hand类型
  Bone = 8,//Bone类型
  Eyes = 9,//眼睛
  kUnityXRInputFeatureTypeInvalid = 4294967295, // 0xFFFFFFFF,未定义的操作类型
}

InputFeature中涉及到的结构体

Hand、Bone、Eyes

Bone

  • deviceId:设备ID

  • featureIndex:featureID

  • Position

  • Rotation

  • ParentBone:父骨骼

  • ChildBones:List

Eyes

眼睛有左眼和右眼:

  • LeftEyes

  • RightEyes

以下传感器是左眼和右眼都有的。

  • EyeRotation

  • EyePosition

  • FixationPoint:修正点的位置

  • OpenAmount:眼睛的睁开量

Hand类型

Hand是一个Bone的封装,一个Hand包含一个RootBone和多个FingerBones

  • 设备Id

  • FeatureIndex

  • RootBone:Bone

  • FingerBones:List返回手指骨骼列表

Unity支持的输入的类型:InputFeatureType。

  • bool

  • uint

  • float

CommonUsages

UnityEngine.XR中定义了一些InputFeatureUsage,与Pico XR SDK的InputFeatureUsage类似。

CommonUsage中定义的是比较通用的InputFeatureUsage,而PXR_Usage中存放的是Pico特有的InputFeatureUsage,这些featureUsage大多是与眼睛有关系。

/// <summary>
///   <para>Informs to the developer whether the device is currently being tracked.</para>
/// </summary>
public static InputFeatureUsage<bool> isTracked = new InputFeatureUsage<bool>("IsTracked");
/// <summary>
///   <para>The primary face button being pressed on a device, or sole button if only one is available.</para>
/// </summary>
public static InputFeatureUsage<bool> primaryButton = new InputFeatureUsage<bool>("PrimaryButton");
/// <summary>
///   <para>The primary face button being touched on a device.</para>
/// </summary>
public static InputFeatureUsage<bool> primaryTouch = new InputFeatureUsage<bool>("PrimaryTouch");
/// <summary>
///   <para>The secondary face button being pressed on a device.</para>
/// </summary>
public static InputFeatureUsage<bool> secondaryButton = new InputFeatureUsage<bool>("SecondaryButton");
/// <summary>
///   <para>The secondary face button being touched on a device.</para>
/// </summary>
public static InputFeatureUsage<bool> secondaryTouch = new InputFeatureUsage<bool>("SecondaryTouch");
/// <summary>
///   <para>A binary measure of whether the device is being gripped.</para>
/// </summary>
public static InputFeatureUsage<bool> gripButton = new InputFeatureUsage<bool>("GripButton");
/// <summary>
///   <para>A binary measure of whether the index finger is activating the trigger.</para>
/// </summary>
public static InputFeatureUsage<bool> triggerButton = new InputFeatureUsage<bool>("TriggerButton");
/// <summary>
///   <para>Represents a menu button, used to pause, go back, or otherwise exit gameplay.</para>
/// </summary>
public static InputFeatureUsage<bool> menuButton = new InputFeatureUsage<bool>("MenuButton");
/// <summary>
///   <para>Represents the primary 2D axis being clicked or otherwise depressed.</para>
/// </summary>
public static InputFeatureUsage<bool> primary2DAxisClick = new InputFeatureUsage<bool>("Primary2DAxisClick");
/// <summary>
///   <para>Represents the primary 2D axis being touched.</para>
/// </summary>
public static InputFeatureUsage<bool> primary2DAxisTouch = new InputFeatureUsage<bool>("Primary2DAxisTouch");
/// <summary>
///   <para>Represents the secondary 2D axis being clicked or otherwise depressed.</para>
/// </summary>
public static InputFeatureUsage<bool> secondary2DAxisClick = new InputFeatureUsage<bool>("Secondary2DAxisClick");
/// <summary>
///   <para>Represents the secondary 2D axis being touched.</para>
/// </summary>
public static InputFeatureUsage<bool> secondary2DAxisTouch = new InputFeatureUsage<bool>("Secondary2DAxisTouch");
/// <summary>
///   <para>Indicates whether the user is present and interacting with the device.</para>
/// </summary>
public static InputFeatureUsage<bool> userPresence = new InputFeatureUsage<bool>("UserPresence");
/// <summary>
///   <para>Represents the values being tracked for this device.</para>
/// </summary>
public static InputFeatureUsage<InputTrackingState> trackingState = new InputFeatureUsage<InputTrackingState>("TrackingState");
/// <summary>
///   <para>Value representing the current battery life of this device.</para>
/// </summary>
public static InputFeatureUsage<float> batteryLevel = new InputFeatureUsage<float>("BatteryLevel");
/// <summary>
///   <para>A trigger-like control, pressed with the index finger.</para>
/// </summary>
public static InputFeatureUsage<float> trigger = new InputFeatureUsage<float>("Trigger");
/// <summary>
///   <para>Represents the users grip on the controller.</para>
/// </summary>
public static InputFeatureUsage<float> grip = new InputFeatureUsage<float>("Grip");
/// <summary>
///   <para>The primary touchpad or joystick on a device.</para>
/// </summary>
public static InputFeatureUsage<Vector2> primary2DAxis = new InputFeatureUsage<Vector2>("Primary2DAxis");
/// <summary>
///   <para>A secondary touchpad or joystick on a device.</para>
/// </summary>
public static InputFeatureUsage<Vector2> secondary2DAxis = new InputFeatureUsage<Vector2>("Secondary2DAxis");
/// <summary>
///   <para>The position of the device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> devicePosition = new InputFeatureUsage<Vector3>("DevicePosition");
/// <summary>
///   <para>The position of the left eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> leftEyePosition = new InputFeatureUsage<Vector3>("LeftEyePosition");
/// <summary>
///   <para>The position of the right eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> rightEyePosition = new InputFeatureUsage<Vector3>("RightEyePosition");
/// <summary>
///   <para>The position of the center eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> centerEyePosition = new InputFeatureUsage<Vector3>("CenterEyePosition");
/// <summary>
///   <para>The position of the color camera on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> colorCameraPosition = new InputFeatureUsage<Vector3>("CameraPosition");
/// <summary>
///   <para>The velocity of the device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> deviceVelocity = new InputFeatureUsage<Vector3>("DeviceVelocity");
/// <summary>
///   <para>The angular velocity of this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> deviceAngularVelocity = new InputFeatureUsage<Vector3>("DeviceAngularVelocity");
/// <summary>
///   <para>The velocity of the left eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> leftEyeVelocity = new InputFeatureUsage<Vector3>("LeftEyeVelocity");
/// <summary>
///   <para>The angular velocity of the left eye on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> leftEyeAngularVelocity = new InputFeatureUsage<Vector3>("LeftEyeAngularVelocity");
/// <summary>
///   <para>The velocity of the right eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> rightEyeVelocity = new InputFeatureUsage<Vector3>("RightEyeVelocity");
/// <summary>
///   <para>The angular velocity of the right eye on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> rightEyeAngularVelocity = new InputFeatureUsage<Vector3>("RightEyeAngularVelocity");
/// <summary>
///   <para>The velocity of the center eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> centerEyeVelocity = new InputFeatureUsage<Vector3>("CenterEyeVelocity");
/// <summary>
///   <para>The angular velocity of the center eye on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> centerEyeAngularVelocity = new InputFeatureUsage<Vector3>("CenterEyeAngularVelocity");
/// <summary>
///   <para>The velocity of the color camera on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> colorCameraVelocity = new InputFeatureUsage<Vector3>("CameraVelocity");
/// <summary>
///   <para>The angular velocity of the color camera on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> colorCameraAngularVelocity = new InputFeatureUsage<Vector3>("CameraAngularVelocity");
/// <summary>
///   <para>The acceleration of the device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> deviceAcceleration = new InputFeatureUsage<Vector3>("DeviceAcceleration");
/// <summary>
///   <para>The angular acceleration of this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> deviceAngularAcceleration = new InputFeatureUsage<Vector3>("DeviceAngularAcceleration");
/// <summary>
///   <para>The acceleration of the left eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> leftEyeAcceleration = new InputFeatureUsage<Vector3>("LeftEyeAcceleration");
/// <summary>
///   <para>The angular acceleration of the left eye on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> leftEyeAngularAcceleration = new InputFeatureUsage<Vector3>("LeftEyeAngularAcceleration");
/// <summary>
///   <para>The acceleration of the right eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> rightEyeAcceleration = new InputFeatureUsage<Vector3>("RightEyeAcceleration");
/// <summary>
///   <para>The angular acceleration of the right eye on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> rightEyeAngularAcceleration = new InputFeatureUsage<Vector3>("RightEyeAngularAcceleration");
/// <summary>
///   <para>The acceleration of the center eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> centerEyeAcceleration = new InputFeatureUsage<Vector3>("CenterEyeAcceleration");
/// <summary>
///   <para>The angular acceleration of the center eye on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> centerEyeAngularAcceleration = new InputFeatureUsage<Vector3>("CenterEyeAngularAcceleration");
/// <summary>
///   <para>The acceleration of the color camera on this device.</para>
/// </summary>
public static InputFeatureUsage<Vector3> colorCameraAcceleration = new InputFeatureUsage<Vector3>("CameraAcceleration");
/// <summary>
///   <para>The angular acceleration of the color camera on this device, formatted as euler angles.</para>
/// </summary>
public static InputFeatureUsage<Vector3> colorCameraAngularAcceleration = new InputFeatureUsage<Vector3>("CameraAngularAcceleration");
/// <summary>
///   <para>The rotation of this device.</para>
/// </summary>
public static InputFeatureUsage<Quaternion> deviceRotation = new InputFeatureUsage<Quaternion>("DeviceRotation");
/// <summary>
///   <para>The rotation of the left eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Quaternion> leftEyeRotation = new InputFeatureUsage<Quaternion>("LeftEyeRotation");
/// <summary>
///   <para>The rotation of the right eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Quaternion> rightEyeRotation = new InputFeatureUsage<Quaternion>("RightEyeRotation");
/// <summary>
///   <para>The rotation of the center eye on this device.</para>
/// </summary>
public static InputFeatureUsage<Quaternion> centerEyeRotation = new InputFeatureUsage<Quaternion>("CenterEyeRotation");
/// <summary>
///   <para>The rotation of the color camera on this device.</para>
/// </summary>
public static InputFeatureUsage<Quaternion> colorCameraRotation = new InputFeatureUsage<Quaternion>("CameraRotation");
/// <summary>
///   <para>Value representing the hand data for this device.</para>
/// </summary>
public static InputFeatureUsage<Hand> handData = new InputFeatureUsage<Hand>("HandData");
/// <summary>
///   <para>An Eyes struct containing eye tracking data collected from the device.</para>
/// </summary>
public static InputFeatureUsage<Eyes> eyesData = new InputFeatureUsage<Eyes>("EyesData");
/// <summary>
///   <para>A non-handed 2D axis.</para>
/// </summary>
[Obsolete("CommonUsages.dPad is not used by any XR platform and will be removed.")]
public static InputFeatureUsage<Vector2> dPad = new InputFeatureUsage<Vector2>("DPad");
/// <summary>
///   <para>Represents the grip pressure or angle of the index finger.</para>
/// </summary>
[Obsolete("CommonUsages.indexFinger is not used by any XR platform and will be removed.")]
public static InputFeatureUsage<float> indexFinger = new InputFeatureUsage<float>("IndexFinger");
/// <summary>
///   <para>Represents the grip pressure or angle of the middle finger.</para>
/// </summary>
[Obsolete("CommonUsages.MiddleFinger is not used by any XR platform and will be removed.")]
public static InputFeatureUsage<float> middleFinger = new InputFeatureUsage<float>("MiddleFinger");
/// <summary>
///   <para>Represents the grip pressure or angle of the ring finger.</para>
/// </summary>
[Obsolete("CommonUsages.RingFinger is not used by any XR platform and will be removed.")]
public static InputFeatureUsage<float> ringFinger = new InputFeatureUsage<float>("RingFinger");
/// <summary>
///   <para>Represents the grip pressure or angle of the pinky finger.</para>
/// </summary>
[Obsolete("CommonUsages.PinkyFinger is not used by any XR platform and will be removed.")]
public static InputFeatureUsage<float> pinkyFinger = new InputFeatureUsage<float>("PinkyFinger");
/// <summary>
///   <para>Represents a thumbrest or light thumb touch.</para>
/// </summary>
[Obsolete("CommonUsages.thumbrest is Oculus only, and is being moved to their package. Please use OculusUsages.thumbrest. These will still function until removed.")]
public static InputFeatureUsage<bool> thumbrest = new InputFeatureUsage<bool>("Thumbrest");
/// <summary>
///   <para>Represents a touch of the trigger or index finger.</para>
/// </summary>
[Obsolete("CommonUsages.indexTouch is Oculus only, and is being moved to their package.  Please use OculusUsages.indexTouch. These will still function until removed.")]
public static InputFeatureUsage<float> indexTouch = new InputFeatureUsage<float>("IndexTouch");
/// <summary>
///   <para>Represents the thumb pressing any input or feature.</para>
/// </summary>
[Obsolete("CommonUsages.thumbTouch is Oculus only, and is being moved to their package.  Please use OculusUsages.thumbTouch. These will still function until removed.")]
public static InputFeatureUsage<float> thumbTouch = new InputFeatureUsage<float>("ThumbTouch");

鼠标事件

0左键 1右键 2滚轮

鼠标点击事件实现的三种方式

  1. Input.GetMouseButton
  2. 利用事件接口,这种方式可以直接在UI上进行操作。
    UnityAction<BaseEventData> enter;
    UnityAction<BaseEventData> exit;
    UnityAction<BaseEventData> click;
    EventTrigger.Entry entry;
    EventTrigger.Entry entry1;
    EventTrigger.Entry entry2;
    EventTrigger trigger;
    void Start()
    {
        enter = new UnityAction<BaseEventData>(OnPointerEnterDelegate);
        exit = new UnityAction<BaseEventData>(OnPointerExitDelegate);
        click = new UnityAction<BaseEventData>(OnPointerClickDelegate);
        trigger = gameObject.AddComponent<EventTrigger>();
        
        entry = new EventTrigger.Entry();
        entry1 = new EventTrigger.Entry();
        entry2 = new EventTrigger.Entry();
        
        entry.eventID = EventTriggerType.PointerEnter;
        entry1.eventID = EventTriggerType.PointerExit;
        entry2.eventID = EventTriggerType.PointerClick;
        
        entry.callback.AddListener(enter);
        entry1.callback.AddListener(exit);
        entry2.callback.AddListener(click);
        
        trigger.triggers.Add(entry);
        trigger.triggers.Add(entry1);
        trigger.triggers.Add(entry2);
    }
    public void OnPointerClickDelegate(BaseEventData data){}
    public void OnPointerEnterDelegate(BaseEventData data){}
    public void OnPointerExitDelegate(BaseEventData data){}
    
  3. 挂脚本,直接实现OnMouseDown函数

进度条更新太快

不要使用onvaluechange,而要使用OnEventTriggerEvent,它表示结束拖动滚动条。

onValueChanged变化太快:

        progress.onValueChanged.AddListener(v =>
        {
            var p = videoPlayerObj.GetComponent<VideoPlayer>();
            if (p.canSetTime)
            {
                p.time = v * p.length;
            }
        });

C#中使用扩展方法为所有的UI空间指定事件回调:


public static class UIBehaviourExtension
{
    /// <summary>
    /// 添加EventTrigger事件监听
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="self"></param>
    /// <param name="type"></param>
    /// <param name="action"></param>
    /// <returns></returns>
    public static T OnEventTriggerEvent<T>(this T self, EventTriggerType type, UnityAction<BaseEventData> action) where T : UIBehaviour
    {
        var eventTrigger = self.GetComponent<EventTrigger>() ?? self.gameObject.AddComponent<EventTrigger>();
        EventTrigger.Entry entry = new EventTrigger.Entry { eventID = type };
        entry.callback.AddListener(action);
        eventTrigger.triggers.Add(entry);
        return self;
    }
}

使用的时候直接x.OnEnventTriggerEvent即可。

摇杆也叫JoyStick,在XR里面叫做LeftPrimaryAxis。

如何使用摇杆模拟上下左右?

摇杆的输入是一个Vector2,摇杆移动范围是一个半径为1的圆形,这个Vector2表示摇杆在圆中的位置。
根据摇杆到原点的距离,可以知道摇杆的力度,只有力度足够高的时候才去处理。
处理的时候,根据x和y可以求出角度,根据这个角度与四个方向的夹角大小可以模拟四个方向。
模拟上下左右的时候如何控制频率?当我按一次摇杆的时候,只执行一次up操作,但是unity是一种不停Update的机制,所以会执行多次up操作。解决方法就是只有上一次处于复位状态的时候才去处理事件。

所见即所得

所见即所得是一种设计理念,是一种UI交互理念。
photoshop、剪映等是多媒体的所见即所得。
doc是文档的所见即所得。

游戏的表象

游戏的表象就是可以响应用户操作的视频。
当人在打游戏的时候,其实是在打视频。
而要想构建视频,就不得不进行比较复杂的现实世界模拟。

可视化技术为啥在学术界不受重视?

数据比界面重要,逻辑比界面重要。 同一套数据,即便使用比较差的UI进行展示也是勉强能看的,并没有优化的必要。

游戏的重点就是有反馈。动画、界面、逻辑全是反馈。

在线和离线、实时和非实时

同样一个问题,在线处理的难度比离线处理难度大很多,因为在线处理要求不能看未来的、后面的数据。
在线不一定是实时,但是大多数情况下,在线处理通常是要求很强的实时性。

工程的逻辑

工程的逻辑很简单,深度学习玩的是尺寸,计算机图像学玩的是立体几何。

游戏的外在

游戏的外在就是搞好shader、做好动画。
游戏的内在就是逻辑。
游戏的外在决定了用户是否愿意玩这个游戏,许多简单的小游戏单纯因为美工优秀就风靡全球,例如切水果、3D切方块等。
游戏的动画会反向驱动游戏逻辑。我过去实现的2048都没有考虑为动画服务,都是动画不友好型的代码,只有数据生成+数据渲染两个步骤,而至于两次刷新之间数据是如何动态变化的必须要表现出来,这就是游戏动画的需要。如果没有把数据之间的变化过程描述清楚,则玩家会感到迷惘困惑。举例来说,如果没有卡片缓慢的动画位移,则玩家分不清楚哪个卡片是上一状态的哪个卡片。

心流

心流Flow是玩家在游戏中一种非常沉浸的状态,在这种状态中,玩家能够获得最好的游戏体验,甚至忘记了时间。任何一个游戏开发者都希望能够开发出能够让玩家进入心流的游戏。要让玩家进入心流,核心是如何根据玩家的水平Skill去匹配不同难度Difficulty的对手或关卡。但是现有的游戏是很难开发出完美的机制去满足所有玩家的水平的,但是AI的出现让实现完美的心流体验成为可能。AI存在这样的潜力去记录玩家的情况,快速的判断玩家的水平来提供合适的对手及关卡。

Unity心法

以玩耍的心态去学习,不要过于严谨,追求好玩和新奇有趣。

unity的调试

在web环境中,chrome的console非常好用。实际上,unity完全可以实现不输于chrome的体验,unity是一个exe,exe可以实现自己的console,exe可以完全控制所有的内容。

unity的优势

unity所使用的技术很多都是开源界已有的技术,它的核心特点就是整合能力特别强,把一些已有技术整合起来就能够使得整体看上去像是新技术。

GUI和游戏的区别

GUI运行效率较高,占用内存CPU资源更少,因为它不需要过于复杂的渲染。游戏运行效率较低,因为它需要不停地刷新页面。
例如flutter,当用户不操作按钮的时候,界面是静止的,应用的性能较高。当用户执行操作的时候,能够立马响应。
而Unity即便用户不操作按钮,Unity也在不停地刷新。当用户执行操作的时候,只能在下一帧中检测用户是否执行了某个动作。

GUI是事件驱动的,游戏是自动演绎的。
游戏是一种可以响应用户操作的视频流。

游戏经常使用绝对布局,GUI经常使用CSS、布局容器等。因为游戏中的对象比较自由,使用CSS无法满足需求。

webxr的缺点

游戏资源通常非常大,在网页端受到的束缚较多。
复杂游戏场景需要大量的计算资源进行渲染,网页端无法充分利用GPU等硬件。

输入很重要

输入决定了游戏的玩法。
手游是手指的游戏。
Pico是6dof的游戏。

游戏的战斗场景和外围场景

游戏的战斗场景是游戏的核心玩法,每个游戏都有战斗场景。
在进入战斗场景之前,是一个可以自由发挥、但是有许多套路的东西,称之为外围场景。
不论哪种外围场景,总要给玩家提供动力,有的是名(徽章),有的是利(金币)。

  • 天梯,华山论剑,泰山十八盘。胜利一局前进一步,失败一句退后一步。到达山顶之后又勋章。
  • 评级,起立评测,尝试准确评估一个玩家的战斗力水平
  • 地图,海岛奇兵中的海岛布局,每个海岛表示一场战斗。
  • 残局,一个一个的考题
  • 铜钱场,经济。可以进一步分场对战,普通场,中级场,高级场。不同场次的赌注不同
  • 好友对战,私有房间
  • 人机对战,人工智能。
  • 关卡制。一个游戏有若干个关卡,玩家按顺序选择关卡。
  • 拓扑图关卡。普通的关卡是一条线,玩家只能一关一关地过。拓扑图关卡是一个拓扑图,只有前驱结点全部解决,才能开启下一个关卡的挑战。
  • 直接选择模式。全部关卡是一个集合,用户想玩哪一个关都可以
  • 排位赛:对战斗力进行排序

与游戏战斗场景相关联进行设计:

  • 三人场
  • 五人场
  • 两营对垒
  • 身份局,三国杀

游戏的战斗场景应该力求做到让玩家全心投入,而外围场景则应该以水平、经济、勋章等让玩家有进入战斗场景的动力。一旦进入战斗场景,用户是否能够留住就取决于游戏的核心玩法了。

Unity没有必要学习2D游戏

Unity没有必要学习2D游戏。
2D比3D简单,3D肯定包含2D。使用正交相机就是2D游戏。
先学跑,再学走。

游戏算法

游戏AI的用途

冷启动

对于一款新上线的游戏,玩家的人数还比较少,这个时候对于PVP的游戏,很容易出现因为玩家偏少而导致匹配等待时间太长的问题。那么如果有游戏AI来模拟人类玩家来匹配对手的话,就可以解决冷启动的问题。

温暖局

这是对于一些玩家,可能出现了多次失败,会对游戏丧失兴趣,这个时候就需要让玩家能够赢一下重拾信心,这就是温暖局。游戏AI便可以应用到温暖局中,让玩家赢,又不是赢得特别容易(不太假),这样对于玩家的回流就非常有用。还有就是对于新手玩家,也需要一些温暖局让玩家能够慢慢学习进步,一开始挫败感太强也会导致新手流失。

PVP中AFK补位

这种情况常见于玩家掉线,为了不影响对局,很有必要使用游戏AI来接入对局,从而减少玩家的投诉,提升用户体验。 对于以上三种应用,前两种以前一般是使用行为树的程序,但这种程序比较简单,非常容易被玩家识别为AI而丧失乐趣。而AFK补位目前行为树程序显然会做的非常差,游戏AI算是一种刚需。

PVE模式

我们直接把和游戏AI对抗作为一种模式让玩家体验。

游戏平衡性工具

一般游戏设计里的英雄都存在强弱不平衡问题,因为如果能通过游戏AI来模拟测试,从而帮助调整游戏的平衡性,对于游戏的效果也能够提升。 使用AI实现自动化数值设计。

PCG

生成地形、环境、生成千变万化的环境。

游戏自动化测试

基于AI实现自动化测试,寻找游戏存在的bug。

辅助设计

利用AI生成人物动画。

游戏AI

AlphaZero, Muzero, imitation learning, Pluribus
从AlphaGo到AlphaStar,OpenAI Five,到腾讯的绝艺,绝悟,到启元世界,超参数等初创公司的出现,深度强化学习带来一波全新的AI热潮。在这个阶段,笔者觉得游戏AI就是通过模仿学习及深度强化学习等技术通过大规模的分布式训练而得到的竞技AI,这种AI可以在MOBA,吃鸡及其他竞技类游戏中得到应用。

学术界工业界都开发了很多实现大规模深度强化学习的框架和理论:

  • Ray/Rllib
  • Fiber
  • OpenAI Rapid
  • Google Seed RL

从零开始构建深度强化学习的知识体系

显然,要成为一个合格的深度强化学习研发人员,需要对整个知识体系有完全的了解,并且对其中的主要部分有深刻的理解和实践。

一般,我们分以下几步来构建知识体系: Step 1:深度学习,掌握神经网络的各种基本模型 当前,对于构建深度强化学习的网络模型,主要会用到这些组件:

  1. CNN
  2. RNN/LSTM
  3. Attention, Transformer, Pointer Network
  4. Memory Network, DNC,...
  5. Graph Network (目前还比较少) Step 2:深度强化学习算法 这部分是深度强化学习的核心,目前主流包括:
  6. DQN
  7. Rainbow,Distributional Q Learning
  8. IMPALA,UPGO
  9. SAC
  10. R2D2,R2D3,APE-X
  11. PPO,APPO 除此之外,深度强化学习算法理论还包括了:
  12. Model-based Learning
  13. MCTS (有些问题可以用到比如围棋)
  14. Exploration
  15. Imitation Learning
  16. Multi-task Learning
  17. Meta Reinforcement Learning
  18. Hierarchical Reinforcement Learning 这些额外的部分都是为了辅助核心的算法让agent学的更快更强。 对于额外的部分,知道的越多当然越好了。 Step 3:Large Scale Deep Reinforcement Learning 大规模深度强化学习 这部分的核心是理解掌握大规模深度强化学习的框架构建,目前主流的可以通过以下几种去理解(有的是框架,有的是开源代码,有的是理论):
  19. IMPALA
  20. Rapid
  21. Fiber
  22. Seed RL
  23. Rllib
  24. R2D3 这部分工作一般需要有研究MLsystem的童鞋配合,要不然搭不起来。目前K8s是比较好的选择,比如Fiber就完美支持K8s,OpenAI的集群也都是基于K8s搭建。 Step 4: Meta Controller/Population-based Learning/Self-Play 只有有了self-play,深度强化学习才能展现其魅力,这部分包括了:
  25. AlphaGo self-play
  26. AutoCurricula
  27. Domain Randomization
  28. AlphaStar League
  29. Reward Shaping, Meta-learned Reward Shaping
  30. Meta Gradient, Self-tuning reinforcement Learning
  31. AI-generated Envs,Open-Endedness, Quality Diversity 总的来说,这部分是在Meta层面去控制Agent的训练。 前面三个step只能保证你可以训起来,能把Agent训成什么样,这部分是至关重要的。

第一层认知:AI就是比人更强大的玩家

象棋AI能够打败人类,AI在游戏中可以扮演人的角色。

第二层认知:AI并不在于多强,而是在于让人觉得爽

要想让人觉得爽,就要让人感觉对面是一个有情感的人,而不是一个冷冰冰、循规蹈矩的机器。

第三层认知:NPC也是AI

游戏中的一切非玩家对象都是AI。

第四层认知:AI辅助游戏开发。

  1. AI设置关卡
  2. AI进行数值设计
  3. AI生成素材
  4. AI导航
  5. AI游戏测试
  6. AI角色动画

概念

PVE:玩家与环境 PVP:玩家和玩家对战

如果是多玩家游戏,必定涉及到游戏数据同步。
与游戏数据同步相关的游戏服务主要是房间服务。

如果游戏有自己的逻辑,最终还是需要自己实现游戏同步。
例如贪吃蛇大作战,每个人在客户端提交的只是动作,游戏状态最终还是服务端计算渲染的。
例如麻将游戏,每个人把自己的操作上传到服务端,服务端维护各个玩家的状态。

游戏同步可以有三种实现方式:

  • 状态同步:人数较多的情况
  • 帧同步:比较适合格斗类游戏,传输的数据量小
  • 快照:用户上传操作,

但是从可行性上考虑,只有状态同步才是正解。

游戏中的网络通信通常使用UDP。