自己跟着学完了,写了不少代码,会放在CSDN代码库,因为老师并没有提供源码,录屏也不是完全连续,所以难免有代码缺少、无法运行的情况,但是确实学到了不少真本事,主要是了解老师的架构思想。
b站课程地址
课程我自己是跟着学完了的,本文是个人笔记记录和好课分享,不卖课不推销不打广告,讲的好不好需要自行辨别,不用跟我抬杠争论技术高低,也不要联系我索要老师后面的付费内容。成年人应该学会独立思考,有自己的判断,为自己的行为负责,不要看了别人买课记笔记就盲目跟着。
——————————————————————————————————————————————
老师没有发配套的素材,我使用的练习素材来自网络平台购买,人物模型我用了原神里面喜欢的角色散兵
模型获取请去模之屋等官方渠道,私人不会提供。
借物表以及其他版权声明:
散兵:Mihoyo/spring
倾奇者:MHY\TDA\WYKP\桥伊\萧容与\FixEll
Edit&Physical:FixEll
流浪者:
◆模型提供:miHoYo
◆模型改造:观海
截图里面偶尔出现的流浪者桌宠:
【流浪者桌宠】可喂食可调戏,桌面自走bb机~
原作者链接
《有问题自己解决不了马上汇报,一般领导发脾气不是因为不会,而是因为不会还不说,“不怕你不会,就怕你不说“》
闪屏一到两秒《打广告》,版权所属,防沉迷公告,热更新,然后重启,可以自动重启,也可以弹窗让玩家重启,然后才是登录(跟原神的流程差不多)
《要选第三个,否则会坑死人的……默认是第一个》第一个就是Windows行尾转Linux行尾,第三个是提交什么上传什么
麦扣老师也提到,先Scale With Screen Size,然后做UI
新建一个空物体做底,要和屏幕一样大小
每个代码开头注释干什么的,每个变量也要注释
其实正常来说需要个loading 界面的 然后需要预加载到1才切换界面 也就是等待返回时间 有数据返回了才做判断切场景或者画面
UI“飞了”:看看Uisystem是不是没有坐标(0,0,0)
使用ScrollView+Toggle,content上挂Toggle Group做角色选择,只能选一个,选择的高亮
每一个Toggle都属于一个Toggle Group,这样才能实现单选,并且每个Toggle也应该拉成一个预制体
其实不仅仅选人界面可以这样,所有限制单选一个的,比如说纪行活跃宝箱,武器啥的的都可以用类似的方法实现。
商业项目不允许出现拖拽 界面多了没法维护 而且容易出现脚本丢失,不要拖拽,如果觉得麻烦,可以写一个递归的find方法← 实际中用Find还是挺多的
老师使用了一个闭包的方法,说如果不按他那样写,每次都选择最后一个,查了一下大概是这么回事:
内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
这个是选择角色,Toggle多选一,那类比武器、奖励多选一,逻辑是一样的
Unity 3D之UI设置父子关系setParent坑:
对于初学者往往会遇到UI加载出来,设置子物体后发现UI不见了,但是明明直接拖到UI层次中又是对的。这个问题就和第二个参数相关。
教程参考帖
省流:UI在制作的时候是父子局部坐标,实例化到场景里,局部坐标转世界坐标,然后希望维持之前的父子关系,因此要选false
格式化输出:
发现Toggle重叠到一起了,是没用Grid Layout Group
限制只能纵向拖拽
运行模式调整Copy Component,退出后pastr as Value
我这很奇怪,设置了LastIndex来记录上次选择的Toggle,但是每次还是只有第一个isOn,并且没有变色,百度一下以后找到了这个教程:
Unity 关于Toggle的ison默认没有显示监听结果的解决:
教程参考帖
这个代码是把toggle装在一个Toggle数组里,跟我之前的用法差不多,如果结合老师的实例化UI的话,那这Toggle数组赋值必须写在实例化toggle以后
Group、toggle都可以用transform.Find通过名字获取,遍历toggle数组来逐个关闭isOn,最后把需要显示的默认值单独设置一下isOn
单例模式有三种写法:
1. 懒人单例
一个构造函数加一个实例
2. 标准单例
Singleton这个类名字可以换成其他任意名字,比如XXManager,按照这个结构写代码的话,这个类的实例就只有这里定义的static一个,给全局用
3. 泛型单例(较常用)
跟麦扣讲的差不多
上面这个写法有一点小问题,有时候没创建实例他也不会判断
一般继承着用,
泛型是对类型的抽象。核心是用instance,至于类型,无所谓
让UserData继承一个UserData类的单例,免得每次都写那一套东西,构造函数不能是private的(必须具有公共的无参构造函数才能使用),因为要在父类里面new一个子类,不然根本找不到东西
继承了泛型单例模版类相当于帮你创建一个传入类型的唯一对象实例(调用你的公有构造方法来实现),你要是私有构造方法,模版就无法帮你创建实例
没有设计模式基础的同学建议先学一下设计模式,推荐《大话设计模式》,程杰著,2007年出版,但是2021了,仍然不会落后
游戏常用设计模式,单例模式,中介者模式,观察者模式,工厂模式等等
(Singleton里面用反射去做 UserData的构造函数就能是私有的 可以去网上找找单例的常见写法)
扩展可以写没有的全新功能,也可以把之前的现有功能进行一些封装
反正方法写了三次就提出一个公共方法准没错,懒人准则
开闭原则,开放扩展,封闭修改
很奇怪的是,老师的GameMgr没有挂空物体也可以照样运行,我的就必须挂在SetUp空物体上才行,不然报Nullreference,有可能是继承的单例模板不一样的原因,我沿用的麦扣的模板,没有用老师的单例
放ModelPlace的目的:不直接改模型的旋转,否则所有模型都反了
这个场景应该开灯,有Light,因为要展示模型
创建3DObject的Quad,如果不显示:尝试旋转一下,然后放大往后拉当背景
美术会在这个场景的基础上放一些花花草草、台子之类的东西
除了Quad以外,Panel、2D的sprite应该也可以,2D的sprite要放图片才能看见
这个界面会做的比较炫酷,因为不涉及性能问题,模型用高模+炫酷技能
一次加载多个是因为没有把之前的清除掉
每个都要给路径,不然会报空
销毁所有子物体
Foreach的性能没有For好,foreach里操作原容器里的数据容易出问题
封装的意义:逻辑不变,只改底层实现
跟麦扣一样,默认都设-1而不是0
旋转:
一个Image划定滑动范围,改名TouchRotate,然后设为透明
这个界面可以用于选星球UI,选人、选星球、选礼包都是一个道理
把OnDrag发生的事情写在SelectRole脚本里面,并且修复了选人不加载上次所选项的问题(把预制体里面的isOn关了)
用Quaternion存储模型生成点初始角度,每次生成之前还原,让模型永远面朝玩家
静态数据
逗号分隔字段,回车分割条目
CSV可以用Excel直接打开,简单方便
XML结点式结构,类似HTML,冗余数据较多,读取数据较慢,已经有点过时
JSON 数据格式 - SkySoot - 博客园:(老师推荐的教程链接)
教程帖子链接
电脑看明白了,人看不明白,实现了持久化存储在硬盘上,jso与对象互换很方便
编码体系,美国的ASCII,我国的GB2312国标码,收集世界所有语言的统一革命性的Unicode(避免乱码:编码和解码的语言不一样)
UTF-8:把32位的Unicode【尽量】映射到8位(变长,不一定是8位)
配置文件放Config(放Assets文件下面也可以,老师觉得生成.meta文件很烦,所以放外面了)——后需要读,果然还是放在了Resources下面
角色表先编ID(唯一标识)
Name要和创建角色的自定义名字区分
txt可以,但是不明显,不能区分是文件还是表格
一路确定,然后反手把占空间的Excel删了
表格数据随用随取——单例模式,只有一份
用Dictionary而不用List:ID不一定连续
CSV默认是GB2312格式,Unity不认,只能用二进制方式来自己解析这个格式——扩展名必须加一个.bytes,需要自己重命名一下
注意这个时候要把.csv的后缀留着
按Ctrl不松手,连按两次R 重命名
表里面加字段,这里对应添加就好了
一般是程序加载的时候统一读表,而不是第一次使用的时候读表
**
遇到报空,时刻记得挂脚本,挂脚本!!! 这个单例和麦扣的单例不一样,这个泛型单例是给数据类的,麦扣那个是给继承Monobehaviour的挂物体上的脚本的,不能复用,可以把麦扣那个类改成MonoSingleton
**
更换继承类的话,要把所有BUG修完了才会起效,不然脚本还是挂不上去
先找相同的,再找不同的
//相同之处:
//数据存储方式——
//索引方式——索引器,[index]访问
//表的读取方式
//不同之处:
//数据类型——抽象为泛型TDatabase类
表数据结构基类TableDatabase(NpcDatabase、RoleDatabase的父类)传入泛型以后统一叫TDatabase
//文件路径——传参,传哪个读哪个
教程参考帖
这个泛型不是随便哪个类都可以使用的,必须要包含两个条件,第一是TableDatabase这样一个类(子类也是父类),第二必须能够new
除了ID以外不一样的部分用数组传进去,然后各自类各自解析
又要继承父类又要单例——把单例放入父类去 Singleton
(老师这样写应该是等效的,但是略微麻烦)
类似Lua
快速提取方法——选中所需代码——快速操作和重构——提取方法
表里面的字段名字要反射程序里面的字段名,所以csv和TDatabase的属性名字要相同。大小写如果不一致,找到的就是null,而且这个null也会被Add进去,最后就报空
果然,老师刚讲完我就踩到坑里了,TestIDList的L没有大写
编辑——高级——查看空白
这个不是一个数据,1,2,3,4是一个列表里面四个数据,*而且不能用逗号分隔(字段分隔符),句号、空格也不行,容易自掘坟墓,写密码很常用,也不要拿来分隔
老师建议的用一个不常用的$符号。
突发奇想:既然角色数据这么复杂的结构类型都可以读,那对话系统里面的对话只有string,不是也可以读入以后放入列表嘛
老师:《操作目录Resources不能让策划和美术乱动》
每次Excel文件修改了,csv还要更新一次,很不方便
于是写个工具,Excel一键导入到Config目录供程序使用,并且自动加上后缀名
打包出来的才是UnityEngine,编辑页面都是UnityEditor
给他一个菜单目录,点击按钮调用函数
也可以放在系统已有的目录下面,但是不建议
用于编辑的Config和Assets同级,写的这个工具用来放到Resources目录下面的Config里面
返回指定目录下的所有文件名
强制读条
开发周期直接读原表,打包以后去Resources,不然策划或者美术改表以后没有调用工具的话可能有一堆bug
区分开发期和运行期
打包发布之前运行一次ConfigToResources工具就行了
自定义标记写在上面的截图哪个位置,这样使用的时候,如果定义了就是真,没定义就是假
GetInstance里面的Resource.Load也要更换
服务器架有防火墙,不然很容易被黑掉
GateServer逻辑核心
LoginServer登录核心,负责处理账号、注册、审核
SelectServer UDP获得最空闲的登录服务器,所有客户端先跟这个服务器连接
什么都不做,实现负载均衡(决定了同服能有多少玩家一起玩),而且不是所有玩家连了服务器就要真玩游戏的
五层(还有不常用的七层、九层网络架构):
客户端和服务器的连接:调接口发消息,至于消息发给谁,客户端写逻辑的不需要关心,一般是消息本身决定的
对逻辑层来说,具体怎么传输、发消息做网络,不管,只需要在客户端上面有个逻辑上的服务器,所有和服务器的操作都去找这个模块,相当于服务器就在客户端
服务器也是一样的,连接服务器的每一个客户端都有一个对应的对象去管理他,就相当于服务器可以访问到每一个客户端
Scripts是存放客户端的脚本,服务器存放在和Script同级的Server文件夹里面
发消息:组织一条消息,然后send出去
对于我们这个模拟逻辑来说,客户端和服务器一一对应,一个客户端只对应一个服务器
注意UI要选择Legency,不要选成TextMeshPro了
Unity 之 UGUI TextMeshPro控件详解
教程参考帖
这里直接把事件作为函数参数传入,成功了会触发一个成功的方法,失败了会触发失败的方法,可借鉴
只是不知道他怎么判断成功和失败的,如果不是这里这种必定成功的情况,if里面应该写什么呢?
用消息解析器来分发消息
字典绑定消息类型(key)和处理函数方法(value),遇到哪个类型的消息就用哪个函数处理
如果所有的消息都放在Receive这里处理,不知道代码得写多长
这样可以把每一个消息的解析放在各自的功能里面做,战斗消息放战斗模块,跳场景消息放场景管理模块做,更多的是诸如背包系统这样的系统消息
连接服务器成功以后,要自动发登录消息
登录以后,要验证账号密码,并且自动寻找玩家存档,找到以后向玩家发送已创建的角色列表,这里老师把之前客户端作假的数据给到服务器了,但这样来说,客户端的数据就是真实的了
注意这里要先new再保存,不能换顺序实际上这个消息文件会放很多份,让客户端和服务端都能看见
但是这样是浅拷贝,其实这俩列表索引的都是一个地方的值
真正模拟客户端和服务器的通信要用深拷贝,这俩数据除了值一样以外没有任何关系ModelResPath不应该存在服务器,服务器不用知道客户端的路径组织关系,不用知道客户端把模型放在哪,只要告诉客户端你这个角色对应哪个模型就行了(这个就是配置表干的事情)
服务器不管你存在哪,只负责告诉你用第几号模型
每个消息解析函数都要判定消息类型
用数组下标来表示选中了哪个角色,因为可能有多个重复的角色
只能定义Write,不能定义aa
分配ThisID:
数据量可能很大,一上线怪物全刷新出来了
创建角色管理类RoleMgr
管理类一般不是单例就是单例的内部,比如几个管理器打包出现,一个Manager在另一个Manager里面,但是外面的Manager是全局唯一的
无意中搜到的一个貌似会踩坑的小点
教程参考帖
这地图好像赤王沙漠+丘丘人营地的组合
从SetUp(Tool-GoToSetUp记得用)——Login——SelectRole——1001场景
做一个提现木偶一样的角色控制类,操作角色胳膊手一系列的动作,RoleManager管理场景里面所有的角色类的脚本
本质上消息就是传递一个个消息类的实例化对象,实例化的时候赋值属性需要传递的信息
可以用AddComponent给新实例化的对象手动挂脚本
注意,储存所有角色用的不是ID而是ThisID
这个消息本身就是角色的属性信息
服务器和表格的数据存在角色里面
类似于多态,同样一个Role,一个生成主角的对象,一个生成配角对象,但都能调用Init函数
创建了角色,但是场景内看不见:
这一帧执行了加载场景和创建角色的命令,但是这一帧结束以后才切场景,模型就被生成到了老场景里面
创建以后加上DontDesroyOnLoad,这样模型就不会因为加载场景被销毁了(当然这么写在这不对)
(这场景好像龙脊雪山……)
服务器发送进场景消息,客户端加载的慢,服务器已经认为客户端进了场景,开始生成配角等
解决办法:告诉消息接收器,收到的消息先别急着处理,场景加载完了再处理
使用异步加载场景
Update或者协程,但这个类不是Monobehaviour类——写一个专门的QuickCoroutine类快速创建协程
创建一个空的GameObject,一直存在,不让销毁,然后在这上面挂脚本跑协程,相当于这个是协程管理器
在GameMgr游戏初始化里面做协程初始化
加载场景的时候会一起做很多事情,比如对象池中对象的缓存、常用的数据读到内存里,很多预加载的东西都是要场景加载的时候一起做的
希望缓存消息,有需要的时候再放出去——所有消息都缓存,后面是否解析和阻塞变量有关系
角色加载进来过大:1.扩大地形,2.模型缩小
扩大地形(注意看场景也做成了大预制体)
雾效果不对,老师直接关了
放个Cube——reset,找(0,0,0)
Cube定位(0,0,0)成功,然后拖拽场景预制体1001,直到Cube露出来为止
然后删了Cube保存场景(这么野的么??会不会导致Terrain的碰撞体或者Nav出错?)
不让直接挂MonoBehaviour,那就创建一个继承自MonoBehaviour类的空脚本当工具人PS:GoToSetUp这个编辑器脚本最好每次都手敲一份,不要随便复制粘贴,会出找不到场景文件的BUG
所有的关系型数据库都支持SQL
LIKE一般用于字符串搜索
每个字段对应的值依次写到括号里面,也可以只插入某一些列的数据,这个时候就要把列名列出来,前面的列名和后面的值同样需要一一对应。不写列名的话,后面的值就和表里面的定义一一对应,写了列名,值的顺序就和列名一一对应。
也就是说,写列名的顺序和表里定义的顺序可以不一样‘
可以先FirstName再LastName
如果不加where限定,那整个表都换了
DELETE FROM table_name 表清空,但是表还存在
Delete 表名,不加where,整个数据库都给删了
一般在商业用法中,禁用Delete语句,而是使用一个标记位0/1来判断这条语句是否被删除
真正的删除:确认表格没问题以后,执行一条非SQL的存储过程语句,一个SQL语句的函数,清除掉表里面所有无用的数据。包括Update也是由存储过程来做。
存储过程:理解成SQL里面的函数,一个写好的没问题的函数来执行这些危险性比较高的操作
DESC降序,ASC升序
根据版本下载动态库和工具
新建一个文件夹,把动态库和工具里面的东西放在一起
点击exe即可打开
注意命令前面是”.”不是“_”
SQLite3基本使用教程:(可以跟着录屏操作手把手做)
教程参考帖
存字符串:char<长度>
后期的数据库做了改进,使用varchar,相当于字符串
这样也行
标准SQL语句都加分号
出现三个省略号:以为命令未完用的英文的(),不是<>,老师的电脑看着是<>
控制台语句,按上方向键再打一遍
其实用单引号双引号都可以(一般单引号引字符,双引号引字符串)
单个字符和字符串区别:字符串多个/0
不等号是一对尖括号<>,!=也可以
整个删除表:drop
界面工具:
动态库是SQLite的核心
SQLite支持Lua
SQLite常用数据类型
Not null是否要求这个列必须不能空
定义的时候没有给字段值,就用这个默认值
primaryKey 主键 这个字段的唯一标识
自动生成,隐藏,自增主键(添加一条,加1,添加一条,加1)
注意这个自增变量中间没有空格,老师的教程里面很长一段时间写错了
关键字推荐大写,但是小写也不会错。左侧的所有操作,都可以用SQL语句来完成
主键:强制不能为空,而且不能重复
想用默认值的话要写字段名
命令行最好写一句删一句
可以用UNIQUE来约束非主键,使其也不能重复
身份证号码、电话号码都是字符串
删除列:
好难用哦,艹
商用MySQL
Count自增
不存在:第一次运行,创建表
点击TableData会自动出现查询语句
自增变量赋值了就不增加了,不管他才自增
请教了一位认识的大佬不管他就得指定列名赋值
还是用mysql吧 sqlite问题一大堆 非主流
弹幕说应该只有主键才可以自增,老师这里试图让字符串自增报错了 (不是主键的改成int也不行)
还必须写成integer,不然还得错(弹幕说Int也可以)
create table Student(id integer primary key autoincrement,name vchar,grade int default(1),idnum unique);
Int会自动去找亲缘类型integer,其实里面并没有int
遇到这种问题就直接按照报错提示写SQLite里面支持的数据类型就行了
老师用的参考教材:
教程参考帖
数据不重复:Distinct
老师的练习题库&答案:列出-至少有一个雇员的所有部门
教程参考帖
日期越小,时间越早
在Unity安装目录下找到SQLite的库,不同版本不通用,放到Assets Plugins目录下
找起来确实比较费劲
这个SQLite3.dll的动态库是SQLite的核心,在SQLite的安装目录那里
.def后缀的文件建议一起复制过来
老师后来更正了,System.data.dll不用复制
unity2019自动集成了system.data.dll
这个文件和编译器有关,不能自己取
应该指向VS安装路径里面的一个动态库,复制过来Unity以后刷新一下资源连接数据库就相当于是打开一个数据库
New一个连接,然后取到连接里面的Command
Insert、update这些不需要返回值
截图老半天,终于截到了!!
抽象数据库接口的目的:可能写着写着要换数据库数据库文件路径,也可以用绝对路径写System.Data还是需要用Unity提供的
真离谱,最后我找这个地方的Mono.Data.SQLite文件不报错了ServerDB也找到了这里的角色信息应该是完全的,角色列表里面展示的信息只是角色信息的一部分
全集,SelectRoleInfo只是其中一个子集
使用var的好处:即使后面改了类型,只要用法不变,就可以不改代码
希望使用一个树形结构,把玩家的数据直接塞进数据库里面
需要使用一个字符串来存储玩家数据
也就是说,玩家类需要能够序列化,转化为字符串永久存储起来,同时还要能反序列化成为内存对象
使用一个插件【转】在Unity中读写文件数据:LitJSON快速教程(这个Dll导入以后貌似不太行)
教程参考帖
大部分其他教程和老师的操作一样,把代码包直接拖进去
Unity JSON编码解码之LitJson 深度剖析
教程参考贴LitJSON/litjson
链接
别说,Github上面整个包塞进去确实是不报错了
【Unity数据持久化_Json】(四)LitJson是什么?使用LitJson序列化、反序列化
教程参考帖
JSON查看工具
发现写漏了这一句返回的都是Object类型,至于怎么解析就是逻辑的事情了这样写更好,基本类型转Object是装箱,Object转基本类型是拆箱重要的类 - MonoBehaviour - Unity 手册
教程参考帖
这里的Player不能继承Monobehaviour
脚本框架:SetUp什么也不做,然后进入逻辑层,除了Logic目录,剩下的都可以作为通用游戏脚本
适用于Unity里面所有的对象,不仅仅是GameObject,脚本也可以,设置以后父节点也会跟着DontDestory
Lua的初始化和循环可以在GameEngine脚本里面完成
在Lua里面就可以用C#的主循环了任何一个脚本调用这个都可以起协程
定时触发一个东西的功能一般不用协程来做
间隔、次数、回调方法
Unity下必须要遵循Update驱动定时器运作的原则
在GameEngine里调用TimeMgr的Loop函数,达到不继承Mono也可以得到Update的时机
创建一个Timer定时器,然后加入定时器管理类TimeMgr的定时器列表里面,通过Loop循环去驱动每一个定时器让其跑起来,如果不想跑了,就从列表里面移除
但其实实现上不能用List来写(如果一个计时器需要停掉另一个计时器的话,这样写就有缺陷),而要用Action写
这次的改值会在本次执行完以后生效,函数A会删除函数B,但是是本次一轮执行完以后才发生,本轮之内函数B还在
(如果想要函数A删除函数B以后马上生效不使用函数B,委托是实现不了的)
避免卡帧:逻辑类,按道理不应该有引擎的东西,拿到别的引擎甚至服务器上应该都能计时才对
老师的目录结构
无限循环,比如体力一直在恢复:(>0计数,<0或者=0无限)用这个isRunning记录状态不值得提倡,应该想办法用其他东西判断出isRunning的状态一定要尽量减少给IsRunning标记赋值的位置,能不用就不用
这样这个isRunning只有Start和Pause这里改变值,避免多个地方改值浮点数比大小,错误写法,不能用等号来判断。需要写一个所有程序通用的浮点数判断
浮点数无限位数,但是内存有限,只能显示出有限位数的小数点
游戏引擎的数学函数是跨平台的,但是微软的不一定是跨平台的倒腾出来了攻击冷却计时和刷新倒计时
不过怎么做到玩家离线了还可以继续计时呢
没想到老师还是用的NavMesh和鼠标点击人物来做的,我还是喜欢原神那种WASD的方式,更加自由,不像Nav那么容易卡
动画状态机也很特别,AnyState出发,全是MotionType决定动画切换,程序里面的枚举可以直接切换成动作
ReSpawn动画(没有的话可以死亡动画倒放)完成后默认切Idel
Unity Animator 倒播动画 方式2种(2020版Unity-亲测)
教程参考帖这种canTransitionToSelf一定不能勾选
动画状态机的编号并不连续,预留一些空间用来加减
保持完全一致
到目标点停不下来:中心点问题,《2.5D游戏没有Y值》,要把三维距离降二维
NavMesh用单独AddComponent的方法,因为不同角色的属性可能需要单独配表
有的项目是transform实时检测y值,不知道是怎么做到的
《只要玩家能走,直接跳到高地上》,不同方式之间没有好不好,只有合适不合适
转向反应慢:加速度太慢,直接设为最大值目标点并不在NavMesh上也能跑,会自动寻找一个在NavMesh上面的最近点
但是检测有默认为10的范围值,过了就找不到了(高于10还在跑可能是因为地形本身有高度)
老师用Init函数,抛弃了Awake和Start,因为几个脚本执行起来《乱七八糟》,说得对
Const本身就有static的意思角速度和加速度调最大,实现状态瞬间转换,免得转向不流畅,还跑得时快时慢
战斗UI老师大致分了五层:
Scene:场景里面的东西,由于技术上要放到UI里面(比如血条),而且是最接近场景的层
Touch:寻路,选中目标(比如NPC)和场景交互
Fight:技能,功能大厅,主角小头像,小地图,在屏幕的四个角,不会互相影响
Normal:背包,角色,系统界面
Top:公告信息
层次不够再往上加
删除UI有可能不是真的删除,可能是隐藏,收入对象池
GameMgr初始化的时候,把所有的架构初始化这个方法路径前面没有斜杠,但是Resources.Load那个方法路径前面就有斜杠
而且这俩方法都没有后缀名
因为发消息导致逻辑中断:查找所有引用调出UserData脚本
注意脚本调用的顺序很重要《场景加载完了并不是真的加载完了,还有乱七八糟其他事情》
FightUIMgr类:管理所有UI,包括摇杆,技能按钮,小地图等摇杆里面inner这个小球仅作展示,不接受触摸事件
UI上面的Transform是RectTransform
注意这里不要把有没有下划线的写反了,不然会造成堆栈溢出,报错StackOverFlow
主角应该从FightUIManager里面获取摇杆事件
摄像机的正前方一般是朝z轴
老师没有用Update,用的计时器
用一个校验值S来控制角色移动的快慢,S值小了角色就会慢速移动整理代码后:摇杆传值,计算出目标,让角色向目标走过去。摇杆停止,停止移动。
TouchScene要实例化在UILayer.Touch层
跟JoyStick一样,先实例化出自己的UI,然后获取组件
Event一般是用+=,一个人用(没听懂),用了委托就不要用+=,如果要用+=就明确声明事件标识一下。多播和一对一要分清楚
这两种相机发射线的方式是等价的
地面加Mesh Collider(性能消耗较大,大多作为静态来用)
(老师的架构和解耦能力真的超强,很容易就做到了报错少,而且没有什么东拉西扯,Inspector界面也很简略,很多思路是我以前从未想到过的,就跟开了一扇窗一样。)
如果点击发现角色没有走到目标位置,可能是目标位置那个东西没有碰撞体,射线没检测到。
不需要检测的层按位取反
这一讲会解决点击地面到达目的地以后播放动画不停的问题
targetPoint加问号表示“可空“,这样就可以赋值null
可空值取值要取Value
NPC的管理方式仿照主角
服务器知道角色坐标,知道走到哪个范围可以跳场景,进入范围内服务器可以“拉着角色走“,但是一般不这么做,因为服务器要管很多人的位置。每次角色移动都检查地图上的跳转点性能消耗是比较大的。
客户端触发跳转,向服务器发消息,我要跳转,服务器验证在不在范围内,以此分流服务器压力
EnterCollider,检测撞进来这个人是不是主角,是就发跳场景请求。
每个场景都有自己的跳出点,所以跳出点就做到场景里面
跳出点之间不能离太近,否则刚进入就跳出去了
LayerMask.NameToLayer有性能消耗,如果频繁使用要存在GameSetting里面调顺序,先生成主角,然后进入场景
重复进入1号地图以后:角色只有编号为1的自己,但是NPC每次都在生成新的,就会有一大堆。
如果有2-3个角色,一跳地图就错了。所以跳地图之前要把之前保存的和场景有关的东西全部清掉。
第一次进场景和跳转场景的时候,对于客户端来说操作不一样。
role.Value.gameObject回收脚本所挂的对象,不仅仅是回收脚本
重置整个UISystem
现在不用管重置Layer,因为肯定传MainRoleLayer,如果后续有扩展,不止排除MainRoleLayer,那时再判断。重置为负的,Layer不能是负的,表示排除层无效
先禁用,场景加载完毕再开启,比如加载进度条的时候不响应任何玩家的触摸事件
跳场景出错可能是这里少写东西了,报错问题也都添加在这解决FIXME:我这遇到了很奇怪的问题,摇杆重复生成了俩,而且不起效果了
Text字拉着拉着就没了:允许横向、纵向越界Overflow,这样字体再大也不会消失了老师的Unity和VS都卡住了也用的SublineText这个编辑器,之前MMD导入以后报错该数字也是用这个,看来这个小众编辑器真的很好使
把所有需要root加载的路径封装成一个抽象类Dialog,用DialogManager管理
路径错误要返回空,因为像按钮这样的东西不是每个界面都有
像ModleStudio这样的部分,尽量重构而不是复制粘贴
Raw Image比起Image多个texture属性,可以直接设置
老师给NPC加的CapsuleCollider碰撞体
地面,包括地上这个圆盘,老师用的MeshCollider
这个半圆形的类天空盒不算MeshCollider,这是2.5D场景,不画天,只要不穿帮就行能获取到Npc脚本就是NPC表里面的是静态数据,服务器里面的是动态数据,要覆盖客户端
如果服务器里面没有那再用客户端的
很方便的新名词:闭包这里因为距离判定的问题,点地移动以后动画总是不停止,老师反反复复改了很久
targetPoint和purpose合二为一,所有的都认为是有目标的行为,区别就是目标是否为空,不论是点地还是摇杆移动都走purpose流程
有目标就往前走,如果刚开始就在目标点,达到了触发目标事件的条件,那就直接触发。如果达不到,那就把目标点记忆起来,然后寻路。到达目标点以后,执行后续动作,同时清空目标
如果Text、Image这种UI只是作为展示,没有触摸、按键之类的内容的话,需要把他的Interactive取消掉、勾选
FightUIMgr:界面与实现分离,一个Mgr里面管了很多个UI
逻辑全在这里写,或者通过Mgr导给别人,每一个UI上只处理和UI相关的事情
数据都通过委托导出去
FightUIMgr用的设计模式主要是外包模式,把东西变个样子展示出来——所有UI封装到Manager里面交互,然后Manager里面分配任务
外层只告诉内层需要怎么做,但是具体怎么实现不用管
效果如图滑动摇杆的时候因为一直处于选中状态,所以UI会一直出现
可以设置远离一段距离以后消失弹幕大佬建议:这里可以把传NULL改为隐藏就行了
新加入脚本TouchOutsideExtern《要在所有子节点都添加之后再添加此脚本》:要先动态生成按钮,然后再加这个脚本还可以这样一边加脚本一边存为变量,666注意这怎么给outSide存储了,然后给他的事件赋值了一个函数
这里要用UIManager来加载和移除,否则用ResourceMgr会导致UI移位
状态的表现和驱动
Npc里面的这些状态有自己的一套实现,基础的结构不需要关心这些东西
具体的实现和状态无关,和AI的类型有关
Idle状态就管距离判断,能不能打。不能打的时候其他状态都走Idle跳转
以前的:
现在的,全部归到一个状态里面做判断,也是行为树的思路,Idle担当判断节点的角色
连结状态的时候要一个状态出发连完
野怪基本思路:
我很少见有讲技能系统的课,还是第一次接触这个,所以这个课程我觉得很难得
游戏的核心,没有之一冻结:不能移动,不能使用物品,不能播动画,不能攻击……
四封:物理攻击,法术攻击,特技,物品使用
架构的目的:更方便地应用于需求更改,任何情况下都很简单的架构是不存在的,公认需求难花长时间实现,公认的需求简单花短时间实现就是好架构
《与任何一个模块交互都是正常的》有AOE就行了,只是个数12345的区别
攻击距离不可能为0,有远程就可了
施法方式还有个引导
最快,每帧结算一次,就是多次瞬间结算
一般要把MipMaps这个选项勾掉
所以要用一个Image把整体包起来,这也是为什么这个最外层的物体没有把技能按钮完全框住的原因,给玩家的操作做容错处理
Image透明度调成0,上面俩小按钮单独用一个Image遮挡
技能按钮要在父节点上方,冷却也是用的360度的FillAmount
对于程序来说,Skill12345完全一样
这里老师没有让BgImage、CountdownImage、CountdownLabel去响应触摸,会把按钮遮住这种“一套“的东西,老师都写成一个单独的类
名字不重要,注释很重要,名字描述不清楚的东西就由注释来描述清楚
一般不用结构体,因为结构体是值类型,函数传递的时候有复制对象的消耗写好初始化,Add,然后在中间填充
套路都是,MainRole里面调用FightUI单例,通过Action绑定MainRole里面的事件和UI——效果就是UI变化以后操作MainRole行为
委托就是注册机制,通过委托把UI和逻辑分离,以后看到注册、观察者等相关东西,第一反应就是委托
所有的角色都会放技能,那就写在Role里面
老师这里拖拽导入卡住了,直接用的ImportPackage
释放火球的简单测试方法:加刚体和力粒子系统出现拐角:因为这个粒子系统里面有一个坐标系统,从预制体所在的坐标移到了人物坐标,就会出现拖尾,解决办法:通过重载ResMgr里面的GetInstance函数,直接Initiate到人物脚下的位置
至于前摇、后摇,播动画的多少秒以后才能放特效,这个需要时间线来做
直接用刚体的不好追踪角色
给火球单独一个脚本,飞行方向是火球(玩家身上的发射点)和目标(敌人身上的受击点)的连线老师也是在火球上面加的碰撞体和OnTriggerEnter,很奇怪为什么他没有用粒子系统的OnParticleCollider老师的刚体和碰撞体(触发器)都是通过代码添加的
“谁申请,谁释放“,谁new出来的谁在哪释放
事件的调用目标、飞行速度、碰撞体(触发器)半径、命中回调
委托也可以作为参数传递初步完成代码(也许可以应用到我的项目里面):
升级版:Role里面写CastSkill(int index)函数,所有角色都可以放技能,主角只是调用CastSkill(index),每个角色身上也要有目标
给火球一个飞行距离限制,并且没有目标也可飞一段距离
这个脚本里面没有加碰撞体,应该是直接在预制体里面加的施法者里面有SkillMgr,SkillMgr里面有Role的owner
正向索引和反向索引
配置项其实全都要通过配置表来做
技能基本表、粒子特效表、飞行道具表、结算物表
技能应该是和行走、动作同级的一套逻辑
技能全流程:
如果到了技能结束的时候发现动作改变,被别的动作取代的话,就不执行恢复逻辑
Ctrl+M+O折叠全部
这样保存单例是不对的,这是个函数,单例最好不要保存
就简单用new初始化,不要调用其他函数
驱动从离你最近的MonoBehaviour驱动
属性内部也可以规定访问修饰符
Invoke的效果就是执行,跟直接执行一模一样如下等价,意思就是执行:
为什么不用-=事件?
时间线描述的是整个技能的流程,仅在角色初始化技能的时候执行一次,运行一遍和十遍技能不应该受改变,如果用了-=,只能放一次技能,放第二次的时候就没有-=了
玩家看的最多的是描述Describe
RichText富文本,使用<>可以改变样式(应该是之前Beavor Joe说的跟HTML是通用的?)
要使用的技能逻辑(配完以后用反射的方式读出来)
很多没有共性来抽象的技能是需要单独定制的,一个技能专门一个类,所以要写一个支持定制的技能框架Alt+Table切换
技能之间还有互斥和状态管理,把技能的释放用单独的类管理起来,技能不能连放,放技能的时候应该停下来
技能逻辑和Index的对应只能从SkillMgr里面来找
这里如果写错技能就直接结束了
停止玩家动作不该放在技能逻辑里面还需要一个僵直的系统,放技能的时候不响应其他摇杆、点地之类的控制事件,直到技能结束
Role里面if(IsCastingSkill){return;}
Creature类:SkillLogicBase动态部分:技能里面最核心的:时间线结算物的范围和粒子特效的范围不一定是一致的,不是每次都能放到一起
state状态管理是难点,比如放完技能要原地站一会
写在PurposeTo里面的目的:主角和配角一并控制,免得配角出现放技能一半跑路的情况Role关于技能的找SkillMgr,SkillMgr又找自己里面的SkillCaster
摇杆和点击移动是角色特有,不用提到父类,其他的目标寻路、放技能,包括Update,都要放入父类
有些角色和NPC不一样的数据要单独定义在消息类里面
这样就可以方便访问子类特有的东西
判断选中的Creature是不是Npc(判断方法居然是强制类型转换,6),是的话才访问
Ctrl+B 重新编译
体现多态:先找virtual
主角可以体现多态**(一个父类变量指向一个子类对象)子类是父类,父类不是子类
Creature的Init是虚函数(virtual),子类Role以及子类的子类MainRole的Init都是虚的
所以这里用父类变量调用初始化函数**,看着是Role的对象,实际上调用的是MainRole的东西,至于MainRole里面调不调用父类,自己说了算,也可以如图所示不调用(比如和父类需要做的东西完全不一样)
总结:什么是多态?
父类的一个变量(role)指向了子类的一个对象,然后用父类的变量调用了一下虚方法(一定要是虚方法!!),这个时候实际调用的是子方法。可能有多个子类,以实例化的子类为准。
调用的方式是统一的,但是指向不同对象的时候,调用的方法是不同的(即体现出不同的结果)
报错了半节课老师才发现的ModelPath(笑哭):Bug:角色能把NPC推走:NavMeshAgent碰撞优先级为None,不发生碰撞阻挡类型设置为不阻挡(笑死,这名字还和Nav面板不一样)解决角色放技能抖动、前后摇摆问题:要用这种2.5D游戏的LookAt方式,去掉y轴,先算出实际的三维坐标方向,然后使用LookAt的重载,看向世界坐标的一个点
不过老师后来发现这个算法有点小错误:提取方法:(奇怪,老师是怎么做到不写_caster运行还不报错的??)再看了一遍,老师是把这个LookAt函数封装到了Ceature类里面
然后狂按回车,啥都不用管按回车到头
复制这里的密钥(应该要的是公钥)
Gitee码云——修改资料——SSH公钥
本机不用再输入账号密码了
克隆填SSH地址即可已经在本地想改SSH:修改远端地址
改成git
更新(不能用下面这个,老师试了好几次都不行)
要用TortoiseGit更新(后来发现也不行……)本地建了个新东西,push上去了……
《一般空工程会有各种各样的问题,随便搞个文件提上去》
HTTP克隆,远端地址改成SSH以后不用输账号密码了
为什么要生成飞行道具?是要伤害目标角色。能放出挥砍这个技能,说明离目标距离肯定是够的,足够近。到时间不是放火球,而是进行伤害结算。而且这个伤害结算根本不需要物理碰撞,直接对目标进行结算。
放技能SkillCaster,初始化SkillMgr
不允许类型不明的技能出现,而且父类访问修饰符尽量用protected,不要用private父类里面的东西通过protected virtual对子类修改进行支持
GetType(),Object里面的函数,这里用来返回没有InitTime的类型调用四次是因为角色+Npc都要初始化技能
这里本来是NormalAttack重构完以后每段代码都需要测试一下,现在要测不同的技能只需要改生成的对象就行了
这就是多态,父类的引用指向子类对象,子类对象随意替换,想换什么换什么,虚函数自动调用子类对象
先从人物、角色身上单独技能列表找到技能ID,然后再去找技能表里面对应的字段
Creature里面访问权限改变之后,变量名也跟着调成大写技能出现报错也要往上加,因为用的是Add的方式,否则索引对不上如果需要读表获取静态数据,就要从这里做文章了:
之前是错误是反射字段有空值(应该是技能脚本和时间线的问题)字段为空的地方没有做容错处理:
对于空值的Skill表内容应该给一个默认值
反射:通过一个字符串,找到这个字符串对应的类型或者字段或者其他东西
字符串——程序结构的映射
装箱拆箱:值类型打包成Object(因为Object本身是引用)读到的这个类必须我们预先知道是SkillLogicBase的子类,不然报错
人身上放个粒子特效每次Load都是类名+前后+Load可以封装到父类的反射里面,注意要加上.csvCreature里给每个生命体设置一个中心点初始化挂载点,中心,手上,屁股上(骑坐骑)《不对,坐骑高度不一样,是人挂在坐骑上》整个模型组合在一起成为一个人,人和坐骑一起动,然后美术播一个骑的动作
跟我之前想的一样,是要把这个点找到,然后特效生成在这个点上面,但玩家和NPC可拥有的挂载点不一样,所以要用多态来做。
这有个粒子系统自带坐标的bug,每次都放歪,不知道老师怎么解决的
和特效节点同时生成一个带有碰撞体但是不显示Mesh的球
更准确地来说应该用胶囊体(防止跳跃高过球)
结算物身上也需要有脚本,什么时候碰撞、销毁
通过协程——用极短的时间间隔(一帧)模拟造成伤害的瞬间,关闭时返回上一帧碰到的对象
这样还删的太快了,一帧还没结束就销毁了
这一帧的update之前开始走协程,这一帧结束才Destory老师遇到了无法触发碰撞体的BUG,然后发现是没加刚体……碰撞和触发都得加刚体
所有的技能都不应该和Ground发生碰撞
这里结算物品是Default层级的,也可以给它单独一个层级
上一讲遗留一个棘手的问题:有时候点击并没有判断结算控制结算体的显示和隐藏是上方正常Update做的,物理碰撞是跟着下方FixedUpdate走的,如果2个Update之间没有执行FixedUpdate(这之间结算物创建了又销毁了),那就会出现没有触发结算的情况
需要让结算对象从创建到删除之间一定经历一个FixedUpdate——协程Yield Return WaitForFixedUpdate
不管等几个渲染帧,能等到下一次物理周期肯定能执行Trigger
AOE不需要施法范围,有没有敌人都可以放(可以炸出来身边的隐形单位)
(说实话我感觉攻不攻击和有没有敌人没关系,没事打着玩不也行吗)
读表错误:表里面有个TimeLine,字段和数值对不上
理论上来说,伤害应该服务器来算,如要对接服务器的话,直接在伤害管理里面替换就行了。把本地计算的技能伤害转成发消息到服务器
发出技能的一瞬间加了Buff——如果按加之前算伤害,就要存储角色属性的副本,如果按加之后算伤害就要直接加
数学公式能用Unity的就用Unity的
这个伤害计算方法倒是和麦扣是一样的,攻击力-防御力,最小扣1点血
表格里面的数据是只读的,绝对不能修改。
Cmd里面是角色属性消息,和服务器同步用的,角色当前的攻防血都该走ServerData
TableData里面是一些静态基础,初始化数据事件监听也用观察者模式,谁监听血量变化就把数值传出去,监听不能赋值,不然可能修改出错导致别的监听错误
这里老师把回调绑定在了主角身上,就发生了一直不调用的错误
《越是诡异的BUG,最后发现问题越傻》
Int参数改float,隐式向上转型调用的这里根本不用改动,整数可以传为float
注意老师这默认值给的-1
有Hide必然有Show
这一讲老师应该是发了代码包,然后让学员注释了很多内容,练习连接服务器
基础功能第一周就完了,剩下的全是项目里面的常用需求,需求先行 枚举这里也要强制类型转换
攻击目标也需要判断是否可以攻击:不是怪物的NPC?分阵营?分家族?
把自己和目标传给一个函数,让函数来帮忙判断
如果父类的攻击判断都没过,就return false不是功能性Npc的都可以攻击,能进到这个函数的肯定都是Npc,可以强转
逻辑改为了《是否能被打》《_owner的目标是否能被_owner攻击》
父类逻辑:只要活着就能被攻击NPC类逻辑:如果攻击者不是玩家角色Role,不行,如果是角色,那判断是不是功能性Npc,不是就能被打
Role角色逻辑:
判断父类,除了主角自己都能被打,这里默认功能性Npc不打主角,可以判断也可以不判断
发现AOE技能会误伤功能NPC——伤害结算的时候也要判断做技能施法者和被击者全都传进去
CastSkillAssist和SkillCaster完全一样,跟着SkillMgr走
存下来那个能算出最小距离的Npc
CastSkillAssist选中了应该走SelectMgr的流程,显示选择UI
MainRole是唯一的——SelectMgr也是唯一的,写成单例效果是一样的,只有MainRole会访问
底层和上层互不依赖,类定义接口,函数层面的抽象定义委托
封装委托
距离过远的话之间导航走过去,但是要实现辅助追踪目标——不能只追踪一个点,Npc跑了玩家也要跟着,而且追完以后不该马上放技能,还要判断距离
GameObjetcPool预制体用
模板加入资源管理器,所有的都靠这个模板实例化(和Resources.Load返回的有区别)Vector3是struct类型,不是一个class,struct值类型,class引用类型,struct不能为空,必须有一个值
想要为空的话加个问号使其为可空类型。Pos不为空执行前面,为空执行后面
等价为Lua里面这么写:
不是所有对象都可以进对象池的,所有被对象池管理的对象都应该挂一个Reset脚本,回收以后回到初始状态可以做成一个接口,让对象自己去实现
持续N秒——定时器,到时销毁
范围性半径R——Collider
每秒M点伤害——碰撞体开关一次
持续性的东西,有这一行——跟着人跑,没有这一行——不跟着人跑,都可以
可以模拟岩浆这样的环境伤害
结算物销毁的时候应该把计时器停掉
可能的BUG:两次检测之间离得太近,这次检测还没有关闭,下次检测又来了。
不过条件太苛刻了,一个渲染帧和一个物理帧之间的时间间隔还没有检测时间间隔长,需求有点变态,不过也有可能,因为这里的Timer是帧驱动的
要跟踪的是一个Creature,不是一个点
Update里面实时更新一下坐标
父类变量指向子类对象:多态父类没有Creature这个变量,这个Init函数专门用来给子类override
TargetPos是初始位置赋值的,检测距离就始终都是初始距离,就会造成追一半停下来
按照目前这个逻辑,主角不需要追踪,npc也不需要追踪,打完停下来就行
通过状态里面这个machine.XX跳转
State类长这样每个状态都有自己的操作对象,所以要写在基类里面StateAttack相当于是自动追击,最终要走到TryCastSkill放技能避免写错:状态机就是进来一次只做一遍,做完以后跳出去,进一次状态机至多只做一次(可以不做)这个事情
NPCAI驱动状态机,状态机驱动状态,肯定要循环
Creature作为父类,Update也可以写成virtual override
状态比较多的一般都有个-1状态和动作直接绑定
Creature里有一个变量来专门记录自己的状态**(打死都不要开放出去)**
状态管理是程序的一大难点,放出去随意修改很容易G
不控制角色动画,而是控制角色状态,从状态改变来控制动画播放
SetAnim这个函数只在SetState里面出现
血量从非0变到0的过程才叫死亡
清空ai只适用于ai不需要有记录的情况下
死亡以后所有操作都要被禁止
这个活的判断方式可能以后还会修改
**《没死不代表就是活着》**所以一定要这么写死亡不能通过移动再改变状态,没死亡也不代表能移动(比如放技能也不能移动)
判断能否移动和能否放技能虽然长得一样,但是也不要作为同一个来使用。宁愿多判断一点,也不能让逻辑太混乱。跑到一半死了还飘尸滑翔到目标点:
动画跳转了,但是状态没有跳转——抖动确保当前是重生状态(很容易被打断)
出BUG的原因:辅助追踪的时候认为没有在施法
End:提前结束
Finish:人为控制/正常结束
技能里面的全找Manager如果摇杆的UI过小的话,可能会导致角色速度达不到预计速度
上坡速度是一样的,但是在2D世界里面判断的距离就不一样了。
一个带返回值的委托的简写:<>最后一个是返回值,其他的是参数
每一帧都要获取技能CD
考虑进没进CD再更新的写法很麻烦,而且容易出错,没必要
技能CD的时候不要禁用按钮,不然提示什么的也没法用
不成立的条件判断都应该放在底层,因为界面上很可能有其他的表现方式,不能,也不应该通过禁用界面来禁用某些行为
《没有地方放就放在根目录下》
CD的记录方式:记录什么时刻进入的CD
剩余时间=当前时间-进入CD时间=已经度过时间
最大CD时间-已经度过时间=CD剩余时间
不是过一秒减一秒
俩接口:1.有没有进入CD,2.界面上严格显示CD剩余时间
取剩余CD的时候,每取一次重新计算一次,这样避免了取CD/计算CD依赖于刷帧/Update
什么时候取就什么时候算,不取就不算
清空CD如下这样写也对
一些持续性的技能(比如暴风雪),可能释放结束以后才算CD
应该给NPC加一个Trace状态,攻击是攻击,追踪是追踪,不然CD的时候就傻站着不追了
【Unity3D技巧】一个简单的Unity-UI框架的实现:
真正重生是在服务器,重刷一遍血量