编辑器扩展之Unity导出Excel表

# 基于EEPlus在Unity编辑器中导出Excel文件 官方链接 百度云下载地址 # 一些api的使用 通过FileInfo来得到Excel文件 string excelName = "uiExcel.xlsx"; string path = Application.dataPath + "/" + excelName; FileInfo file = new FileInfo(path); 通过ExcelPackage打开文件 ExcelPackage package = new ExcelPackage(file); 在Excel文件里添加sheet ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("sheet1"); 添加列名 worksheet.Cells[1, 1].Value = "ID";... Read more about 编辑器扩展之Unity导出Excel表

资源的加载与释放

资源加载 两种动态加载机制: Resource.Load,从一个默认打进程序包里加载AssetBundle。 AssetBundle,需要自己创建,运行时动态加载,可以指定路径和来源。 AssetBundle的加载: Create创建 CreateFromFile()。从文件读取,不会把整个AssetBundle文件都加载到内存,而是类似建立一个文件操作句柄和缓冲区,需要时才实时Load,所以这种加载方式是最节省资源的,但只能用于standalone程序,加载最快。 CreateFromMemory(byte[])。从Memory读取,byte[]可以来自文件读取的缓冲。 WWW加载。其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已。 Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。 Load加载       用AssetBundle.Load()。这时才会从AssetBundle的内存镜像中读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)。异步读取用AssetBundle.LoadASync(),一次读取多个用AssetBundle.LoadAll()。 AssetBundle的释放: AssetBundle.Unload(false)。释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。 AssetBundle.Unload(true)。释放AssetBundle文件的内存镜像,并销毁所有用Load创建的Asset内存对象。 详解:       Instaniate一个prefab,是一个队Assets进行clone(复制)+ 引用的过程,GameObject,transform是clone生成出来的,其他如texture是纯引用的关系(Instaniate多个GameObject不会重复创建引用对象),mesh、material、shader等是引用和复制同时存在的。引用的Assets对象是不会被复制的,只是一个简单的指针指向已经Load的Asset对象。另外,对于Script,clone一个script等于new一个class实例,实例才会完成工作,把它挂到Unity主线程的调用链里去,class实例里的OnUpdate,OnStart函数才会被执行。多个物体挂同一个脚本,其实是多个物体上挂了那个脚本类的多个实例而已。在new class的过程中,数据区是复制的,代码区是共享的,算是一种特殊的复制+引用关系。所以Load出来的Assets其实是个数据源,用于生成新对象或被引用,生成的过程可能是复制(clone)也可能是引用(指针)。当Destroy一个实例时,只是释放那些clone对象,并不会释放引用对象和clone的数据源对象,Destroy并不知道是否还有别的object在引用那些对象。 注:系统在加载新场景时,所有的内存对象都会被自动销毁,包括用AssetBundle.Load加载的对象和Instaniate克隆的,但不包括AssetBundle文件自身的内存镜像,那个必须用Unload来释放,这种数据缓存是非托管的 总结: 加载: AssetBundle.CreateFrom…:创建一个AssetBundle内存镜像,注意同一个AssetBundle文件在没有Unload之前不能被再次使用。 WWW.AssetBundle:同上,要先new一个再yield return然后才能使用。 AssetBundle.Load(name):从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset对象,即多次Load一个Asset并不会生成多个副本(singleton)。 Resource.Load(path&name):同上,只是从默认位置加载。 Instaniate(object):clone一个object的完整结构,包括其所有component和子物体,浅copy,并不复制所有引用类型。有个特殊用法可以用Instaniate来完整拷贝一个引用类型的Asset,比如Texture等,要拷贝的Texture必须类型设置为Read/Write able。 释放: Destroy:主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。如果用于销毁从文件加载的Asset对象会销毁相应的资源文件,但是如果销毁的Asset是copy的或用脚本动态生成的,只会销毁内存对象。 AssetBundle.Unload(false):释放AssetBundle文件内存镜像。 AssetBundle.Unload(true):释放AssetBundle文件内存镜像,同时销毁所有已经Load的Assets内存对象。 Resources.UnloadAsset(object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象。 Resources.UnloadUnusedAssets():释放所有没有引用的Asset对象。UnusedAssets不但要没有被实际物体引用,也要没有被生命周期内的变量所引用,才可以理解为Unused(引用计数为0)。 GC.Collect():强制垃圾回收器立即释放内存。... Read more about 资源的加载与释放

获取Text文字的渲染长度

在项目中遇到一个需求,一大段Text文字如果一页放不下就放下一页。通过计算单个字符长度再截断,效果不太好,因为字符有的胖些有的瘦些,占用的空间大小不一样。这样只能算出一段文字渲染后的长度,然后去填充。 核心代码 代码如下: Font myFont = text.font; myFont.RequestCharactersInTexture(message,text.fontSize, text.fontStyle); CharacterInfo characterInfo = new CharacterInfo(); char[] arr = message.ToCharArray(); foreach (char c in arr) { myFont.GetCharacterInfo(c, out characterInfo, text.fontSize); totalLength += characterInfo.advance; }   其中RequestCharactersInTexture是指定渲染哪些字符,characterInfo可以获得生成的去重后字符。myFont.GetCharacterInfo(c, out characterInfo, text.fontSize)分别获得每个字符的信息,characterInfo.advance就可以得到每个字符的渲染长度。 获取文字渲染长度 代码如下: public static... Read more about 获取Text文字的渲染长度

Socket编程

Socket客户端与服务器端进行TCP协议通信     基于Tcp协议的Socket通讯类似于B/S架构,面向连接,但不同的是服务器端可以向客户端主动推送消息。 使用TCP协议通讯需满足以下条件: 建立一个套接字(Socket) 绑定服务器端IP地址及端口号–服务器端 利用Listen()方法开启监听–服务器端 利用Accept()方法尝试与客户端建立一个连接–服务器端 利用Connect()方法与服务器建立连接–客户端 利用Send()方法向建立连接的主机发送消息 利用Recive()方法接受来自建立连接的主机的消息(可靠连接) TCP-服务器端Socket 代码: //创建socket Socket server = new Socket(AddressFamily.InterNetwork,SocketTyp.Stream,ProtocolType.Tcp); //绑定IP和端口号 IPAddress ipaddress = new IPAddress(new byte[]{192.168.1,1}); EndPoint point = new IPEndPoint(ipaddress,9999); server.Bind(point); //开始监听,等待客户端连接 server.Listen(100);//参数是最大连接数 //暂停当前线程,直到有一个客户端连接再执行 Socket client = server.Accept(); //使用返回的socket跟客户端通信 string... Read more about Socket编程

Unity&Android真机调试

adb是androidSDK的一个工具,位置就在sdk目录下的platform-tools文件夹下。通过adb,不仅可以查看在Unity中自己设定的打印信息,包括系统信息和异常都能获取到。 1.启动adb 确保手机和电脑在一个局域网下。 打开CMD窗口,输入命令:adb tcpip 5555(打开手机adb网络调试功能),如果正常的话控制台会回显:restarting in TCP mode port: 5555 打开手机查看手机的IP地址,加入手机IP地址为192.168.1.x,输入命令:adb connect 192.168.1.x,如果正常控制台回显:connected to 192.168.1.x:5555 查看是否连接成功。输入命令:adb devices 2.打包项目调试     在unity中,选择File -> Buld Settings -> 选择 Android,勾选 Development Build 和 Script Debugging这两项。点击Build&Run之后会自动编译文件并将APK推送到手机上安装。程序运行后在Mono中打开Run->Attach to process 会发现你手机的选项,选择手机,在脚本里面添加断点进行调试。 3.直接在控制台看日志     当程序在手机上运行后,在控制台中输入:adb logcat -s Unity。即可在控制台中看到输入日志。清除之前logcat命令:adb logcat... Read more about Unity&Android真机调试

Lua编程-小记

1. 将字符串分割成一个一个单元,存在表中 代码如下: local s = "sofgs啊等级高5584撒旦法规" local tb = {} for utfChar in string.gmatch(s, "[%z\1-\127\194-\244][\128-\191]*") do table.insert(tb, utfChar) end -- 如果要提取出字符串中的数字 local s1 = "" local s2 = "" for k,v in pairs(tb) do local n = tonumber(v) if... Read more about Lua编程-小记

Lua编程-时间戳转换

时间戳是种时间表示方式,指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。在程序中经常用于标识某一刻的时间。 1. 时间戳转换为具体时间 代码如下: local t = 141275362 local time = os.date("%Y/%m/%d/%H/%M/%S",t) print(time) 输出:1974/06/24/11/09/22    -- 获得系统当前时间 local table = os.date(“*t”,os.time()) - 年:table.year, - 月:table.month, - 日:table.day, - 时:table.hour, - 分:table.min, - 秒:table.sec 2. 具体时间转换为时间戳 代码如下: os.time() -- 当前时间戳 os.time({year=2012,... Read more about Lua编程-时间戳转换

Unity编辑器扩展 - 寻找脚本引用

我们有时候会在一个项目开发中途进入到项目组,这时就需要我们去熟悉了解这个项目,去看该项目的源码。有的项目的工程组织结构不是很清晰,比较乱,这会给我们熟悉项目带来阻碍。例如,某一个脚本我们想知道工程中那些地方对他进行了引用,一个一个物体的找吗?No效率太低了,我们可以用编辑器扩展写一个小工具来快速查找一个脚本在工程中的所有引用。 第一种方法   该方法需要选择一个物体和一个脚本,然后点击Find按钮,就可以查找到该物体及其子物体中所有要查找的脚本的引用。 具体代码如下: 代码如下: using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; public class MonoFinder : EditorWindow { Transform root = null; MonoScript scriptObj = null; int loopCount = 0; List<Transform> results = new List<Transform>(); [MenuItem("Finder/FindScript")] static void Init()... Read more about Unity编辑器扩展 - 寻找脚本引用

【Unity热更新6】- LuaFramework示例解析

LuaFramework中Main示例场景的执行流程 1、运行游戏,通过执行GameManager物体上的Main.cs脚本,调用该脚本里的Start方法,执行方法里的语句启动游戏。 代码如下: void Start() { AppFacade.Instance.StartUp(); //启动游戏 } 2、上面的Start方法里调用了APPFacade.cs里的StartUp方法,同时调用了AppFacade构造方法。然后调用AppFacade的InitFramework方法(AppFacade继承了Facade。AppFacade里的构造方法使用了父类即Facade里的构造方法。该构造方法调用了虚方法InitFramework。而子类AppFacade又重写了InitFramework方法)。 代码如下: override protected void InitFramework() { base.InitFramework(); RegisterCommand(NotiConst.START_UP, typeof(StartUpCommand)); } 3、AppFacade的InitFramework方法调用了RegisterCommand方法,引向了StartUpCommand脚本,该脚本里初始化所有的管理器(LuaManager、PanelManager、SoundManager、TimerManger、NetworkManager、ResourceManager、ThreadManager、ObjectPoolManager、GameManager),将其加载到GameManager物体上。 代码如下: //-----------------初始化管理器----------------------- AppFacade.Instance.AddManager<LuaManager>(ManagerName.Lua); AppFacade.Instance.AddManager<PanelManager>(ManagerName.Panel); AppFacade.Instance.AddManager<SoundManager>(ManagerName.Sound); AppFacade.Instance.AddManager<TimerManager>(ManagerName.Timer); AppFacade.Instance.AddManager<NetworkManager>(ManagerName.Network); AppFacade.Instance.AddManager<ResourceManager>(ManagerName.Resource); AppFacade.Instance.AddManager<ThreadManager>(ManagerName.Thread); AppFacade.Instance.AddManager<ObjectPoolManager>(ManagerName.ObjectPool); AppFacade.Instance.AddManager<GameManager>(ManagerName.Game); 5、GameManager.cs脚本启用,调用CheckExtractResource方法释放资源。判断数据存放目录释放存在,如果存在就启动OnUpdateResource协程,如果AppConst.UpdateMode=true就更新资源下载,否则不更新。如果不存在,就启动OnExtractResource释放协程。等更新完后,就调用OnInitialize方法,调用Game.lua文件中的OnInitOk方法,进入lua逻辑。 4、PanelManager.cs里有一个创建面板的方法CreatePanel(string name, LuaFunction func = null)方法。方法里传入两个参数:面板的名字和一个lua方法。 5、PromptPanel.lua脚本里提供了控件变量(btnOpen、gridParent)给PromptCtrl.lua脚本使用。... Read more about 【Unity热更新6】- LuaFramework示例解析

【Unity热更新5】- LuaFramework

LuaFramework一些脚本作用: 1、Util:对Mono的功能进行封装,这样不继承Mono的类就能使用Mono的东西了(如transform.Find、GetComponent)和一些其他的工具方法。 2、AppFacade:继承Facade,整套框架的入口。 3、Base:继承MonoBehavior,是一切View和Manager的基类;持有各种Manager的引用;能注册移除View的信息。 4、View:只有一个处理消息OnMessage的方法。 5、AppView:继承View,注册处理View消息。 6、LuaManager:继承Manager,入口类。初始化Lua代码加载路径,引用一个LuaState并封装其功能(读取lua文件、调用方法等)。 7、LuaHelper:提供了各种管理器的获取方法和一些函数回调。 8、ResourceManager:加载AssetBundle的相关操作。在PC平台上默认加载的是Assets\StreamingAssets里的东西,移动平台上则是Application.persistentDataPath。 9、TreadManager:解包与下载资源。 10、PanelManager:默认lua创建的panel都要在tag为GuiCamera的物体下,提供创建panel的方法。 加载AB包的四个步骤: 打包assetbundle。 加载总的清单文件。 加载assetbundle的依赖文件。 加载assetbundle。     先通过Initialize方法加载总的清单文件,然后通过LoadPrefab方法来加载AB包,此时会把这个AB包的请求放在m_LoadQequests中,然后在OnLoadAsset方法对该AB包的所有请求进行处理,通过GetLoadedAssetBundle方法看内存中是否存在这个AB包,如果有就检查该AB包的依赖包是否也在内存中,如果都在,就把请求的包内资源加载进来,并回调方法。如果出现缺包情况,会通过OnLoadAssetBundle方法加载AB包及其依赖包。 热更新的四个步骤: 1、打包:将资源全部打包到StreamingAssets文件夹。 2、解包:在移动端StreamingAssets文件夹是只读的,热更新需要写入文件,因此Application.persistentDatapath这个可读可写的路径才是数据在移动端的存放路径。同时也为了比较MD5值,需要将StreamingAssets的东西解包到Application.persistentDataPath。 3、更新:files.txt文件记录了所有的资源文件及其MD5值,每次进入游戏时都会从服务器下载最新的files.txt,然后对其遍历比较MD5值,如果值不同或不存在则下载。 4、加载:先加载资源的依赖,再加载资源。 框架的工作流程: C#:     打包后启动游戏,GameManager进行判断,如果是游戏安装后第一次启动,就进行解包,如果AppConst.UpdateMode为false,就不会检测更新,否则就进行更新操作。然后进入初始化操作,调用Game.lua中的OnInitOK方法,进入lua逻辑。 lua:     然后调用指定控制器的Awake方法、PanelManager的CreatePanel方法,调用C#代码,创建panel,为其添加LuaBehaviour,调用xxxPanel.lua的方法,获取控件引用,进行逻辑处理。 参考资料 Read more about 【Unity热更新5】- LuaFramework

【Unity热更新4】- ToLua的文件结构

ToLua插件文件结构 1、BaseType:存放一些基础类型的绑定代码。 2、Core:提供的一些核心功能。包括封装的LuaFunction、LuaTable等等。 3、Editor:Extend文件夹里是扩展一些类的方法。ToLuaExport(真正生成lua绑定的代码)、ToLuaMenu(菜单功能对应的代码)、ToLuaTree(辅助树结构)。 4、Misc:杂项,目前有LuaClient、LuaCoroutine(协程)、LuaLooper(用于tick)、LuaResLoader(用于加载lua文件)。 5、Reflection:反射相关的代码。 6、Source:Generate文件夹里是生成的绑定代码。LuaConst(一些lua路径等配置文件)。 Tolua里一些脚本的作用: 1、LuaAttribute.cs:在Tolua生成绑定代码时做一些标识。 2、LuaBaseRef.cs:Lua中对象对应C#中对象的一个基类,主要作用是有一个reference指向lua里面的对象。引用计数判断两个对象是否相等。 3、LuaState.cs:初始化lua路径,加载相应的lua文件,注册前面生成的绑定代码及各种辅助函数。 4、ObjectTranslator.cs:给lua中对C#对象的交互提供了基础,C#中的对象在传给lua时并不是直接把对象暴露给了lua,而是在这个ObjectTranslator里面注册并返回一个索引,并把这个索引包装成一个userdata传递给lua,并且设置元表。在lua通过传进来的对象调用C#的方法是,它会调用ToLua.CheckObject或ToLua.ToObject从ObjectTranslator获取真正的C#对象。 5、LuaFileUtils.cs:通过.lua文件路径和AssetBundle文件路径这两种方式来找.lua文件,并读取返回byte[]。 6、LuaEvent.cs:类似C#中的Event,提供Add和Remove方法。 7、LuaBinder.cs:如果执行的.lua文件需要用到Unity中的类型,则需要用这个类给LuaState进行绑定。 8、LuaLooper.cs:继承MonoBehavior,在Update/LateUpdate/FixedUpdate中执行对应的LuaEvent。 Read more about 【Unity热更新4】- ToLua的文件结构

【Unity热更新3】- ToLua里C#与Lua的交互

C#调用Lua脚本 1、执行一个Lua脚本里的代码 代码如下: LuaState lua=new LuaState(); lua.Start(); string fullPath=Application.dataPath+"/.../.../";(Lua脚本所在的具体路径) lua.AddSearchPath(fullPath); lua.DoFile("xxx.lua"); 2、调用Lua函数 代码如下: LuaFunction func=lua.GetFunction("函数名"); 或者 LuaFunction func=lua[函数名] as LuaFunction; func.Call(); 或 func.CallFunc(); Void CallFunc() { func.BeginPCall(); func.Push(); func.PCall(); func.EndPCall(); } func.Dispose(); //析构Lua虚拟机 3、访问Lua的数组 代码如下: object[] objs=func.Call((object)array) objs[i] Lua调用C#脚本 1、访问C#中的数组... Read more about 【Unity热更新3】- ToLua里C#与Lua的交互

【Unity热更新2】- ToLua使用基础

ToLua是uLua的第三代热更方案。核心思想是利用C#的反射,将C#类代码进行包装,并注册到Lua里。 安装ToLua插件版 Tolua插件版只有最基本的Lua解析,warp,打包等功能,适合大项目集成。 ToLua下载地址 下载完之后把解压的tolua-master里面的Editor,plugins,Source和Tolua到工程的Assets/ThirdParty/Tolua(具体路径随意)。 修改路径 修改LuaConst中的路径: 代码如下: public static string luaDir=Application.dataPath+"/Lua"; public static string toluaDir=Application.dataPath+"/ThirdParty/ToLua/ToLua/Lua"; 修改CustomSettings中的路径: 代码如下: public static string saveDir=Application.dataPath+"/ThirdParty/ToLua/Source/Generate/"; public static string luaDir=Application.dataPath+"/Lua/"; public static string toluaBaseType=Application.dataPath+"/ThirdParty/ToLua/ToLua.BaseType/"; wrap文件 对于Lua中调用的C#里定义的类,需要将其wrap,才能在lua中正确地调用。 首先在CustomSettings.cs脚本中将C#里定义供lua调用的类添加进customTypeList方法里面。 _GT(typeof(你所定义的类名)), 然后执行“Lua”——>“Gen Lua Wrap Files”。wrap出的文件就会生成在“…/ToLua/Source/Generate”的目录下。 热更架构 更新流程... Read more about 【Unity热更新2】- ToLua使用基础

【Unity热更新1】- 热更初探

热更新:可以不重新下载客户端,而更新游戏内容。 热更新原理 热更新涉及的3个目录:   游戏资源目录:包含Unity工程中StreamingAssets文件夹下的文件。安装游戏之后,这些文件会被复制到目标机器上的特定文件夹里。不同平台的该文件夹目录如下: Mac OS/Windows:Application.dataPath+”/StreamingAssets”; Ios:Application.dataPath+”/Raw”; Android:”jar:file://”+Application.dataPath+”!/assets”;   数据目录:“游戏资源目录“在Android和iOS上只读,不能把网上下载的资源放到里面,所以需要建立一个可读可写的“数据目录”,第一次开启游戏后,程序将“游戏资源目录”的内容复制到“数据目录”,这个步骤只执行一次,下次再打开游戏不再复制。游戏过程中的资源加载,都是从“数据目录”中获取、解包。LuaFramework定义的数据目录如下: Mac OS/Windows:c:/LuaFramework/ Android/Ios:Application.persistentDataPath+”/LuaFramework” 调试模式下:Application.dataPath+”/StreamingAssets/”   网络资源地址:存放游戏资源的网址,游戏开启后,程序会从网络资源地址下载一些更新的文件到数据目录。这些目录包含不同版本的资源文件,以及用于版本控制的files.txt(里面存放这资源文件的名称和md5码)。程序先下载“网络资源地址”上的files.txt,然后与“数据目录”中的文件的md5码比较,更新有变化的文件。 如何利用Lua进行热更新 在移动端编写Lua的解析器,通过这个解析器,可以运行最新的Lua脚本,然后C#代码不需要变动,把控制游戏逻辑的代码都写成Lua脚本,以后只需要修改Lua脚本就好了。 Read more about 【Unity热更新1】- 热更初探

Lua语法

Lua是一个以性能著称的轻量级的脚本语言。可以跨平台运行解析,而不需要编译的过程。Lua是一个区分大小写的编程语言。 Lua语法学习 变量 1、标识符 Lua中使用标识符定义一个变量,标识符由字母,数字,下划线组成。最好不要使用下划线加大写字母的标识符,因为Lua的保留字也是这样。一般约定,以下划线开头连接一串大写字母的名字(如_VERSION)被保留用于Lua内部全局变量。 2、定义变量 Lua定义变量是没有类型的,根据存储什么数据来决定是什么类型。如num=10。 3、变量类型 (1) nil表示空数据,等同于null。 (2) boolean布尔类型,Lua把false和nil看作是“假”。数字0与空字符串“ ”为真。 (3) string字符串,用“”或‘’来表示,用2个方括号[[]]来表示“一块”字符串。 (4) number小数类型,表示双精度类型的实浮点数。 (5) table,其实是一个“关联数组”,数组的索引可以是数字或字符串。Lua中的数组索引是从1 开始的。 4、Lua变量 有三种类型:全局变量、局部变量、表中的域。 变量默认是全局的。全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,如果想删除一个全局变量,只需要将变量赋值给nil。删除table表里的变量也是一样的。 局部变量的作用域从声明位置开始到所在语句块结束。 运算符 1、算术运算符。+、—、*、/、%、^(没有++,–)。 2、关系运算符。<、>、<=、>=、==、~=(表!=)。 3、逻辑运算符。and、or、not分别表示与、或、非。 4、连接运算符。、、表连接两个字符串。 5、一元运算符。#,返回字符串或表的长度。 if语句 代码如下: 1、if 条件 then end 2、if 条件 then else... Read more about Lua语法

计算机图形学 - shader分类

shader就是一段运行在GPU上的程序。它负责将输入的Mesh网格以指定的方式和输入的贴图或颜色等组合作用,然后输出,绘图单元可以依据这个输出来将图像绘制到屏幕上。输入的贴图或颜色等,加上对应的shader,以及对shader的特定的参数设置,将这些内容(shader及输入参数)打包存储在一起,得到的就是一个Material,然后我们可以将材质赋予合适的renderer(渲染器)来进行渲染(输出)。 shader按管线分类 1、固定功能渲染管线:这是标准的几何&光照(Transforming&Lighting)管线,功能是固定的,它控制着世界、视、投影变换及固定光照控制和纹理混合。T&L管线可以被渲染状态控制,矩阵,光照和材质参数。功能比较有限。基本所有的显卡都能正常运行。 2、可编程渲染管线:对渲染管线中的顶点运算和像素运算分别进行编程处理,而无须像固定渲染管线那样套用一些固定函数,取代设置参数来控制管线。 unity中的3种shader 1、Fixed function shader:固定功能着色器,用于高级shader在老式显卡无法显示时的fallback。 2、Surface shader:表面着色器,unity推崇的shader类型,使用unity预制的光照模型来进行光照运算,只需要一个表面处理函数surf即可,使用CG/HLSL语法。 3、Vertex & Fragment shader:顶点&片段着色器,属于可编程渲染管线,使用CG/HLSL语法。 3种shader的异同点 共同点: 都必须从唯一一个subShader开始。 Properties参数部分,作用及语法完全相同。 具体功能都在SubShader里(Subshader: 子Shader,Shader会自上而下运行第一个硬件能支持的Subshader,主要作用是对不用硬件的支持。) 都可以打标签,例如Tags { “RenderType” = “Opaque” } LOD 200 以及Lighting On等。 都可以Fallback。 都可以处理基本的功能,例如光照漫反射(Diffuse)以及镜面反射(Specular)。但是Vertex and Fragment和Surface都能实现Fixed function实现不了的高级功能,例如基于uv计算的效果等等。 不同点: Fixed function shader以及Vertex and... Read more about 计算机图形学 - shader分类

计算机图形学 - 渲染管线

渲染管线是实时渲染的底层实现。简单来说,就是告诉GPU一堆数据(视点、三维物体、光源、照明模型、纹理等),然后GPU通过一系列计算,将图像绘制在屏幕上的一个过程称为渲染管线。管线的主要功能是生成或渲染二维图像、三维物体、光源、着色方程式、纹理等。生成的二维图像能很好的反应三维物体或三维场景。 渲染管线的三个阶段 1、应用程序阶段:Unity3D做好碰撞检测、视锥剪裁、场景图建立、空间八叉树更新等等计算,CPU、内存把计算好的数据(顶点坐标、法向量、纹理坐标、纹理信息)通过数据总线传给GPU做下一步处理。其中顶点坐标、法向量、纹理坐标放在Vertex buffer中在顶点处理阶段使用,纹理信息放在Texture buffer中在光栅化阶段使用。 2、顶点处理阶段:主要工作就是“变换三维顶点坐标”和“光照计算”。负责顶点坐标变换、光照、裁剪、投影以及屏幕映射,该阶段基于GPU进行运算。 顶点处理的转换操作和对应空间:对象空间(世界转换)–>世界空间(视点转换)–>相机空间(投影转换)–>裁剪空间。 转换后的顶点坐标组成线、片元,超出屏幕外的进行裁剪,这一步就是图元装配和三角形处理。顶点处理阶段完成之后输出转换后的顶点坐标和需要绘制的片元。 3、光栅化阶段:计算片元中每个像素对应的颜色值和深度值保存到像素缓冲区中。这里就要用到应用程序阶段计算产生存放在Texture buffer中的纹理信息。 光栅化阶段的步骤:片元、坐标–>背面剔除–>alpha测试–>模板测试–>深度测试–>融合处理–>抖动处理–>逻辑运算–>像素帧缓冲。 Read more about 计算机图形学 - 渲染管线

Unity编辑器扩展 - 自定义窗口

自定义编辑器窗口 1、新建一个脚本,引入编辑器命名空间:using UnityEditor; 2、继承EditorWindow再设置MenuItem。 代码如下: Public Class MyEditor:EditorWindow { [MenuItem("Window/MyEditor")] Static void SetWindow() { EditorWindow.GetWindow<MyEditor>(); } } 这样在菜单栏中点击Window->MyEditor就创建出一个自定义的窗口。 3、对创建的窗口进行编辑。 代码如下: private String text; Void OnGUI() { //输入框 text=EditorGUILayout.TextField("输入文字:",text); //如果点击了XX按钮 if(GUILayout.Button("XX")) { ....//按钮点击处理 } //提示信息 EditorGUILayout.LabelField("注意:....."); //如果窗口的一行有许多控件。会有开始,结束标志,然后在中间定义各种控件。 EditorGUILayout.BeginHorizontal();//定义行控件开始标志。 .....//定义控件 EditorGUILayout.EndHorizontal();//定义行控件结束标志。 //如果再定义第二行控件的话。... Read more about Unity编辑器扩展 - 自定义窗口

项目优化之针对内存

内存方面的开销主要来源于Unity内部内存占用和Mono托管内存占用。 资源内存占用 1、纹理 不透明贴图压缩格式:ETC 4bit(安卓),PVRTC(iOS);透明贴图压缩格式:RGBA 16bit或RGBA 32bit。 尽量降低纹理尺寸。如果512×512的纹理显示效果可以的话,就不要用1024×1024的纹理。 开启mipmap功能。目的是降低渲染带宽的负荷,提升游戏的渲染效率。3D场景模型和角色建议开启Mipmap功能,但UI纹理就没必要开启Mipmap功能了。此外,2D游戏也不需要开启Mipmap功能。 关闭Read&Write选项。开启该选项会使纹理内存增大一倍。 2、模型 尽量控制模型的面数,在300~1500之内比较合适。 尽量保持骨骼数在30根之内。 材质数量最好不要超过3个。 3、音频 BGM使用.ogg或.mp3的压缩格式。 音效使用.wav或.aif的不压缩格式。 Mono托管内存占用 Unity中Mono可以自动地改变堆的大小来适应所需要的内存,并适时地调用GC(垃圾回收)来释放已经不需要的内存。但存在一个问题,Mono的堆内存一旦分配,就不会返还给系统,这意味这Mono的堆内存只升不降,产生了大量的无效堆内存。 GC(垃圾回收) 字符串连接会产生一个新的字符串,而之前的字符串就成为了垃圾,而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧字符串的空间会被GC当做垃圾回收。使用string.format或stringBuilder代替。 尽量不要使用foreach,而使用for。foreach会涉及到迭代器的使用,而每循环一次迭代器会产生24Bytes垃圾。 不直接访问gameobject的tag属性。把go.tag==”human”换成go.CompareTag(“human”),因为访问物体的tag属性会在堆上额外分配空间。 使用对象池。实现空间的重复利用。 尽量使用struct而非class,因为struct是栈区,class是堆区。 注:以上内容来源于网上搜集整理 Read more about 项目优化之针对内存

项目优化之针对GPU

GPU负责整个渲染流水线,它会对CPU传过来的数据进行一系列的计算,最后输出到屏幕上。因此,从顶点,像素,带宽等方面对GPU进行优化。 顶点优化     顶点计算是指GPU必须对每个网格上的顶点进行的计算操作,顶点计算主要受到两个因素的影响:需要计算的顶点数量,每个顶点需要进行的操作。 1、优化几何体     剔除不必要的复杂网格,对于不在视图中的物体,不需要过多的细节网格。可以创建面数较低的模型来替代高模。 2、LOD(level of detail)技术     根据摄像机距离对象的远近,选择不同精度的模型,目的是降低显卡的负荷,但缺点是占用更多的内存。     建议在建模软件中按照精度由高向低地为模型命名为”模型名字_LOD0”,”模型名字_LOD1”等等,最后的序号越低代表模型精度越高。这样的命名规则可以让unity自动为模型添加LOD组(LOD Group)。导出fbx之前,需要让这两个模型位置重合,其他导出设置与普通模型导出一致。 3、遮挡剔除(occlusion culling)技术     遮挡剔除是用来消除在其他物体后面看不到的物体,不用再对那些看不到的顶点进行计算,进而提升性能。     在默认的渲染管线中,会根据摄像机的视锥体范围对场景模型进行剔除操作,在视锥体以外的物体不被渲染。在视锥体以内的物体会以离摄像机最远的物体开始渲染,再逐渐渲染靠近摄像机的物体,后渲染的物体会覆盖先渲染的物体,这种技术称为视锥体剔除。     但视锥体剔除只会剔除摄像机视角以外的物体,而对于视角内但被其他物体遮挡的物体还是会渲染。而遮挡剔除是剔除一个在摄像机视角内被其他物体遮挡的物体,使其不被渲染。 原理:     在场景空间中创建一个遮挡区域,该遮挡区域由单元格(cell)组成,这些单元格会把场景拆成多个部分。当摄像机能看到该单元格时,该单元格中的物体会被渲染。而被其他单元格遮挡住的单元格中的物体将不会被渲染。 步骤 自动生成遮挡剔除 选择场景中需要生成遮挡剔除的物体下的所有子物体,在Inspector窗口勾上static选项,把该对象设置为静态物体,父物体不要设置为静态物体,静态类型设置为“Occluder Static”和“Occludee Static”。 选择“Window”->“Occlusion Culling”,打开遮挡剔除面板,在Scene中会添加被划分为多个单元格的包围体。点击“Bake”按钮,等待遮挡区域生成完毕。 在Scene窗口的右下角有一个遮挡剔除测试面板,把Edit改为Visualization,移动场景中的摄像机,在Scene中会显示当前被剔除的效果,这时帧速率也会因为渲染量的减少而大幅度增加。 手动生成遮挡剔除 新建一个空的游戏对象OcclusionAreaObject,为其添加一个Occlusion Area组件,设置Occlusion Area的范围使其能覆盖需要遮挡剔除的区域,场景中的其他必要对象都要设置为静态的。 打开遮挡剔除面板,点击“Bake”按钮生成遮挡区域。 在遮挡剔除测试面板中,把Edit改为Visualization模式,观察遮挡剔除效果。 入口遮挡剔除 使用入口遮挡可以动态的通过脚本控制使某个物体是否拥有遮挡剔除效果。 为A对象添加Occlusion Portal组件,并把它的open属性取消勾选,A不需要进行“遮挡烘焙”,因为A不要受到系统遮挡剔除的效果。并且要保证摄像机不能看到A后面的所有对象。 在A上添加一个脚本,用代码控制open属性的开启关闭。 4、层消隐     通过给每个层(layer)设置一个可以看见的距离,当摄像机与设置该层的物体的距离超过这个值,该物体将不会再被渲染。 代码如下:... Read more about 项目优化之针对GPU

项目优化之针对CPU

CPU方面的性能消耗主要是引擎渲染、物理计算、脚本代码等方面的性能开销。因此针对这几个方面对CPU进行优化。 降低DrawCall     Unity生成一帧画面的过程:引擎先确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置、法线、UV等),索引(顶点如何组成三角形),变换(物体的位置,旋转,缩放,摄像机位置等),相关光源,纹理,渲染方式(由材质/shader决定)等数据准备好,然后通知GPU开始绘制,GPU基于这些数据,经过一系列的运算,在屏幕上画出成千上万的三角形,最终构成一副图像。     Unity每次准备数据并通知GPU渲染的过程称为一次DrawCall,该过程是逐个物体进行。 1、Static Batching(静态批处理)     将静止的物体标记为Static,批处理相同材质或纹理的静态对象,合并材质。但有个缺点是它把批处理中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体,这样就需要分配相应大小的内存,会导致内存损耗。因此,要平衡各方面的效率。     原理是化整为零,将多个场景物体预先合成一个大的物体进行绘制,unity会先整合成一个大的vbo(顶点缓冲区对象),而不整合IBO(索引缓冲区对象),一次性提交vbo给GPU,然后并不是把整个vbo都绘制,而是每次需要绘制其中某些物体时改变IBO,选择大vbo上的某一段进行绘制。静态合批可以将多个小物体的绘制合并成一个大物体的绘制,减少对渲染状态的改变,它一次并行绘制多个物体。一个vbo的大小是有限制的,如果物体数量过多,也会被拆分成多个绘制。 2、Dynamic Batching(动态批处理)     该处理是完全自动进行的,对于顶点数在300以内的可移动物体,只要使用相同材质,就会组成Batch。 Tips 动态批处理仅支持小于900顶点的网格物体。 如果着色器使用顶点位置,法线和UV值三种属性,那么只能批处理300顶点一下的物体;如果着色器需要使用顶点位置,法线,UV0,UV1和切向量,只能批处理180顶点以下的物体。 不要使用缩放。分别拥有缩放大小(1,1,1)和(2,2,2)的两个物体不会批处理。 统一缩放的物体不会与非统一缩放的物体进行批处理。 使用不同材质的实例化物体将导致批处理失败。 缩放大小(1,1,1)和(1,2,1)的两个物体不会进行批处理,但使用缩放大小(1,2,1)和(1,3,1)的两个物体可以批处理。 使用lightmap的物体含有额外(隐藏)的材质属性,比如lightmap的偏移和缩放系数等,不会批处理。 多通道的shader会妨碍批处理。 预设体的实例会自动使用相同的网格模型和材质。 合并图集     将多个纹理打包成图集来尽量减少材质的使用。合并贴图时应该注意选择同时出现在屏幕的对象贴图进行合并。 光照和阴影     实时光照和阴影可能增加DrawCall,带有光源计算的shader材质会因为光照产生多个DrawCall。使用灯光会打断DrawCall Batching,尽量使用烘焙灯光贴图来实现灯光效果。 物理组件 设置一个合适的Fixed Timestep。减少物理计算的次数,来提高游戏性能。 不要使用网格碰撞器(mesh collider)。 脚本代码 不要频繁使用GetComponent等接口,尤其是在循环中,最好将组件缓存。 使用内建的数组,比如用Vector3.zero而不是new Vector(0,0,0)。 善于使用ref关键字。 将经常需要使用的属性查询缓存起来。 习惯性的将暂时不用的Gameobject设置为非激活。 不要频繁使用Instantiate和Destroy,使用对象池。 尽量少使用Update,LateUpdate,FixedUpdate,这样提升性能,多使用事件(不是sendmessage,而是C#中的事件委托)。... Read more about 项目优化之针对CPU

读取Sqlite数据库文件

Sqlite是一款轻量型的数据库,它占用资源非常的低。在Unity里可以很方便地对Sqlite数据库中的数据进行增,删,改,查等操作。 基本语法 1、增 insert into 表名 values(值1,值2) insert into 表名(列1,列2…)values(值1,值2) 2、删 delete form 表名 where 列名=值 3、改 update 表名 set 列名=新值 where 列名=某值 4、查 select 列名 from 表名 示例 1、在Assets文件下创建Plugins和StreamingAssets文件夹,导入Mono.Data.Sqlite.dll和Sqlite3.dll两个库文件到Plugins文件夹,将创建的数据库文件放到StreamingAssets文件夹。创建的数据库文件如下图: 2、创建一个空物体,在其上添加一个脚本,命名为SqliteScript.cs 代码如下: using UnityEngine; using System.Collections; using Mono.Data.Sqlite;//引入命名空间 public class... Read more about 读取Sqlite数据库文件

读取XML文件

XML,是一种可扩展标记语言,是一种简单的数据存储语言,使用一系列简单的标记来描述数据。在unity中主要是用来处理大量的字符串。 XML常用类 XMLDocument —— XML文件类 XMLNode —— XML节点类 XMLAttribute —— XML属性类 XMLElement —— XML元素类 示例     在Unity中创建一个空物体,在其上挂载一个脚本,命名为XMLScript.cs,先写个方法来在Unity中生成一个XML文件,然后再写个方法来读取这个XML文件。 代码如下: using UnityEngine; using System.Collections; using System.Xml;//引入命名空间 public class XMLScript : MonoBehaviour { //声明一个XML文档对象,通过该对象可以往文档里添加不同的元素 XmlDocument doc; void Start() { //初始化XML文档对象 doc = new XmlDocument();... Read more about 读取XML文件

读取Json文件

Josn是一种轻量级的数据交换格式,JSON能够描述四种简单的类型(字符串、数字、布尔值及null)和两种结构化类型(对象及数组),在Unity里经常用Json来处理大量的字符串,容易解析,效率非常快。 基本结构 1、语法 数据存在键值对中 数据由逗号分隔 花括号保存对象 方括号保存数组 eg:表示两个学生的基本信息 {"Student":[{"name":"Tom","age":19,"gender:"M"}, {"name":"Marry","age":22,"gender:"W"} ] } 2、Json开发 使用两种类库:(需要将System.Json.dll和LitJson.dll放到工程目录下) System.Json(生成Json文件) LitJson(解析Json文件) (1)System.Json的类 JsonArray类 JsonArray是0个或多个JsonValue对象的有序序列 JsonObject类 JsonObject是一个无序的0个或更多个的键值对集合 JsonValue类 一个具体的Json对象的Value值 示例     在Unity中创建一个空物体,在其上挂载一个脚本,命名为JsonScript.cs,写个方法来在Unity中生成一个Json文件,然后再写个方法来读取这个Json文件。前提要导入System.Json.dll和LitJson.dll两个库文件到Unity中。 代码如下: using UnityEngine; using System.Collections; using System.Json; using LitJson; using System.IO; public class JsonScript :... Read more about 读取Json文件

读取CSV文件

CSV是一种以逗号间隔的文本文件,经常把游戏中一些人物属性,武器数值存放在CSV文件中,这就需要我们在Unity中用代码将这个CSV文件中的数据读取出来放在一个数组中,在需要的地方获得这些数据。 如何创建CSV文件 通过创建txt文件,输入数据,以逗号分隔,注意逗号是英文输入法下的逗号,然后另存为**.csv文件,编码格式改为UTF-8,保存即可。 通过创建Excel文件,然后另存为**.csv文件,点击确定即可。 解析CSV文件     将weapon.csv文件拖到Resource文件夹下的Data文件夹下。在Unity中新建一个脚本,命名为ReadCSV.cs。 代码如下: using UnityEngine; using System.Collections; public class ReadCsv : MonoBehaviour { //定义一个数组,存放表格数据 private string[][] _ArrayData; void Awake () { //拿到CSV文件 TextAsset data = Resources.Load("Data/weapon", typeof(TextAsset)) as TextAsset; //将表格中的数据按行分割存储 string[] lineArray = data.text.Split("\r"[0]); //初始化数组 _ArrayData... Read more about 读取CSV文件

如何在Unet中添加多个玩家角色

最近在给项目中添加局域网功能时,发现在Unity的新版网络Unet中的NetworkManager下只能添加一个PlayerPrefab,也就是说在每个客户端只能Spawn出相同的角色模型,这在实际游戏项目中是不可能的,整个游戏只有一个模型会使游戏感到单调,然后查了一些资料,在Unity工程中测试了一下,终于找到了解决办法。 解决办法     解决方案就是:重写NetworkBehavior中的OnServerAddPlayer方法,自己写一个脚本让他继承NetworkManager,不用引擎给的那个NetworkManager组件。 代码如下: public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader) { GameObject player = Instantiate(Resources.Load("Player1")) as GameObject; NetworkServer.AddPlayerForConnection(conn, player, playerControllerId); } 然后将Player添加到各个客户端: 代码如下: public override void OnClientConnect(NetworkConnection conn) { NetworkMessage test = new NetworkMessage(); test.chosenClass = chosenCharacter;... Read more about 如何在Unet中添加多个玩家角色

音频管理类

在Unity中游戏开发中,整个游戏中有许多地方要添加音效,在每个地方都添加AudioSource组件是非常消耗资源的,所以建一个音频管理类将所有的音频统一进行管理,将非常的方便。 首先新建一个空物体,在其上添加一个脚本,命名AudioManager.cs,用来管理所有的音频片段,代码如下: using UnityEngine; using System.Collections; using System.Collections.Generic; public class AudioManager : MonoBehaviour { //音频片段的数组 public AudioClip[] audioClipArray; //音频库 private static Dictionary<string, AudioClip> audioDic; //背景音乐的音频源 private static AudioSource bgm; //挂载此脚本的游戏对象本身 private static GameObject _Instance; void Awake () { _Instance =... Read more about 音频管理类

那些游戏告诉我们的那些哲理

《俄罗斯方块》告诉我们:犯下的错误会积累,获得的成功会消失; 《植物大战僵尸》告诉我们:须常调整状态,方能应付不同挑战; 《愤怒的小鸟》告诉我们:有时沉下身心,是为了飞的更高; 《跑跑卡丁车》告诉我们:永远别觉得时间还多,可以浪费; 《水果忍者》告诉我们:水果与炸弹同在,机遇与挑战并存; 《超级玛丽》告诉我们:成功看的不是你跳得多高,而要看你究竟跑得多远; 《魂斗罗》告诉我们:阻止你前进的往往不是前方的敌人,而是背后的黑枪; 《贪吃蛇》告诉我们:打败自己的不是糖衣炮弹,而是自己越来越长的身体,自己才是最强大的敌人; 《沙罗曼蛇》告诉我们:躲在后面不一定能避开危险,而有时需要尝试向前; 《捕鱼达人》告诉我们:网撒得多并不一定能捕到,关键是找准时机和位置; 《拳皇》告诉我们:要想进入下一关,要嘛把站在你面前的人打死,要嘛就要保持自己的血(命)比对方长; Read more about 那些游戏告诉我们的那些哲理

Unity中Asset Bundle的打包和加载

Asset Bundle是Unity引擎提供的一种用于存储资源的文件格式,开发者可以通过Asset Bundle将游戏中所需要的各类资源打包压缩并上传到网络服务器上,另外,在运行时游戏可以从服务器上下载改资源,从而实现资源的动态加载。 下面通过一个工程来讲解Asset Bundle的打包和资源的加载。资源之间没有依赖关系的,Unity官方文档有,比较简单,下面讲资源之间有依赖关系的打包和加载。 打包 1、在场景中新建一个Cube,将其做成一个预设体,再创建一个材质,命名为red,分别将预设体cube和材质red的Asset Bundle名称命名为cube,red,在Unity中Asset Bundle的名称默认为小写。 2、新建一个C#脚本,命名为BuildAssetBundleScript.cs。代码如下: 代码如下: using UnityEngine; using System.Collections; using UnityEditor; public class BuildAssetBundleScript : MonoBehaviour { [MenuItem("AssetBundle/Build")] static void BuildAssetBundle() { BuildPipeline.BuildAssetBundles(Application.dataPath + "/AssetBundle"); } } 注:通过 BuildPipeline.BuildAssetBundles 方法即可将设置了Asset Bundle的资源全部打包,括号里面是存放Asset Bundle文件的文件夹路径,必须要先在工程中创建出这个文件夹,不然打包的时候会报错。 加载 3、在场景中新建一个空物体,在其上添加一个脚本,命名为LoadAsset.cs。代码如下:... Read more about Unity中Asset Bundle的打包和加载

FPS类游戏的关卡设计

【团队竞技】是规则十分简单的PVP模式,目前广泛出现在各类主流FPS游戏中。因此,设计团竞地图也成为关卡策划初学者的必修课。 区域划分 常规的团队竞技地图,在区域划分上,可以大致分为五个区域:   出生区域*2, 保护区*2, 交战热区*1. 关卡设计者需要利用结构分割出上述的区域特征。通常需要注意以下原则:   1、出生区通往外界的出口方向不能唯一,不然双方实力不均时会造成被压家,而无法出去的可能。   2、在出生区出口,不要让刚出生的玩家遭遇侧面埋伏。   3、保护区内,结构平衡性应偏向受保护的一方,避免战局出现一边倒的情况。   4、要有清晰的,不受干扰的对枪视野。如果没有中间热区的结构,下图中红蓝双方将没法安心的对狙,他们需要顾及的角度会增加数倍。 Tips:团队竞技的初学者可以先从对称地图做起,轴对称、中心对称均可。 信息控制   如图,红蓝两方,被靠墙的一座高墙隔开,那么他们的交战可能性就只有左边的路径。 Tips:在实际设计中,可以灵活利用不对等结构,设计出占城的攻防关系。 视野控制   在设计的过程中,我们经常遇到这样的问题,设计师希望玩家在A区交战,但玩家经常会遭受B区和C区的冷枪,死的不服;这种干扰,在快节奏的团竞模式中是非常不佳的战斗体验。原因就是我们没有为A区制造出合理的视野保护。如何控制视野呢?常见的方法有以下几种: 1、平行法。   为了制造更单纯的对枪机会和更快速的行动路线。利用场景边缘较长的围墙结构,配合一段平行的掩体,将玩家视野控制在一个狭长范围内。 2、横断法   以分割玩家视距为目的,分割区域,制造双方的盲区,使一方在另一方的盲区内形成战斗的随机性,提高竞技乐趣。   常见结构: 3、高低差:   通过高度的落差,制造视觉的盲区,分割区域。 4、垂直隔断:   通过Z轴方向上设置障碍,阻挡视野,在一定角度上形成保护区域。   习题:下图结构中,红蓝双方在图中位置相遇,假设他们枪法相当,都有左右闪躲的机会。那么,谁的优势更大? 先用平面图分析一下双方的视野 虽然掩体完全不对称,但可以看出,双方视野范围不相上下,那是不是战斗优势也相当呢?我们再看下图 1、红方走进对方盲区要比蓝方更快。 2、当红方率先进入蓝方盲区之后,红方再度出现的可能性,将会变成2个。 3、而相比之下,蓝方要想在3秒内不陷入劣势,只能选择正面迎敌,因为往左往右移动,都会相对被动。   所以这题的答案是:红方因为能够率先隐藏自己,主动性更强,优势略大。   这是一个标准的利用视野制造的不平衡案例,我们称之为“瓶口结构”。   Tips:通过对视野的控制,让玩家按设定的进攻路线推进,从防守区到交战热区,再到对方的防守区之前,尽量不要让遇敌的角度大于90度。这样可以让玩家唉尽可能少的受到侧方向的干扰,打的爽快,死的明白(如下图)。 5、有偿获利   打过CF运输船的同学应该都知道,出生区域右侧有一个暗道,暗道通向敌后防守区域。形成战术迂回,偷袭敌方。   最近有看到一篇分析文章,将这个点位称作突袭要地,并批评出口处设计的过于暴露,掩体太少,很容易被击毙。   我认为这个分析是不准确的:   1、如果玩家选择这条路线进攻,将会经过一段较长的时间,没有遇敌,杀不到人。当他暴露在出口时,有较大几率遇到背身或侧身的敌人,这是对其前面没有交战,没有击杀的时间补偿。所以这里只是玩家相同收益下的策略选择,而非战术要地。这个设计就是给一块糖,达到杀敌效率上的“收支平衡”。... Read more about FPS类游戏的关卡设计

MOBA类游戏的核心设计分析

MOBA,中文译为“多人在线战术竞技游戏”,本篇文章将要讨论的是目前比较火的DOTA2,LOL等代表性的“DOTA LIKE”游戏,剖析其核心玩法。 MOBA类游戏的核心玩法 以DOTA2地图举例: 这是整个游戏界面的缩小地图,每一局游戏都是独立且互不影响的,玩家在每局游戏的开始,都需要从英雄池中选择一个英雄来作为本局游戏自己所操控的英雄,根据游戏人数最多上限为10人进行5人VS 5人的战斗,率先摧毁敌方基地的一方玩家,将获得胜利。  如图所示,每个数字分别代表:   ①泉水:英雄出生/复活点,可以回复生命值和魔法值,可以购买装备。   ②基地:率先被摧毁的一方将输掉本局比赛。   ③兵营/防御塔:失去兵营的一条线小兵强度将弱于对方的本条线,防御塔自动攻击进入攻击范围内的敌人,具有反隐和传送支点的功能。   ④野区:60秒一次刷新一波野怪,不同区域的野怪类型不同,击杀可获得经济。   ⑤ROSHAN:地图中最强力的中立野怪,击杀可获得丰富的奖励。   游戏的玩法:简单来说就是通过本方5人操控的不同英雄,合理分配不同的进攻路线,通过获取不同的线上/野区收益,购买装备变得强大,击杀对方英雄/摧毁对方防御塔,通过摧毁任一条线上的地方防御塔/兵营后,摧毁对方基地,即可获得本局游戏的胜利。游戏考验了玩家的团队合作,意识,大局观,操作,心理素质,运气等综合能力,累积优势,化为胜势,从而得到胜利。这是MOBA类游戏的常规游戏方式,在这里只是简单介绍了其核心玩法,不再继续对玩法内容进行扩展。 DOTA&LOL 接下来将详细的对比一下,作为MOBA界无可争议的两大巨头,核心玩法相类似的两款游戏,在游戏设计理念上的不同,如何让它们走向两条截然不同的两条道路。 一、地图构造 1.地图大小   DOTA2中,具有较慢移速(270)的炸弹人,从泉水走到左上角需要的时间的55秒,而在LOL中,具有较慢移速(325)的机器人走到同样位置需要40秒,光从英雄的初始移动速度到达的地图时间来看,LOL的地图相对于DOTA2来说更为紧凑,地图的大小,影响到的是方方面面,例如野区构造,支援方式,团战等等,当然,单纯的比较地图大小是没有意义的,英雄的技能可达长度,移速增长幅度,瞬间大范围转移的方式等等,都会对游戏造成较大影响,但光从地图来看,DOTA2游戏内容要比LOL更多,LOL的游戏内容更加紧凑。 2.地图视野   除了常规的英雄视野以外, LOL还有“草丛”的机制,草丛外的角色正常情况下是无法拥有草丛内的视野的,DOTA2也有类似的机制,不过DOTA2要复杂一些,玩家所能看到的视野都遵循一套完整的视野构筑系统。 LOL: DOTA2: 分析:   DOTA2和LOL虽然都有同样的常规角色视野和道具视野,以及功能类似的草丛视野,但两者之间的机制是有很大不同的,LOL中三个不同的视野机制更像是独立的三个系统:当它们各自满足某种条件时,可以打开另一部分系统的权限,从而获取视野。   DOTA2虽然影响视野的因素更为复杂,但我更愿意把它归为同一个因素:现实,或者说这个因素的名字叫真实视野。也就是符合现实社会影响人们获取视觉信息的常规因素。即:视力,光线,道具,障碍。白天自然比夜晚看的远,山峰上的人自然比平原上的人看的更远。处在视野盲区的物体自然看不到等等。即使是提供视野的道具,也需要遵循这样的规则,所以在地图的视野这一方面,DOTA2要比LOL更为复杂一些,这种复杂是基于现实出现的多种复杂情况而导致的。LOL的话在这一方面要更为清晰,视野系统带给玩家的功能和视觉反馈更加直观。 3.野区构造   上方是LOL和DOTA2的地图构造,LOL中的红色区域标注了野怪的刷新位置,DOTA2地图中的黄点区域是野怪的刷新位置。 分析: 两者之间的区别在于: 1.LOL中的野怪刷新点要比DOTA2略多,并且更为密集。 2.LOL野怪刷新频率要比DOTA2小很多。   而由于LOL中有惩戒的召唤师技能存在,几乎任何英雄都可以打野,而打野的英雄选择上更多考虑的是GANK和清野的效率,而DOTA2由于先天没有清野技能的存在,前期要想单独清野的难度不小,在打野英雄的选择上更多考虑是清野效率而非GANK。 两者的野区机制造成的区别有两个:   1.对于前期来说,LOL的节奏更快,一个优秀的打野可以在前期帮助线上建立很大的优势,对游戏节奏的影响非常巨大,而在中期,由于巨龙所带来的永久BUFF,下路的竞争尤为激烈,对战双方往往会不断在下路寻找优势,早早抱团在巨龙附近爆发团战,赢得团战的一方不仅获得人头优势,还能拿下巨龙增加优势,使游戏的节奏加快。反观DOTA2,前期因为刷野效率不高使打野位几乎不会主动GANK而以发育为主,节奏比起LOL要慢不少。   2.DOTA2由于野区刷新频率更快,在经济获得的比重占据更多,中后期为C位补充经济,能够掌握野区视野的一方往往能够赢得比赛,所以双方的视野布置和GANK很多时候会针对野区,相比起LOL,野区的实际作用和战略意义要更加重要。 二、技能 LOL中和DOTA2中都拥有很多不同的技能,根据技能特性也大致可以分为:   ①控制型:无法移动,施法,使用道具等。   ②限制型:限制或禁止某一项能力的使用。... Read more about MOBA类游戏的核心设计分析

角色动画的apply root motion和bake into pose问题

# 问题 导入到unity3d的一些带动画的模型,有的动画会有位移变换,有的则没有。而是否将模型在动画中的位移变换,应用到模型在场景中的位移,有两个地方需要设置:Apply Root Motion 和 Bake into Pose,因此unity中将动画的变换分成两种:Root Transform 和 Body Transform。 # 具体分析 1.如果设置Root Transform,将会影响模型的实际位置,前提是需勾上Apply Root Motion,如果不勾上变换将不应用。模型的位置不会改变。 2.如果设置Body Transform,在场景中模型位置会发生变换, 但实际的位置将不会发生变化。Apply Root Motion起两个作用: (1).决定是否应用Root Transform,如果应用,在播放动画的同时,模型的位置会同时发生变化,如果不勾选,将不应用Root Transform。所有的变化将不起作用。 (2).在动画结束后,将Body Transform中的变换在动画结束后应用到模型。 下面将分为四种情况(×代表不勾选,√代表勾选): 情况 Apply Root Motion Bake into Pose 1 × √... Read more about 角色动画的apply root motion和bake into pose问题

第一幕

很长时间里就想找一个简单点的,干净点的地方写一些关于学习和项目中的一些收获,放一些比较好的技术文章,奈何国内一些博客网站杂的东西太多,不太喜欢,后来在知乎上看到有人推荐可以用github page来搭建一个个人博客,看了一下感觉非常nice,网站上面的东西全由自己部署,重要的是这还是免费的,这对我等青(Qiong)年(bi)太友好了。 # 为什么要搭建这个博客网站 虽然现在已经很少人写博客了,但写博客有一个好处:你写出来的东西不仅仅是你自己看,放到网上有可能会被别人看到,所以如果你不想被别人说垃圾,就要认认真真的写好,这个过程也是对自己思路的一个提升,你不仅要能做的出来,还要能够说出来。 # 怎么搭建这个博客网站 其实这个博客很早就搞了,只是当时这里面有些东西不太懂,然后搁置了,后来有一些问题慢慢地解决了,直到现在才写这篇序言。这个博客主要是由Git+Github+Markdown+Jekyll来搭建环境,我是直接从网上下的 jekyll模板,然后改了一下就作为自己的博客,这个模板涉及到jekyll,ruby等编程语言,我虽然不太懂这方面,但是看懂代码改一下还是会的,相信这对大部分码农应该不是难事。 # 最后 以后有时间,把自己以前写的读书笔记和一些解决的技术难点放到这里,不仅可以防止自己忘了还能回来看看,还能为其他遇到同样问题的小伙伴提供一个解决方案,一举两得。 Read more about 第一幕