新火4注册后还新买的vivo手机被注册还可以用不?

https://blog.csdn.net/
https://static-blog.csdn.net/images/logo.gif
美好时光,海苔
https://blog.csdn.net/gohuge
https://blog.csdn.net/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
原则:制作方便,使用简单
逻辑管理、界面加载,显示,隐藏,关闭,根据标示获得相应界面实例、层级深度、分组、导航、动画、样式
资源管理、图集管理,界面元素合并,资源缓存
规范制定,颜色,字体,大小尺寸,命名等... ...
资源使用,制作工具
模板使用,样式使用
数据传递,组件的MVVM操控
加载,缓存,释放
富文本、Tips
提示框、对话框、
道具栏、分页栏、Collider 等
具体内容:
一、UI管理
1、定义UI的树结构,定义分组
不同UI环境不同,可能与场景共同销毁,也可能同一组一起隐藏;所以要进行分组管理。
UI的主要分组有:
主界面组,该组为应用的主控台,基本不会销毁,可能在剧情播放期间会隐藏。
功能界面组,该组可能会同功能场景的销毁而一同销毁。
通用界面组,该组为常用的提示框,对话框,等常规功能界面分组。
自定义分组,提供给应用的扩展组,特殊系统使用,比如实时活动界面、公告栏。
2、UI加载显示和释放
应用层通常只需调用open,close类似这样的函数打开关闭UI,但在管理器需要做一些封装;
首先是UI对象的构建,然后是加载对应的UI图集,通常很多应用的UI会做延迟销毁,所以每次打开
先去缓存找一圈是否存在。UI加载完成后要进行初始化处理,屏幕适配,分组设置,显示排序,等。
从用户体验层面考虑,用户打开过的窗口可能会保存操作记录,再次打开的时候
会进行历史记录的导航,导航到对应分页。
主要用于美术迭代,一款成熟的应用UI通常会更换几版,如果界面以样式表的方式记录界面样式。
这样的支持可以使美术调整界面的时候只改变样式表的信息,再进行检查一轮微调即可。
关于样式表的做法,以unity制作UI为例,在制作界面后通过工具将该perfer包含的所有gameobj进行样式分析,导出样式表xml。
在其他界面制作的时候,挂在样式表脚本并指定对应样式表,里面的元素将会安装规则去寻找样式进行填充。
并同时解决了打包中资源复制的情况,资源可以按样式表管理和加载。
二、UI制作
1、资源管理
UI制作的时候,特别重要的一点是资源管理,管理不合理便很难找到资源并复用,也很难达成资源复用的效果。
资源管理的冲突主要在分类和分组之间,分类是理解,分组是管理(分组不合理资源就会存在多个分组之中)。
即使很多是通用资源,那也可能存在通用资源分批加载和分阶段加载的情况。
资源分类大概为:初始化资源组、共享资源组、功能资源组(功能资源分的比较细)并且在等级段未开启对应功能
的时候也没有加载相关资源的必要(比如10级军团开启,10级之后的军团共享资源才会加载)。
2、制作工具
各种游戏引擎都提供了针对引擎控件的UI编辑器,但对于美术来说PS才是最好的。
所以也有一些公司采用PS直接生成界面文件来用的。
优点在于,优化了产出流程,“可能”提高了产出效率。
缺点在于,资源管理复杂,提供给美术的PS插件学习成本已经和对应引擎UI制作学习成本几乎一致
(排除大力气做控件到PS中的公司)做第一版UI用psd生成较快,但是后续调整却变得更慢。
三、UI使用
UI的时候基本上就是逻辑层的业务了,通常的情况是:
打开界面 -& 传入参数 -& 添加事件监听 -& 处理用户输入 -& 移除事件监听 -& 销毁。
虽然使用很容易,但还是要做几层基类封装,基类UI,提示类UI,对话类UI,便于可扩展。
UI模块的性能开销依然很高,也是我们统计中的重要性能杀手。在中低端设备上,超过80%的研发项目在UI端都面临较为严重的性能问题,主要体现在以下几个方面:
(1)同一时刻存在大量需要更新的Panel,比如,攻击时刻的血条、不断移动的伤害数字等HUD。这些在“人堆”中范围攻击时或者进攻高地时是很容易大面积出现的;
(2)不断滑动的摇杆和点击的技能icon,这些是MOBA战斗场景中使用最为频繁的UI元素,研发团队需要时刻注意这些icon的变动是否会带来较大范围内的重建。
以上情况都是MOBA项目研发团队每天面对的主要UI问题,可能一个Widget的使用疏忽,就能让研发团队的UI模块性能开销飙升一个量级。
我们的做法是,对富文本进行了纹理回收,比如场景不断产生的数字。
五、通用性
通用性会直接影响项目的开发便捷性和规范性。
有了通用,模板化的UI组件后,程序只需复用组件(业务不同只是换一个实现对象)。
就可以进行数据填充和操作了,同时大家对很多纤细的理解上也保持较高的统一。
所以一开始项目就要定义好哪些写基础组件,要求demo一旦完成就要立即实现。避免开发过程中有其他同事使用的时候卡主。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
这段时间遇到这样一个问题:采用Erlang自带的slave启动从节点后,从节点操作文件路径完全是master节点的。接下来,我需要解决这个,我的想法是从节点只有代码加载master节点,操作文件依然是本节点的。当然,也可以通过prim_file操作本地文件,file操作file_server指向的问题,但mnesia,lagger等组件都采用的是file操作文件,如果我采用上述方式启动则无法写入文件在本地。解题步骤:1、Erlang为什么采用slave:start之后,子节点的file路径就会选择为master节点;原因:-loader inet 我们修改了这个加载方式,但我们又需要该方式加载master代码。2、从节点加载master的Code又是怎么加载的子节点要加载远端节点的代码,通常做法是在启动项配置-loader inet host,表示加载对应host的代码;当然需要先启动erl_boot_server,并且添加子节点信任,erl_boot_server:add_slave。此时只要cookie相同,则子节点就会加载远端的代码3、启动子节点有哪些方式,通常如下a、子节点运行sh文件启动b、远端节点通过port直接启动,如slave模块,问题:通常自己写的sh文件,基本就包含 cookie,ssl,mnesia,-detached -noinput等,如果指填写这些参数slave节点只会加载代码,不会操作远端文件,那为什么slave模块启动的会出问题。必须采用prim_file才行,因为file的每次操作都是方位FILE_SERVER 进程执行的。那为什么slave执行的start会有问题,主要是这段代码!!!多了一个 “ -master “slave启动的时候是port根据rsh,直接向远端执行erl + 上述的cmd执行的。此时多了大家并不注意的 -master 参数,那么它又做了些什么,在file_server启动的时候 在验证是否存在-master ,启动文件进程的方式不同,而下面的启动方式则是造成子节点启动后文件操作的是master的原因。最后来检查,是否代码正常加载,切目录采用的是本地目录:
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
回顾一下,之前都遇到些什么问题:一、问题1、给对象添加NavMeshAgent组件后,单独设置对象的position总是不对!原因:后面发现要采用agent.Warp 来设置位置2、NavMeshAgent的移动速度偏慢,明明Speed调的很高了还是慢,且停止的时候刹车刹不住,还要往前面走一段距离!原因:acceleration加速度设置问题,加速度低了3、需要静止不动的场景对象如箱子,会被撞开或撞歪了方向!原因:通过Rigidbody-Freeze将Position的Y轴冻结,Rotation所有方向冻结4、再次选中地图通过Navigation生成路径,有些格子还是没有路径!原因:没有路径的格子没有勾选static选项5、场景点击地表不够寻路反应不够灵敏!原因:地表上还有其他元素,将他们的layer设置为IgnoreRaycast6、在有多个scene的情况下他们的Navigation会同时生效考虑:采用新版的NavMesh解决,相关使用方式和插件于7、设置NavMesh障碍物,可直接给物体增加Nav Mesh Obstacle 控件二、自动修路实现1、NavMeshSourceTag(标记地表元素)作为地图的可寻路介质,需要一个标记才能将其找出来,就行原始Navgation需要标记static;该脚本主要是将物体网格放在一个静态容器,物体销毁的时候也需要从该容器销毁;2、LocalNavMeshBuilder(修路建筑工人)建筑工人需要知道路要从哪点开始修,修多大,然后确定对应范围内的地表,并实时构建路径;由于建筑工在不停的修路,所以道路改变(搭桥)后路径也会立刻更新3、NavMeshLink(网格链接)业务中避免不了跳过水池,越过刀山火海,此时网格和另外的网格路径之间需要一个链接者Link;当然这些东西也需要一个地方那装,NavMeshPrefabInstance就负责将所有的链接网格放进去,进行处理。不过这个移动会有单独的AgentLinkMover 来执行。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
gzip 使用deflate算法进行压缩。通用类库为zlib,由LZ77+Huffman共同实现。1、 LZ77算法简介这一算法是由Jacob Ziv 和 Abraham Lempel 于 1977 年提出,所以命名为 LZ77,被一些人称为字典算法。2、 LZ77算法的压缩原理如果文件中有两块内容相同的话,那么只要知道前一块的位置和大小,我们就可以确定后一块的内容。所以我们可以用(两者之间的距离,相同内容的长度)这样一对信息,来替换后一块内容。由于(两者之间的距离,相同内容的长度)这一对信息的大小,小于被替换内容的大小,所以文件得到了压缩。LZ77算法使用"滑动窗口"的方法,来寻找文件中的相同部分,也就是匹配串(它是指一个任意字节的序列)。3、 Huffman编码原理我们把文件中一定位长的值看作是符号,比如把8位长的256种值,也就是字节的256种值看作是符号。根据这些符号在文件中出现的频率,对这些符号重新编码。对于出现次数非常多的用较少的位来表示,对于出现次数非常少的用较多的位来表示。这样一来文件的一些部分位数变少了而一部分位数变多了,但由于变小的部分&变大的部分,所以实现了压缩。4、Huffman树编码进行Huffman编码,首先要把整个文件读一遍,在读的过程中,统计每个符号的出现次数(1个字节的256种值看作是256种符号)。然后根据符号的出现次数建立Huffman树,通过Huffman树得到每个符号的新的编码,然后把文件中的每个字节替换成他们新的编码。建立Huffman树:父结点组成一个新的树,这个新的树的值为它的两个子树的值的和,等于父节点的值。Huffman树的所有父结点到它的左子结点的路径标记为0,右子结点的路径上标为1。看看网上提供的一个示例图,如下所示:a b c d e1 4 4 3 1通过最终的Huffman树,我们可以得到每个符号的Huffman编码。a 为 110、b 为 00、c 为 01、d 为 10、e 为 111另外:比如,a的编码为000,b的编码为0001,c的编码为1,那么当遇到0001时,就不知道0001代表ac,还是代表b。出现这种问题的原因是a的编码是b的编码的前缀。由于Huffman编码为根结点到叶子结点路径上的0和1的序列,而一个叶子结点的路径不可能是另一个叶子结点路径的前缀,所以一个Huffman编码不可能为另一个Huffman编码的前缀,这就保证了Huffman编码是可以区分的。5、使用Huffman编码进行压缩和解压缩为了在解压缩的时候,得到压缩时所使用的Huffman树,我们需要在压缩文件中,保存树的信息,也就是保存每个符号的出现次数的信息。压缩:读文件,统计每个符号的出现次数。根据每个符号的出现次数,建立Huffman树,得到每个符号的Huffman编码。将每个符号的出现次数的信息保存在压缩文件中,将文件中的每个符号替换成它的Huffman编码,并输出。解压缩:得到保存在压缩文件中的,每个符号的出现次数的信息。根据每个符号的出现次数,建立Huffman树,得到每个符号的Huffman编码。将压缩文件中的每个Huffman编码替换成它对应的符号,并输出。6、实现摘要deflate中的huffman编码:对Lz77得到的压缩后结果,需要统计字符生成编码表huffmantree(指示每个编码代表什么字符),根据码表对内容进行编码,具体的压缩大小在于精细分配结构体的位域来实现Huffman编码的压缩效果的。编码表huffmantree和编码后的data都一起放置在文件中。deflate中的解压:读取二进制文件,构建huffmantree表,读取数据根据huffmantree生成字符(这些字符是符合LZ77算法的)。用LZ77解码,这个时候应该需要对窗口生成哈希表(数组+链表);对解压的数据,进行搜索匹配拷贝替换为相应的串即可。gzip源码分析   main()中调用函数 treat_file()。   treat_file()中打开文件,调用函数 zip()。注意这里的 work的用法,这是一个函数指针。   zip()中输出gzip文件格式的头,调用 bi_init,ct_init,lm_init,  其中在lm_init中将 head初始化清0。初始化strstart为0。从文件中读入64KB的内容到window缓冲区中。  由于计算strstart=0时的ins_h,需要0,1,2这三个字节和哈希函数发生关系,所以在lm_init中,预读0,1两个字节,并和哈希函数发生关系。  然后lm_init调用 deflate()。   deflate() gzip的LZ77的实现主要deflate()中。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
环境说明测试对象(Redis服务器)阿里云数据库Redis集群版-双副本,8个节点,16G内存,引擎版本Redis 2.8(控制台查看为2.8.19)测试环境(Redis客户端)阿里云服务器计算型8核16G,Linux系统,与Redis服务器处于同一内网(网络延迟&1ms),Golang版本为1.9.2,使用插件Redigo()测试数据说明:每份玩家数据为100个Key(Key长度小于10Byte),100个Key对应的100个Value大小(单位为Byte)之和固定(在不同测试中该值设定不同,但同一测试的多次试验中该值不变)的随机数据,测试时使用2000份玩家数据user1 = {
key1: value1,
key2: value2,
key100: value100}user2 = ...测试方法单连接:每个测试点运行10次,Max为最大值,Min为最小值,Avg为平均值多连接:与单连接相同,但每个测试点的运行结果取多个连接中耗时最长的那个值例:在4个连接测试中,其中一次运行结果中每个连接处理时间分别为100ms,120ms,180ms,60ms,则该次运行结果取180ms,待10次运行结束后,该次运行结果180ms再参与最大值、最小值和平均值的计算测试结果测试1指令:MSET(批量写)数据:Value总和为20KB连接数(一个goroutine处理一个连接)12482050100200500100010000单位:msMax:580.83Min:440.21Avg:488.52Max:422.24Min:248.05Avg:279.69Max:208.93Min:175.42Avg:191.80Max:173.45Min:152.07Avg:163.01Max:165.09Min:125.58Avg:142.72Max:131.53Min:110.78Avg:124.52Max:132.24Min:117.26Avg:124.12Max:141.65Min:122.41Avg:131.08Max:258.39Min:137.88Avg:235.92Max:286.86Min:253.26Avg:266.05Max:220.56Min:159.18Avg:206.37测试2指令:MGET(批量读)数据:Value总和为100KB连接数(一个goroutine处理一个连接)12482050100200500100010000单位:msMax:2250.96Min:1340.84Avg:1928.13Max:1068.41Min:825.76Avg:965.90Max:875.87Min:576.29Avg:673.44Max:607.30Min:492.91Avg:525.13Max:543.10Min:425.47Avg:487.70Max:547.82Min:437.31Avg:475.74Max:519.16Min:452.92Avg:484.98Max:530.31Min:439.34Avg:491.04Max:1060.96Min:504.03Avg:785.06Max:1041.96Min:954.31Avg:992.77Max:1713.36Min:785.35Avg:1049.64测试结论从结果上看,50连接数是一个分水岭:当 连接数 & 50 时,增加连接数能较明显的缩短指令处理时间,这还算是比较符合直觉的,因为增加连接数相当于提高并发量,自然就缩短了处理时间当 连接数 & 50 时,增加连接数不仅没有缩短时间,反而时间还增加了,不清楚原因只能不负责任的推测一下:Redis服务器处理的机制、goroutine的调度影响针对 连接数 = 50 的情况,根据阿里云官方给出的数据,Redis集群内部最大网络带宽是484MByte/s在MSET中,Value总和为20KB,总数据量为2000 * 20KB即40MB,那么在最大带宽下,需要的时间为40/484s即82.64ms,测试的平均时间为124.52ms,可见大约2/3的时间消耗在集群内部的数据传输上在MGET中,Value总和为100KB,总数据量为2000 * 100KB即200MB,所需时间为100/484即413.22ms,而测试的平均时间为475.74ms,4/5以上的时间消耗在数据传输上由此得出结论,在这种使用场景下(并发低(对应连接数),请求低(对应QPS),但数据总量大),Redis集群的主要使用瓶颈是在网络带宽上,包括集群内部以及与Redis服务器处于同一内网下的Linux服务器的网络带宽另外,连接数 = 50 也不是绝对值,该值与多个因素有关:在相同指令(MSET或MGET)下,Key的大小和数量、Value的分布(因为Value总量固定,因此分布有可能是平均的、也有可能是两极化的)等都有关系,因此应该在具体情况下测试和确定问题记录Pipeline传输和接收的数据总量不一致:在使用Pipeline一次性发送20万条SET指令,数据总量为200MB后,再一次性发送20万条GET指令来获取,发现获取到的数据有时不足200MB,有时多于200MB。在Redigo的官方Github的Issues中找到了答案但未验证()
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
1、安装MariaDB安装命令yum -y install mariadb mariadb-server安装完成MariaDB,首先启动MariaDBsystemctl start mariadb设置开机启动systemctl enable mariadb接下来进行MariaDB的相关简单配置mysql_secure_installation首先是设置密码,会提示先输入密码Enter current password for root (enter for none):&–初次运行直接回车设置密码Set root password? [Y/n] &– 是否设置root用户密码,输入y并回车或直接回车New password: &– 设置root用户的密码Re-enter new password: &– 再输入一次你设置的密码其他配置Remove anonymous users? [Y/n] &– 是否删除匿名用户,回车Disallow root login remotely? [Y/n] &–是否禁止root远程登录,回车,Remove test database and access to it? [Y/n] &– 是否删除test数据库,回车Reload privilege tables now? [Y/n] &– 是否重新加载权限表,回车初始化MariaDB完成,接下来测试登录mysql -uroot -ppassword安装配置完成。2、权限授予授权远程登录用户(root用户不做远程登录)因为前面设置了创建远程访问用户: CREATE USER 'ghj'@'127.0.0.1' IDENTIFIED BY 'ghj';为远程用户授权:grant all privileges on *.* to 'ghj'@'127.0.0.1' identified by "ghj"指定授权:GRANT SELECT, INSERT, UPDATE ON `sywk`.* TO 'ghj'@'127.0.0.1';
显示权限:SHOW GRANTS FOR 'ghj'@'127.0.0.1';刷新配置:FLUSH PRIVILEGES;结束,可以访问了。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
文章来源于知乎中的提问,进行整理后的结果,形成完整的正确做事思维: 这是管理学的命题,“做正确的事”是指首先要决策正确,找对方向与目标,“正确的去做事”是指方法选择正确,要有效率。如何理解二者的关系是本命题的关键,应该是“做正确的事”是基础和前提,也就是说只有先找到了正确的事去做,然后才是选择正确的方法去做。如果反过来,只有正确的方法而没有正确的目标和方向,只能是越走越偏。越正确的去做一件错事,就会错的越多。管理大师彼得·德鲁克曾在《有效的主管》一书中简明扼要地指出:“效率是以正确的方式做事,而效能则是做正确的事。效率和效能不应偏废,但这并不意味着效率和效能具有同样的重要性。我们当然希望同时提高效率和效能,但在效率与效能无法兼得时,我们首先应着眼于效能,然后再设法提高效率。”请注意,在这段论述中,彼得·德鲁克提出了两组并列的概念:效率和效能,正确做事和做正确的事。在现实生活中,无论是企业的商业行为,还是个人的工作方法,人们关注的重点往往都在于前者:效率和正确做事。但实际上,第一重要的却是效能而非效率,是做正确的事而非正确做事。正如彼得·德鲁克所说:“对企业而言,不可缺少的是效能,而非效率。” “正确地做事”强调的是效率,其结果是让我们更快地朝目标迈进;“做正确的事”强调的则是效能,其结果是确保我们的工作是在坚实地朝着自己的目标迈进。大量研究表明,在工作中,人们总是依据下列各种准则决定事情的优先次序:(1)先做喜欢做的事,然后再做不喜欢做的事。(2)先做熟悉的事,然后再做不熟悉的事。(3)先做容易做的事,然后再做难做的事。(4)先做只需花费少量时间即可做好的事,然后再做需要花费大量时间才能做好的事。(5)先处理资料齐全的事,再处理资料不齐全的事。(6)先做已排定时间的事,再做未经排定时间的事。(7)先做经过筹划的事,然后再做未经筹划的事。(8)先做别人的事,然后再做自己的事。(9)先做紧迫的事,然后再做不紧迫的事。(10)先做有趣的事,然后再做枯燥的事。(11)先做易于完成的事或易于告一段落的事,然后再做难以完成的整件事或难以告一段落的事。(12)先做自己所尊敬的人或与自己有密切的利害关系的人所拜托的事,然后再做自己所不尊敬的人或与自己没有密切的利害关系的人所拜托的事。(13)先做已发生的事,然后做未发生的事。很显然,上述各种准则,都不符合高效工作方法的要求。对于这个问题应按事情的“重要程度”编排行事的优先次序。所谓“重要程度”,即指对实现目标的贡献大小。对实现目标越有贡献的事越是重要,它们越应获得优先处理;对实现目标越无意义的事情,愈不重要,它们愈应延后处理。简单地说,就是根据“我现在做的,是否使我更接近目标”这一原则来判断事情的轻重缓急。在上述的十三种决定优先次序的准则中,对我们最具支配力的恐怕是第九种——“先做紧迫的事,再做不紧迫的事”,大凡低效能的员工,他们每天80%的时间和精力都花在了“紧迫的事”上。也就是说,人们惯常地习惯是按照事情的“缓急程度”决定行事的优先次序,而不是首先衡量事情的“重要程度”。按照这种思维,他们经常把每日待处理的事区分为如下的三个层次:(1)今天“必须”做的事(即最为紧迫的事)。(2)今天“应该”做的事(即有点紧迫的事)。(3)今天“可以”做的事(即不紧迫的事)。但遗憾的是,在多数情况下,愈是重要的事偏偏愈不紧迫。比如向上级提出改进营运方式的建议、长远目标的规划,甚至个人的身体检查等,往往因其不紧迫而被那些“必须”做的事无限期地延迟了。所以,在麦肯锡公司,我们告诉新来的员工的第一个法宝就是:做要事,而不是做急事。精心确定主次做要事而不是做急事的观念如此重要,但常常为我们所遗忘。必须让这个重要的观念成为我们的工作习惯,在每开始一项工作时,都必须首先让自己明白什么是最重要的事,什么是我们最应该花最大精力去重点做的事。精心确定事情的主次有助于我们养成这样的习惯。在确定每一年或每一天该做什么之前,你必须对自己应该如何利用时间有更全面的看法。要做到这一点,你要问自己四个问题:1.我从哪里来,要到哪里去?我们每一个人都肩负着一个沉重的责任,虽然现在我每天在做着一些平凡的事,但再过10年或20年,我们中的有些,可能会成为公司的领导。大企业家、大科学家。所以,我们要解决的第一个问题就是,我们要明白自己将来要干什么?只有这样,我们才能朝着这个目标不断努力,把一切和自己无关的事情统统抛弃。2.我需要做什么?要分清缓急,还应弄清自己需要做什么。总会有些任务是你非做不可的。重要的事你必须分清某个任务是否一定要做,或是否一定要由你去做。这两种情况是不同的。非做不可,但并非一定要你亲自做的事情,你可以委派别人去做,自己只负责监督其完成。3.什么能给我最高回报?人们应该把时间和精力集中在能给自己最高回报的事情上,即会比别人干得出色的事情上。在这方面,让我们用帕累托定律来引导自己:人们应该用80%的时间做能带来最高回报的事情,而用20%的时间做其他事情,这样使用时间是最具有战略眼光的。4.什么能给我最大的满足感?有些人认为能带来最高回报的事情就一定能给自己最大的满足感。但并非任何一种事情都是这样。无论你地位如何,你总需要把部分时间用于做能够带给你满足感和快乐的事情上。这样你会始终保持生活热情,因为你的生活是有趣的。明白了上述四个问题,并以此来判断我们即将面对的纷至沓来的事情,就不至于让我们陷入到事务性的泥潭中,我们可以很快地确定出事情的主次,以最有效率的工作方法去获得最大效能的收获。事情的四个层次我们每个人每天面对的事情,按照轻重缓急的程度,可以分为以下四个层次,即重要且紧迫的事;重要但不紧迫的事;紧迫但不重要的事;不紧迫也不重要的事。1.重要而且紧迫的事情这类事情是你最重要的事情,而且是当务之急,有的是实现你的事业和目标的关键环节,有的则和你的生活息息相关,它们比其他任何一件事情都值得优先去做。只有它们都得到合理高效地解决,你才有可能顺利地进行别的工作。2.重要但不紧迫的事情这种事情要求我们具有更多的主动性、积极性和自觉性。从一个人对这种事情处理的好坏,可以看出这个人对事业目标和进程的判断能力。因为我们生活中大多数真正重要的事情都不一定是紧急的。比如读几本有用的书、休闲娱乐、培养感情、节制饮食、锻炼身体。这些事情重要吗?当然,它们会影响我们的健康、事业还有家庭关系。但是它们急迫吗?不。所以很多时候这些事情我们都可以拖延下去,并且似乎可以一直拖延下去,直到我们后悔当初为什么没有重视,没有早点来着手重视解决它们。3.紧迫但不重要的事情有这样的事情吗?当然,而且随时随地会出现。本来你已经洗漱停当准备休息,好养足精神明天去图书馆看书时,忽然电话响起,你的朋友邀请你现在去泡吧聊天。你就是没有足够的勇气回绝他们,你不想让你的朋友们失望。然后,你去了,次日清晨回家后,你头晕脑涨,一个白天都昏昏沉沉的。你被别人的事情牵着走了,而你认为重要的事情却没有做,这或许会造成你很长时间都比较被动。4.既不紧迫又不重要的事情很多这样的事情会在我们的生活中出现,它们或许有一点价值,但如果我们毫无节制地沉溺于此,我们就是在浪费大量宝贵的时间。比如,我们吃完饭就坐下看电视,却常常不知道想看什么和后面要播什么。只是被动地接受电视发出的信息。往往在看完电视后觉得不如去读几本书,甚至不如去跑跑健身车,那么刚才我们所做的就是浪费时间。其实你要注意的话,很多时候我们花在电视上的时间都是被浪费掉了。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
地宫说明我们看到的游戏地宫有如暗黑的地下城风格,泰瑞利亚的地道风格,苍之纪元和冒险岛的走格子风格。东方故事的深渊探险风格,这里了结合苍之纪元+深渊探险实现一套地宫副本的基础结构;与其走格子不同的是,地宫是触屏点击行走,当然ARPG的寻路实现走格子便不屑一谈了。我们简单的拆解一下地宫的基础程序结构,大概有:1、地图编辑器:可自动随机生成地宫,可编辑地表和景观,可编辑事件对象,可设置地图呈现效果;2、地宫场景:第三人称单位控制,视角控制,网格寻路实现,物件碰撞和交互处理;3、地宫事件(持续开发):事件呈现和触发(碰撞或近位点击触发),事件编辑和放置。地图制作地图声明为三层即地表层,景观层,和事件层,分三层在于,后续地表的起伏表现,景观层编辑优化,运行中事件管理,等... ...我们可以通过地牢算法,生成地牢随机网格,在通过网格路径优化,优化至项目中可行的地图网格布局;不过只有算法是不行的,利用算法生成的二维坐标网格,我们可以在网格中布局地表样式(可以随机或选择性布局,景观雷同)。完成生成后,策划可以通过编辑器改善地图,和添加事件,下面为编辑器的部分思路代码://*****************************************************************************
//Created By
//@Description 地图制作工具
//*****************************************************************************
using UnityE
using UnityE
using Boo.L
namespace Assets.Game.Editor.Scene
public class TravelMapTools : EditorWindow
// 地表,景观,事件 资源
public static TravelMapRes surfaceRes, barrierRes, eventsR
[MenuItem("Tools/地宫地图", false, 111)]
static void window()
TravelMapTools win = (TravelMapTools)EditorWindow.GetWindow(typeof(TravelMapTools), false, "TravelMap", false);
surfaceRes = new TravelMapRes("Assets/GameRes/Model/Map/TravelSurface/",".FBX");
barrierRes = new TravelMapRes("Assets/GameRes/Model/Map/TravelBarrier/",".FBX");
eventsRes = new TravelMapRes("Assets/GameRes/Model/Map/TravelEvents/",".prefab");
win.Show();
private int row = 32, col = 32;
// 景观几率
private int scape = 10;
private bool W
// 对象集合
private bool[,] mapA
// 生成次数
// 地表,高空,事件
private GameO
private GameObject surface, barrier,
private GameO
private BoxC
private Vector2 sp1, sp2, sp3;
void OnGUI()
int x = 10;
GUI.Label(new Rect(x, 30, 150, 220), "输入地图名称");
GUI.TextField(new Rect(x, 60, 150, 20), "MapName");
// 选择地表资源
GUI.Label(new Rect(x, 100, 150, 220), "选择地表资源");
sp1 = GUI.BeginScrollView(new Rect(x, 130, 150, 200), sp1, new Rect(0, 0, 150, 400));
for (int i = 0; i & surfaceRes.resNames.C i++)
surfaceRes.selected[i] = GUI.Toggle(new Rect(5, i * 25, 150, 20), surfaceRes.selected[i], surfaceRes.resNames[i]);
GUI.EndScrollView();
// 选择高空资源
GUI.Label(new Rect(x + 180, 100, 150, 220), "选择高空资源");
sp2 = GUI.BeginScrollView(new Rect(x + 180, 130, 150, 200), sp2, new Rect(0, 0, 150, 400));
for (int i = 0; i & barrierRes.resNames.C i++)
barrierRes.selected[i] = GUI.Toggle(new Rect(5, i * 25, 150, 20), barrierRes.selected[i], barrierRes.resNames[i]);
GUI.EndScrollView();
// 生成地图
if (GUI.Button(new Rect(x, 350, 130, 25), "生成地图"))
// 优化地图
if (GUI.Button(new Rect(x, 390, 130, 25), "优化地图"))
// 添加物件
GUI.Label(new Rect(x + 20, 430, 200, 25), "点击添加物件");
// 上方添加物件
if (GUI.Button(new Rect(x + 40, 450, 50, 25), "上"))
CopyBlock(0);
// 左方添加物件
if (GUI.Button(new Rect(x + 10, 480, 50, 25), "左"))
CopyBlock(2);
// 右方添加物件
if (GUI.Button(new Rect(x + 70, 480, 50, 25), "右"))
CopyBlock(3);
// 选择高空资源
if (GUI.Button(new Rect(x , 520, 130, 25), "添加事件"))
CopyBlock(-1);
sp3 = GUI.BeginScrollView(new Rect(x, 560, 150, 150), sp2, new Rect(0, 0, 150, 300));
for (int i = 0; i & eventsRes.resNames.C i++)
eventsRes.selected[i] = GUI.Toggle(new Rect(5, i * 25, 150, 20), eventsRes.selected[i], eventsRes.resNames[i]);
GUI.EndScrollView();
public void Creat()
// 创建地图
block = GameObject.Find("Block");
bc = block.GetComponent&BoxCollider&();
// 资源选择
surfaceRes.ReSelected();
barrierRes.ReSelected();
Undo.DestroyObjectImmediate(map);
map = NewMap();
map.transform.localPosition = Vector3.
mapArray = InitMapArray();
CreateMap(mapArray);
public void ResSelected(string path, List&GameObject& list, List&string& resList, bool[] selected)
list.Clear();
for (int i = 0; i & selected.L i++)
if (selected[i])
GameObject obj = AssetDatabase.LoadAssetAtPath(path + resList[i], typeof(GameObject)) as GameO
list.Add(obj);
public void Next()
if (times & 7)
Undo.DestroyObjectImmediate(map);
map = NewMap();
mapArray = SmoothMapArray(mapArray);
CreateMap(mapArray);
GameObject NewMap()
GameObject obj = new GameObject("mapName");
obj.AddComponent&BlockMap&();
obj.AddComponent&Terrain&();
obj.AddComponent&TerrainCollider&();
BlockMap bm = obj.GetComponent&BlockMap&();
surface = new GameObject("surface");
surface.transform.SetParent(obj.transform);
barrier = new GameObject("barrier");
barrier.transform.SetParent(obj.transform);
events = new GameObject("events");
events.transform.SetParent(obj.transform);
// 随机地块
GameObject RandomBlock(GameObject layer, List&GameObject& list, int x, int lay, int z)
string name = GetBlockName(x, lay, z);
GameObject blockobj = GameObject.Find(name);
// 预防重复添加
if (blockobj != null)
blockobj = Instantiate(block,
new Vector3(x * bc.size.x, (lay == 1 ? 0 : (lay - 1) * bc.size.y), z * bc.size.z),
Quaternion.identity, layer.transform) as GameO
blockobj.name =
Block b = blockobj.GetComponent&Block&();
b.rowIndex =
b.colIndex =
b.layerIndex =
int index = Random.Range(0, list.Count);
GameObject res = Instantiate(list[index], blockobj.transform);
res.transform.localPosition = Vector3.
public void CopyBlock(int way)
GameObject obj = (GameObject)Selection.activeO
if (!obj.transform.parent.gameObject.name.StartsWith("Block"))
obj = obj.transform.parent.gameO
Block b = obj.GetComponent&Block&();
switch (way)
case 0: //上
RandomBlock(barrier, barrierRes.objs, b.rowIndex, b.layerIndex + 1, b.colIndex);
case 2: //左
RandomBlock(surface, surfaceRes.objs, b.rowIndex - 1, b.layerIndex, b.colIndex);
case 3: //右
RandomBlock(surface, surfaceRes.objs, b.rowIndex + 1, b.layerIndex, b.colIndex);
case -1: //事件格子
eventsRes.ReSelected();
RandomBlock(events, eventsRes.objs, b.rowIndex, b.layerIndex + 1, b.colIndex);
string GetBlockName(int x, int y, int z)
"Block_" + x + "_" + y + "_" +
bool[,] InitMapArray()
bool[,] array = new bool[row, col];
for (int i = 0; i & i++)
for (int j = 0; j & j++)
array[i, j] = Random.Range(0, 100) & 60;
if (i == 0 || i == row - 1 || j == 0 || j == col - 1)
array[i, j] =
bool[,] SmoothMapArray(bool[,] array)
bool[,] newArray = new bool[row, col];
int count1, count2;
for (int i = 0; i & i++)
for (int j = 0; j & j++)
count1 = CheckNeighborWalls(array, i, j, 1);
count2 = CheckNeighborWalls(array, i, j, 2);
if (count1 &= 5 || count2 &= 2)
newArray[i, j] =
newArray[i, j] =
if (i == 0 || i == row - 1 || j == 0 || j == col - 1)
newArray[i, j] =
return newA
int CheckNeighborWalls(bool[,] array, int i, int j, int t)
int count = 0;
for (int i2 = i - i2 & i + t + 1; i2++)
for (int j2 = j - j2 & j + t + 1; j2++)
if (i2 & 0 && i2 & row && j2 &= 0 && j2 & col)
if (!array[i2, j2])
if (!array[i, j])
void CreateMap(bool[,] array)
for (int i = 0; i & i++)
for (int j = 0; j & j++)
if (!array[i, j])
GameObject go = RandomBlock(surface, surfaceRes.objs, i, 1, j);
GameObject go = RandomBlock(barrier, barrierRes.objs, i, 2, j);
if (array[i, j])
GameObject go = RandomBlock(surface, surfaceRes.objs, i, 1, j);
if (Random.Range(1, 100) & scape)
go = RandomBlock(barrier, barrierRes.objs, i, 2, j);
}//*****************************************************************************
//Created By
//@Description 地图制作工具
//*****************************************************************************
using Boo.L
using System.IO;
using UnityE
using UnityE
namespace Assets.Game.Editor.Scene
// 资源管理类
public class TravelMapRes
// 资源路径
// 资源名称
public List&string& resNames = new List&string&();
// 选中位置
public bool[]
// 选中资源
public List&GameObject& objs = new List&GameObject&();
// 许可资源类型
public string resPrex =
public TravelMapRes(string p)
this.path =
InitResList(this.path);
public TravelMapRes(string p,string prex)
this.path =
InitResList(this.path);
public void InitResList(string path)
resNames.Clear();
if (Directory.Exists(path))
DirectoryInfo direction = new DirectoryInfo(path);
FileInfo[] files = direction.GetFiles("*", SearchOption.AllDirectories);
for (int i = 0; i & files.L i++)
if (files[i].Name.EndsWith(".meta"))
if (resPrex != null && !files[i].Name.EndsWith(resPrex))
resNames.Add(files[i].Name);
this.selected = new bool[resNames.Count];
this.selected = new bool[0];
public void ReSelected()
objs.Clear();
for (int i = 0; i & selected.L i++)
if (selected[i])
GameObject obj = AssetDatabase.LoadAssetAtPath(path + resNames[i], typeof(GameObject)) as GameO
objs.Add(obj);
public GameObject RandomSelectedObj()
ReSelected();
return objs[Random.Range(0, objs.Count)];
}角色控制地图中角色控制和相机差不多和ARPG一样了,视角可上下,左右移动,相机跟随角色和调整视角。//*****************************************************************************
//Created By Gouhj on .
//@Description 移动控制器
//*****************************************************************************
using UnityE
using UnityEngine.AI;
public class PlayerMove : MonoBehaviour
[Header("是否点击移动")]
public bool isClickMove =
[Header("是否采用网格导航")]
public bool IsNavA
[Header("移动速度")]
public float m_speed = 5f;
[Header("跑动动作")]
public string RUN = "is_run";
[Header("待机动作")]
public string IDLE = "is_idle";
float ogrY = 0;
// 动作 0 待机,1 跑动
int currentState = 0;
// 目标位置
Vector3 targetP
void Start()
agent = this.GetComponent&NavMeshAgent&();
targetPos = this.transform.
ogrY = targetPos.y;
void LateUpdate()
if (isClickMove)
MoveControlByMouse();
MoveControlByTranslate();
//Translate移动控制函数
void MoveControlByTranslate()
if (Input.GetKey(KeyCode.W) | Input.GetKey(KeyCode.UpArrow)) //前
this.transform.Translate(Vector3.forward * m_speed * Time.deltaTime);
if (Input.GetKey(KeyCode.S) | Input.GetKey(KeyCode.DownArrow)) //后
this.transform.Translate(Vector3.forward * -m_speed * Time.deltaTime);
if (Input.GetKey(KeyCode.A) | Input.GetKey(KeyCode.LeftArrow)) //左
this.transform.Translate(Vector3.right * -m_speed * Time.deltaTime);
if (Input.GetKey(KeyCode.D) | Input.GetKey(KeyCode.RightArrow)) //右
this.transform.Translate(Vector3.right * m_speed * Time.deltaTime);
// 鼠标点击移动
void MoveControlByMouse()
if (Input.GetMouseButton(0))
//获得鼠标屏幕位置
Vector3 mousePos = Input.mouseP
//将屏幕位置转为射线
Ray ray = Camera.main.ScreenPointToRay(mousePos);
//用来记录射线碰撞记录
if (Physics.Raycast(ray, out hit))
// 点中地板砖
//if (!hit.collider.name.Equals("Terrain"))
targetPos = new Vector3(hit.point.x, ogrY, hit.point.z);
if (!player) {
player = this.transform.GetChild(0).gameO
transform.rotation = Quaternion.LookRotation(targetPos - transform.position);
if (IsNavAgent)
agent.SetDestination(targetPos);
if (IsNavAgent)
if (agent.remainingDistance == 0)
AnimatorChange(0);
AnimatorChange(1);
if (Mathf.Abs(transform.position.x - targetPos.x) & 0.1 || Mathf.Abs(transform.position.z - targetPos.z) & 0.1)
AnimatorChange(1);
transform.position = Vector3.MoveTowards(transform.position, targetPos, m_speed * Time.deltaTime);
AnimatorChange(0);
// 动作切换
void AnimatorChange(int state)
if (!player)
if (currentState == state)
currentState =
switch (state)
Animator ani = player.GetComponent&Animator&();
ani.SetTrigger(IDLE);
ani = player.GetComponent&Animator&();
ani.SetTrigger(RUN);
}//*****************************************************************************
//Created By
//@Description 第三人称相机
//*****************************************************************************
using UnityE
namespace Game
public class PlayerCamera : MonoBehaviour
private Vector3 offsetP
private float scrollSpeed = 10; //鼠标滚轮速度
private bool isR //开启摄像机旋转
private float rotateSpeed = 2; //摄像机旋转速度
// Use this for initialization
void Start()
player = GameObject.Find("player").
//摄像机朝向player
transform.LookAt(player.position);
transform.SetParent(transform);
//获取摄像机与player的位置偏移
offsetPosition = transform.position - player.
// Update is called once per frame
void Update()
//摄像机跟随player与player保持相对位置偏移
transform.position = offsetPosition + player.
//摄像机的旋转
RotateView();
//摄像机的摄影控制
ScrollView();
void ScrollView()
//返回位置偏移的向量长度
distance = offsetPosition.
distance -= Input.GetAxis("Mouse ScrollWheel") * scrollS
//限制变化长度的范围在最小为4最大为22之间
distance = Mathf.Clamp(distance, 4, 22);
//新的偏移值为偏移值的单位向量*变换长度
offsetPosition = offsetPosition.normalized *
void RotateView()
//按下鼠标右键开启旋转摄像机
if (Input.GetMouseButtonDown(1))
isRotating =
//抬起鼠标右键关闭旋转摄像机
if (Input.GetMouseButtonUp(1))
isRotating =
if (isRotating)
//获取摄像机初始位置
Vector3 pos = transform.
//获取摄像机初始角度
Quaternion rot = transform.
//摄像机围绕player的位置延player的Y轴旋转,旋转的速度为鼠标水平滑动的速度
transform.RotateAround(player.position, player.up, Input.GetAxis("Mouse X") * rotateSpeed);
//摄像机围绕player的位置延自身的X轴旋转,旋转的速度为鼠标垂直滑动的速度
transform.RotateAround(player.position, transform.right, Input.GetAxis("Mouse Y") * rotateSpeed);
//获取摄像机x轴向的欧拉角
float x = transform.eulerAngles.x;
//如果摄像机的x轴旋转角度超出范围,恢复初始位置和角度
if (x & 10 || x & 80)
transform.position =
transform.rotation =
//更新摄像机与player的位置偏移
offsetPosition = transform.position - player.
}事件相关地图生成显然都是用地块填充的,所以为了控制地块,添加地块事件,以及设置地块属性等因素,一定会给地块绑定一个脚本。而事件的触发,可通过配置决定是碰撞还是点击触发,在把消息派发到事件系统就好了。关于事件的触发,可和这篇文章关联起来:地图起伏效果实现//*****************************************************************************
//Created buy Gouhj on .
//@Description 地块特效
//*****************************************************************************
using System.C
using System.Collections.G
using UnityE
public class BlockMap : MonoBehaviour {
[Header("地块是否起落")]
public bool isDownUp =
[Header("是否遮罩探索")]
public bool isUnknow =
[Header("设置玩家")]
public GameO
[Header("上升位置起点")]
public int downPosY = 10;
[Header("上升速度")]
public float upSpeed = 0.5f;
// 地表,高空,事件
GameObject surface, barrier,
// 地表二维坐标系
GameObject[,]
// 函数和咧数
public int row,
// 地块大小;
public float bsizeX = 2, bsizeZ = 2;
private int startRow, startC
// 附近的格子
List&GameObject& nearList = new List&GameObject&();
// 上升列表
List&GameObject& upList = new List&GameObject&();
// 地表默认平面在世界坐标中的高度
int defaultSurfaceY = 0;
// 默认圈数
int defaultRoundCount = 2;
// Use this for initialization
void Start () {
if (player != null)
tr = player.
tr = GameObject.Find("player").
surface = GameObject.Find("surface");
barrier = GameObject.Find("barrier");
events = GameObject.Find("events");
blocks = new GameObject[row,col];
for (int i = 0; i & surface.transform.childC i++)
obj = surface.transform.GetChild(i).gameO
block = obj.GetComponent&Block&();
blocks[block.rowIndex, block.colIndex] =
if (block.isStartPoint)
startRow = block.rowI
startCol = block.colI
if (isDownUp) {
obj.SetActive(false);
// Update is called once per frame
void Update () {
UpAndDown();
// 显示周围
void show(int r,int c)
for (int i = 0; i & i++)
for (int j = 0; j & j++)
if (blocks[i, j] == null)
// 判断是否是周围的格子
bool IsNear(int r,int c,GameObject obj)
Block b = obj.GetComponent&Block&();
//if(b.rowIndex-r)
// 上下起伏效果实现
void UpAndDown()
if (isDownUp) {
ShowNear();
UpListRun();
void UpListRun() {
if (upList.Count == 0)
GameObject obj =
for (int i = upList.Count - 1 ; i &= 0 ;i --) {
obj = upList[i];
// 地表默认平面
if (obj.transform.position.y &= defaultSurfaceY) {
upList.RemoveAt(i);
obj.transform.position = new Vector3(obj.transform.position.x,
defaultSurfaceY, obj.transform.position.z);
obj.transform.position = new Vector3(obj.transform.position.x,
obj.transform.position.y + upSpeed, obj.transform.position.z);
void ShowNear() {
if (tr == null)
SelectNear(nearList);
GameObject obj =
for (int i = 0; i & nearList.C i++) {
obj = nearList[i];
if (! obj.activeSelf) {
// 下降到起伏位置
obj.transform.position = new Vector3(obj.transform.position.x,
obj.transform.position.y - downPosY ,obj.transform.position.z);
obj.SetActive(true);
upList.Add(obj);
void SelectNear(List&GameObject& list) {
list.Clear();
float x = tr.position.x;
float y = tr.position.y;
float z = tr.position.z;
int r = Mathf.CeilToInt(x/bsizeX);
int c = Mathf.CeilToInt(z/bsizeZ);
// 玩家位置
list.Add(blocks[r, c]);
SelectRound(list,r,c, defaultRoundCount);
void SelectRound(List&GameObject& list,int r,int c,int round) {
for (int i = 1; i & round+1; i++) {
//上,下,左,右
CheckIn(list, r, c + i);
CheckIn(list, r, c - i);
CheckIn(list, r - i, c);
CheckIn(list, r + i, c);
//左上,左下,右上,右下
CheckIn(list, r - i, c + i);
CheckIn(list, r - i, c - i);
CheckIn(list, r + i, c + i);
CheckIn(list, r + i, c - i);
bool CheckIn(List&GameObject& list,int r,int c) {
if (r & 0)
if (r &= row)
if (c & 0)
if (c &= col)
GameObject obj = blocks[r, c];
if (obj == null)
list.Add(obj);
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
开发情况 项目在2016年3月制作demo并立项,4月制作版署版本申请版号,8月基本开发完成并开启测试。在H5领域算上比较早的一款真真意义上的MMORPG,还携带了阵营PVP战场。付费内容是项目调整和最反复无常的内容,层消耗了一个核心程序2个月的时间做一个被砍掉的礼包推送系统。以下是开发总结:重大问题1、项目计划开发以来一直忙于项目内容,没有长远的官方买量计划,依赖渠道,上线后完全被渠道推着走,更少跳出来关注产品和市场。在盲目开发渠道内容中,项目要求和标准不清晰,团队人员成长搁浅,项目很快进入了维护期。2、版号问题本来版号早就拿下,但商标注册的时候和王龙存在争议,导致各个渠道发行的时候定制化改动太多,影响版本维护。游戏名称也一直比较受争议,和网龙争取商标权后逼不得已改名决战荣耀,导致渠道这边出现了很多定制内容。3、团队问题团队关注用户体验较为滞后,游戏前期表现生硬,设计人员对程序理解不足,采用开发成本和系统主次不匹配,较多低成本能来更好表现和体验的方案考虑不到,产品一开始方向变化没有和切实的工作结合起来调整。项目没有项目经理和制作人,主策划过多陷于业务,产品没有具体的发展规划。渠道接入问题时间反复不确定,对外部的伤害很大,调度不敏捷,接入期间版本计划几乎为空对玩家伤害较大。接入期间,anysdk正在发展H5渠道存在未知问题,很多渠道需要单独接入,商务一次拿下40个左右渠道分两批接入;各种数据都还没稳定下来就急于接入渠道,其实可以考虑不重要的渠道不上,先把产品数据做好再上第二批渠道。模板化由于上了渠道,为了保障渠道数据变化就要不停地开活动,除了新渠道接入,版本工功能开发;程序没有时间做调优和优化工作,很多功能结构已不健康,共性设计复用性低,比如没有设计可供策划配置的活动模板,就需要一直安排一个人做活动开发,当然这个时候也不会安排1个月左右做模板,导致程序做了很多地性价比的工作。架构不完整开发时包裹和任务的量级评估不够,我们认为一个挂机游戏,不需要复杂的任务、成就和包裹结构,开发时进行简单的过程化编程。但游戏定位发生了变化,在最终开发成了一个MMORPG,且数据变化比普通端游还高(玩家切场景率相当高)智能陪跑,复杂的大数据检测、智能合服、阵营、领地、营地等已超出中型端游的调度级别,如我们另外三个项目《魔侠传》、《巫神归来》、《异星帝国》。后来回忆,当决策发生变化时,程序应当立即向策划索取结构调整时间。定制化设计策划为玩家定制化设计过多,就礼包而言都有多种,根据玩家不同时期,不同情况不同数据状态推送不同的礼包,这显然是一个特大的坑,开发这种钻石直接购买且需要采集大量数据和规则的系统,必定需要一个核心程序来开发以保障质量,但是这个系统随着数据变化和新功能的增加会持续改进,导致一个核心程序在这里整整陪同调整就占用了至少2个月时间,最终这种推送礼包砍掉了,改为直接卖礼包;而这段时间如果这个程序没做这些去做更多的新手内容,我想那才更重要的。(像下面,远景+更具表现力的新手关卡却是维护期才开发的内容!)屏幕适配游戏开始定制的大小没有前瞻性,后面做大地图修改量极高。开始立项的时候我们只考虑了H5在各种机型上的显示情况,主要是不能出现黑边,但除此之外没有考虑到游戏后期是否需要全屏化;全屏化后的效果又是怎么样,比如《蜀山世界》和《御天传奇》,《蜀山世界》在分辨率高的手机上,可视区域自然大一些,这自然符合PC全屏设计的适配效果,也由于这种设计所以它没有全屏遮挡的一级界面,而《蜀山世界》在实现这种效果下对一级界面进行了填充处理,我认为者用户体验更好。回过头来我们在前期没有考虑到后面自然要面对这些情况:《传奇世界H5》,采用iphone6高宽像素(750*1334,即9:16的比例),优先保证微信用户最优体验显示(当时认为H5用户用微信玩的人更多,更方便传播),进行了人工比例剪裁,将微信顶部黑条的区域计算在内,形成了750*1252的最终比例(保证微信环境玩游戏是全屏的感觉),在适配不同机型、平台时,会保持宽高比例不变,所以会出现“适配黑边”,如下图所示:按最常规的分辨率720×1280计算,如果我们设置这个为参考分辨率,那么遇到低分辨率的机型就会存在显示不完的情况。如果遇到高分辨率的情况就会出现黑边;资源管理1、我们有较好的性能却被用在了低性价比的地方从CPU和内存上去比较,最优的时候性能超越H5暗黑之王的四倍,得益于我们对所有资源对象进行管理,回收、缓存、再利用,包括很多数据对象都是情况对象数据,对象会被再次使用;程序甚至接管了白鹭引擎的纹理管理、音频和以及代码解析、list控件等性能才达到优秀的地步。支持各种资源使用量以及精致度都远远大于其他游戏,但资源管理和利用没有统筹规划,想到什么上什么(比如10级一套装玩家甚至跳着穿)性价比低的内容较多,最终造成GPU占用较高还是不够流畅。2、UI拼接工作没有交接给策划这点比较悲哀,涉及到资源管理,和UI制作没有决心交给配置策划。后面程序在UI和资源上几乎花费了半个人力,本来我们前端就只有一个人。3、特殊技能动作如果玩家每10级一套套装形象,150级不算时装都有15套动作,如果我们增加了某一个技能动作,或者其他动作就需要把所有形象都做一套该动作,浪费资源成本,我们可以采用通用特效避免每套形象都要配一个动作,比如决战魔域中 弓手的爆裂箭和战士的跳斩。闪退问题前期主要遇到IOS端异常退出,后面把通信传送改为文本传输,当然还有WEBGL的问题ios:采用canvas,android:采用webgl 火狐采用canvas。翻译翻译问题的暴露是在韩版计划里,我们之前没有考虑过多国语言版。程序除了复用性较强的提示放在了翻译文件中,有部分提示直接写在了代码里,想想都后悔。地图地图采用的是瓦片地图,分为surface地表层,forbid 阻隔层,airlayer高空层,scenelayer远景层。1、另外还有一些元素,比如switchin、switchout切入,切出区域;2、地图首次加载minimap模糊地图,然后根据玩家位置,加载对应可视区域的地图信息;3、根据分辨率计算地块大小每块400-600之间,满足九宫格最优加载数量即可。但远景层始终没用上,远程和地表错位的动态效果其实很好的,性价比也很高。版本管理1、版本号通过md5工具对所有资源生成一份清单,下一次发布版本会用两次清单进行比较,如果不同则会增加相对资源版本号。2、资源管理每个渠道有对应的入口html,将加载同一份资源,html中标注了不同的渠道信息用于程序中识别不同渠道的业务。3、动态管理由于不同渠道可能存在换资源等情况,我们暴露了源路径管理,可以在html中动态设置,保障一旦渠道有不同需求时可以调用各自的资源管理文件,实际还真用到了。中控和多集群1、决战魔域的整体架构初衷是,全渠道,全服都在一个集群中运行,无限开服衮服。后面遇到了,不同渠道可能存在的需求不同,比如开服时间和版本不同(后面根本没这样操作过)我们硬是做了多个集群。2、对集群设计过于信任,在一次导量中出现了磁盘IOPS和一个BUG的问题同时出现,程序无法立即准确定位问题,集群不可能立即修复。需要再开一组集群接收更多的导量玩家,但没有设计中控最终不能第一时间开辟第二组集群,最终在集群中停到了陪跑业务后,节余出的性能承载更多玩家(别相信阿里云高效云盘真能撑住2W的IOPS,后面我们直接换SSD了)。用户端远程调试我们在后端直接将Weinre注入玩家客户端&script src="http://192.168.25.55:8080/target/target-script-min.js#anonymous"&&/script&然后就可以调试看玩家客户端了,使用 Weinre 远程调试:1、安装weinre ,ios下载node.js后可以通过npm安装(这东西安装后好处很多)2、通过npm install -g weinre命令可以安装,但是可能存在权限问题所以要在前面加sudo运行 Weinre:会启动一个web服务器weinre --boundHost 192.168.25.55;默认通过localhost:8080或者 192.168.25.55 就可以进去了事件添加和移除其中,EventBin 有 thisObject: thisObject字段,这就是为什么每次添加事件后,都必须要进行removeEventListener,否则EventDispatcher始终握有eventBin的句柄,从而握有thisObject,如果将thisObject从parent中remove,thisObject并不会回收,因为EventDispatcher对他有引用,最终导致了内存泄露。但是在P28中,一般情况下不会去手动的移除egret.TouchEvent.Touch_Tap点击事件,那是因为点击事件被注册在面板的子节点中,例如按钮,按钮本身继承自EventDispatcher作为了一个“事件派发器”,当子节点被移除时,在不存在内存泄露的情况下,forelet类的this不再被外部引用,所以该“事件派发器”也会被被成功回收,不会存在内存泄露的问题。总结1、过程发展如果不健康,那结果一定不会很好;2、不将就,不随意听信,只要是正确的,就坚持做好。... ...
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
网格自动寻路:①
把场景中不动的物体勾选static②
烘焙寻路网格③
给需要寻路的物体添加脚本添加NavMeshAgent组件和控制脚本实现:① 制作一个场景Terrain对象的地图,上面可以铺上一些Cube把场景设为静态路径烘焙只烘焙静有静态导航属性的对象,也可以通过在Inspector中设置要烘焙的对象,没有改属性的变没有路径烘焙过寻路网格后会显示蓝色的网格,表示可行走区域选择window→navigation,调出navigation面板,选择bake,形成一个蓝色路面,enemy将在这个蓝色路面上进行寻路注意,寻路对象的NavMeshAgent组件在没有网格的情况下不可使用。using System.C
using System.Collections.G
using UnityE
using UnityEngine.AI;
public class RayTest : MonoBehaviour {
private RaycastH//射线碰到的碰撞信息
public GameObject navP//寻路的人
private NavMeshA
private void Start()
agent = navPlayer.GetComponent&NavMeshAgent&();
private void Update ()
//射线起始位置
ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray,out hit, 100) && Input.GetMouseButtonDown(0))
agent.SetDestination(hit.point);
Debug.DrawLine(ray.origin, hit.point, Color.red);
碰撞检测:发生触发的条件:1、发生碰撞的物体两者其中之一有Rigidbody即可,发生碰撞的两个游戏对象必须有Collider,其中一方勾选IsTrigger表示被动方 2、确认地形上是可达的,像下图盒子放置的位置,恰好玩家卡在缝里上不去就一直没发生碰撞事件角色控制角色控制大概有4种途径,鼠标点击,键盘控制(下面代码附这两种逻辑),另外还有手机的重力感应和手势键盘public class PlayerMove : MonoBehaviour
[Header("是否点击移动")]
public bool isClickMove =
[Header("是否采用网格导航")]
public bool IsNavA
[Header("移动速度")]
public float m_speed = 5f;
[Header("跑动动作")]
public string RUN = "is_run";
[Header("待机动作")]
public string IDLE = "is_idle";
float ogrY = 0;
// 动作 0 待机,1 跑动
int currentState = 0;
// 目标位置
Vector3 targetP
void Start()
agent = this.GetComponent&NavMeshAgent&();
targetPos = this.transform.
ogrY = targetPos.y;
void LateUpdate()
if (isClickMove)
MoveControlByMouse();
MoveControlByTranslate();
//Translate移动控制函数
void MoveControlByTranslate()
if (Input.GetKey(KeyCode.W) | Input.GetKey(KeyCode.UpArrow)) //前
this.transform.Translate(Vector3.forward * m_speed * Time.deltaTime);
if (Input.GetKey(KeyCode.S) | Input.GetKey(KeyCode.DownArrow)) //后
this.transform.Translate(Vector3.forward * -m_speed * Time.deltaTime);
if (Input.GetKey(KeyCode.A) | Input.GetKey(KeyCode.LeftArrow)) //左
this.transform.Translate(Vector3.right * -m_speed * Time.deltaTime);
if (Input.GetKey(KeyCode.D) | Input.GetKey(KeyCode.RightArrow)) //右
this.transform.Translate(Vector3.right * m_speed * Time.deltaTime);
// 鼠标点击移动
void MoveControlByMouse()
if (Input.GetMouseButton(0))
//获得鼠标屏幕位置
Vector3 mousePos = Input.mouseP
//将屏幕位置转为射线
Ray ray = Camera.main.ScreenPointToRay(mousePos);
//用来记录射线碰撞记录
if (Physics.Raycast(ray, out hit))
// 点中地板砖
//if (!hit.collider.name.Equals("Terrain"))
targetPos = new Vector3(hit.point.x, ogrY, hit.point.z);
if (!player) {
player = this.transform.GetChild(0).gameO
transform.rotation = Quaternion.LookRotation(targetPos - transform.position);
if (IsNavAgent)
agent.SetDestination(targetPos);
if (IsNavAgent)
if (agent.remainingDistance == 0)
AnimatorChange(0);
AnimatorChange(1);
if (Mathf.Abs(transform.position.x - targetPos.x) & 0.1 || Mathf.Abs(transform.position.z - targetPos.z) & 0.1)
AnimatorChange(1);
transform.position = Vector3.MoveTowards(transform.position, targetPos, m_speed * Time.deltaTime);
AnimatorChange(0);
// 动作切换
void AnimatorChange(int state)
if (!player)
if (currentState == state)
currentState =
switch (state)
Animator ani = player.GetComponent&Animator&();
ani.SetTrigger(IDLE);
ani = player.GetComponent&Animator&();
ani.SetTrigger(RUN);
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
问题表现普遍现象为服务器卡顿,无法进入服务器,场景卡住无响应,问题产生的原因大概有:1、CPU飚高通常是密集型调度产生,通过各种语言提供的性能分析工具,top出问题进程;在找到该进程当前栈的调度者即可。Java可以通过打印当前系统线程,的方式直接定位各个线程的栈停在什么位置;Erlang可以通过etop找出调度高的进程,在通过eprof找出改进程当前执行的函数是什么;Golang可以通过pprof直接获得当前程序的栈调度排序。2、内存飚高可能存在对象一直开辟,没有回收的情况,或者消息队列处理成等待状态,需要找到内存高的进程,检查其消息队列,分析内存形态(堆还是栈),堆内存一眼即可看出什么泄漏,栈开销过大,就需要检查重度函数了。Java通常打印OutOfMemoryError,直接定位;或者通过 jstat 分析gc和各个对象所处的情况Erlang会输出overflow日志定位,也可通过process或erlang:memory查询情况,etop也可能找出占用内存较高的进程。3、IO高IO高,可能存在通信延迟,磁盘写入等待过长等问题,可通过iostat确认,检查通信和IO操作的部分。这部分通常要么数据库出现问题,要么网络被攻击了,要么发送数据过大,(文件操作排除)通过服务器zabbix监控程序检查对应时间点的网络/磁盘 IO可确认时间点。4、上述都正常需要检查机房网络问题,片区网络,从环境上考虑。逻辑问题1、先确认数据和状态、数据是否正确,状态是否正常(因为这两者具有感染性);2、如果数据和状态都正常,那很简单就是逻辑顺序或者逻辑判定问题;3、如果数据和状态异常,那么需要列出改变数据和状态的感染源,从每个源头分析是否造成数据异常。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
数据库性能测试windows测试环境: 系统:win7 ,内存:16G , CPU:Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz 3.40GHz ,单节点写(每条数据大小50Byte):速率(不间断写入):25000条/秒写入上限:1200w条无锁读(单条数据50Byte):速率(不间断读取):35000条/秒上限:无锁读(单条数据50Byte):速率(不间断读取):30000条/秒上限:无写(单条数据200k):速率(不间断写入): 5000条/秒上限: 5W条同时读写(单条数据50Byte):速率: 20000条/秒上限:800w条写入速率瓶颈在磁盘io,写入规模与内存有关。超过写上限时,内存飙升,写入超时。linux(单节点)测试环境:系统:Centos 5.8 ,内存:8G , CPU:Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz 3.40GHz ,单节点写(每条数据大小50Byte):速率(不间断写入):25000条/秒(数据规模10w条)写入上限:600w条无锁读(单条数据50Byte):速率(不间断读取):3000条/秒(数据规模10w条)上限:无锁读(单条数据50Byte):速率(不间断读取):2500条/秒(数据规模10w条)上限:无写(单条数据200k):速率(不间断写入): 2000条/秒(数据规模5w条)上限: 5W条同时读写(单条数据50Byte):速率: 20000条/秒上限:500w条写入速率瓶颈在磁盘io,写入规模与内存有关。超过写上限时,内存飙升,写入超时。linux(集群)服务器环境:系统:Centos 5.8 ,内存:8G , CPU:Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz 3.40GHz 测试项目:(由两个slave节点组成的集群)客户端环境(本地虚拟机):系统:Centos 7.0, 内存:8G , CPU:Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz 3.40GHz,测试方法:用tsung(压力测试工具)模拟多个客户端异步向服务器发起请求(每一次请求对应一次数据写入)写(单条数据50k):速率:7000条/秒规模:100w条读(单条数据50k):速率: 7000条/秒规模:100w条同时读写(单条数据50k):速率:2500条/秒规模:50w条性能瓶颈在网络io。mensia 、自研数据库、redis 比较 mnesia自研数据库redis测试环境linux 虚拟机 centos 7.0, erlang otp 17.5linux 虚拟机 centos 7.0, erlang otp 17.5linux 虚拟机 centos 7.0, erlang otp 17.5, Redis 2.8.7, eredis单节点单进程(数据规模100w条)写耗时(单位秒)36.637.3223.5读耗时(单位秒)29.814.0217.4删除耗时(单位秒)36.825.6221同时读写耗时(单位秒)38.2/41.838.5/44.0300/301单节点多进程(数据规模100w条)写平均耗时(10个进程,单位秒)104.3154(无锁)373读平均耗时(10个进程,单位秒)90.754.3373读平均耗时(20个进程,单位秒)196.197.0746读平均耗时(40个进程,单位秒)430.9205.61465读平均耗时(50个进程,单位秒)545.9258.71773单节点多进程(数据规模10w条)读平均耗时(10个进程,单位秒)8.65.238读平均耗时(20个进程,单位秒)19.29.874读平均耗时(40个进程,单位秒)42.020.6142读平均耗时(60个进程,单位秒)67.331.7214单节点多进程(数据规模100w条,多张表)读写平均耗时(10个进程,10张表,单位秒)83/7152/135125/130读写平均耗时(20个进程,20张表,单位秒)179/170115/270243/251读写平均耗时(40个进程,40张表,单位秒)400/404/486/537如何在Erlang中操作Redis?下载和编译:git clone git://github.com/wooga/eredis.git
./rebar compile在console中使用:erl -pa ebin/
{ok, C} = eredis:start_link().
{ok, &&"OK"&&} = eredis:q(C, ["SET", "foo", "bar"]).
{ok, &&"bar"&&} = eredis:q(C, ["GET", "foo"]).在模块中使用:save_order_id({Pid, DynData}) -&
{ok, Rds} = eredis:start_link(),
{_,Val} = ts_dynvars:lookup(order_id, DynData),
Id = binary_to_list(Val),
{ok, _} = eredis:q(Rds, ["RPUSH", "order_list", Id]).
get_order_id({Pid, DynData}) -&
{ok, Rds} = eredis:start_link(),
{ok, Id} = eredis:q(Rds, ["LPOP", "order_list"]),
redis使用eredis 支持的-SET 插入或修改 -DEL 删除 -GET 读取-MSET 多条数据插入 -MGET 多条数据读取HASH hash相关命令LIST list相关命令 -LPOP -LPUSH -RPUSH -LRANGESET set相关命令 -SADDTransactions 事务相关命令 -MULTI -EXECPipelining -多条语句顺序执行redis 安装服务端安装部署redis 分布式部署
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
会话-session  会话是指一个终端用户与交互系统进行通讯的过程,这个过程是连续的,也可以时断时续的,TCP的三次握手就创建了一个会话,TCP关闭连接就是关闭会话;一、会话的存在方式  在程序设计上session的存在通常以一个独立的进程(Erlang),一个线程(Java)的方式存在。  它通常会被以连接的业务为基础封装成一个业务模块或对象,处理对应连接的消息收发以及状态管理(其消息通常会被对应的协议模块处理,然后路由至业务模块);  路由消息的对应业务模块在处理业务的时候,根据程序应用环境设计的不同,可能会在会话本进程/线程处理(不担心阻塞的情况),  也可能会在路由时就新开辟进程处理(在Erlang上常常如此,毕竟对它来说开辟进程不算一回事儿),但分裂进程处理不代表会异步返回,在消息的流程控制上应该提供异步和同步接口。二、会话缓冲  我们可以会在编写会话模块时少不了给它定义创建时间、活跃时间、唯一标记、属性,等。  用途可想而知,一个连接就是一个会话代表着一个用户,用户登录前后的状态,用户登录后的数据,用户进行业务输入时的业务处理,逻辑模块最便捷的方式就是从会话的属性中获取数据进行业务处理,  然后再由存储管理器定期将会话中的数据发送至数据中心进行存储,在Java上数据和会话的关系只是一个引用,数据取走的函数也就只多了一个同步锁,但Erlang这个进程世界上可能不太一样,当然也可以存储在会话进程,  但Erlang的Port在Linux进化中并不具备优势,当会话业务庞大到会阻塞消息收发的时候,当会话进程进行业务处理业务模块中有需求再访问会话进程数据造成等待死锁的时候,你就不能再采用会话存放了,  另辟蹊径创建一个数据存储的伴随进程可能值得考虑,业务节点的存储管理器会根据自身节点在集群环中所处的位置进行间隔性数据存储。  由于伴随进程的存在,它可以不和会话进行双向连接,它不必随着会话进程中止而自身销毁。它只负责缓存用户数据相对安全。三、会话分布管理  在集群中有网关层这个角色,会话通常都在这一层创建,当达到百万级别的会话时可能被均衡到多个网关节点,我们要对会话进行分类管理,比如游戏可能会以大区为单位管理会话;  管理会话的目的主要是服务器进行消息派发的时候会进行选择,如果业务较多一个管理器是忙不过来的,所以管理就要仔细衡量其运行和应用环境了。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
问题记载问题记载 11、做好各个功能的性能评估 12、 任务结构调整 13、缓存的充要条件 24、数据散列设计带来的问题 25、计算层和网关层的分层考虑 26、增加进程指定规则创建机制,原因在于: 27、排行榜,P25之前遇到的排行榜问题性能问题 38、P25大地图的设计 39、其他建议 3 1、做好各个功能的性能评估P28-17年7月24日渠道瞬时导量2w+,导致服务器承载超过上限服务器卡顿无法正常游戏,玩家难以登录当天维护两次;问题主要原因是,程序BUG+配置不够,具体如下:a、数据库采用高效云盘且只有两个存储节点,由于存储节点相互备份性能甚至低于单节点,无法承载2W+的IOPS;b、程序Bug,程序切换场景代码存在bug,有玩家在服务器出现了一直切换场景的BUG,该BUG给计算节点带来了较高负载;c、最初现象是场景管理器处理业务超时,但不是主要原因,主要是上面问题引起。解决办法:a、扩展存储节点2台为4台,并切换为固态硬盘 ;b、优化场景管理器,以阵营为一个单元进程在集群的各个节点当中;c、增加中控,保证在遇到其他问题时可以立即开辟一个新的集群容纳新来的玩家(主要是最近登录服务器和充值消息的转发)。 2、任务结构调整之前任务产生的计算量以及数据库访问频率较高,造成较多性能开销,做了如下优化P28最初设计参照传奇挂机,任务数量为20个左右,所以直接在后端构建任务系统;但后面任务数量一度扩展到500个左右,纯后端构建任务和成就系统,监听了游戏中所有的行为负载较高,且多次读取数据库造成了较高的数据库IO和进程等待,所以重构了任务系统,前端推动和检测,后端验证结果CPU降低了不少。 3、缓存的充要条件Erlang的缓存机制最常用的有Ets和进程字典,并依附于某个业务进程中,但分布式设计中需要尽量解耦,功能之间的联系除了代码的直接调用就是数据之间的关系处理,我们希望的是节点尽量是无状态的。数据尽量不缓存于业务节点,处理不当将会直接影响节点的可伸缩性和安全性;但是有些情况有必须缓存比如,P28包裹产出和消耗巨大,给数据库带来了较高负载,不得不重新考虑业务的实时性,将包裹缓存至会话进程每5分钟检查是否有变化,有则存储否则退出时存储一次。 4、数据散列设计带来的问题P28之前包裹、任务容器都是做的散列结构,一张索引表一张具体的物品表,优点是:单独获取和操作指定装备比较便捷,一条数据变动产生的文件偏移量变化小IO少,最危险的就是数据断键,造成索引和数据不一致,会导致很多垃圾数据的产生;在P28上的问题是,游戏产出和消耗装备速度非常快,所以玩家进行熔炼N件的时候需要N次读取和写入,我们即不能散列存放,因为操作频繁;又不能整行存放,因为一件物品变化就会读写整个包裹数据。所以,后面将包裹数据缓存到会话进程上,数据结构用整行存储,因为频率变低了。 5、计算层和网关层的分层考虑P28的网关节点和计算节点的业务是在一起的,而P25则是分开的,我们的原因在于:a、负载问题:计算节点的均衡负载需要自行实现zm_pid_dist的回调,回调函数的实现不受保障;b、网络问题:玩家受场景推动持续产生战斗行为和奖励等事件,通信率高,所以场景被设计在了网关层;c、业务问题:除了场景没有较为密集高频率的计算以及大数据自动分析管理的需求。所以我们让网关层和计算层运行在同一个节点上,业务交叉也方便些。 6、增加进程指定规则创建机制,原因在于:P28有较多交互类活动是在不同进程运行但又需要数据共享,比如多张地图同时阵营活动;在正常情况下zm_pid_dist出来的进程会分裂到各个计算节点,可是我们希望同一个阵营的地图进程创建到一台物理机上,减少血量,伤害,排名等数据的跨节点同步;比如在某个军团挑战军团BOSS的时候,我们又希望同一个军团创建出多张军团BOSS的地图也在同一个节点上。虽然提出了这个机制,其实也有它的应用面,但是我们做这个解决方案是错误的。血量同步,数据同步最好还是放数据库的内存表,可以和策划沟通战斗的即时性问题,血量1s同步一次就解决了这个高频问题,所以这是一个过度开发。 7、排行榜,P25之前遇到的排行榜问题性能问题1、之前实时排行榜采用的是分段处理保存,但是使用的是文件数据库来保存,效率低,对于排行数据变化频率比较高的那种排行榜,io的开销比较大;2、目前的优化,目前优化后采用的是内存榜,在服务器初始化时通过排行榜管理进程初始化排行榜,然后排名的更新等都在内存中操作,降低了io的开销,提高了更新和获取排行榜数据的效率。而P28这边的排行榜一开始就是以服务器为单位的进程存在集群的不同节点上。 8、P25大地图的设计之前大地图采用的是滑到某块区域去获取数据库的数据,滑动快获取信息多,很容易造成性能瓶颈。目前采用的场景进程的处理方案,在滑到某块区域后获取场景进程中管理的数据。 9、其他建议a、唯一性提前控制,建议集群分段可采用cookie分段,在获取唯一ID时不同集群采用各自分段ID,否则平台BI将会出现相同数据;b、大区概念,P28集群到现在仍然没有大区概念,只有服务器概念,服务器中有对应的渠道则该渠道玩家就可以进入。因为P28有阵营概念就去掉了大区,但后面运营的时候对于服务器管理产生了诸多影响,比如自动合服的时候不能识别是不是同一个大区的,采用相同渠道号的服务器作为同一个大区进行相关业务处理的;c、场景计算,每帧推动的时候会选择目标,选择技能,根据战斗者属性计算伤害,可以考虑伤害计算好一次后不再计算,后续继续采用这个数值。
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
我们使用了两个后端框架分别使用在两个MMORPG游戏上面,用户量级都是几百万的,且把这两个框架定义为 二代框架和三代框架;这里主要记录两款不同框架用户数据存放及使用的问题,为后续开发做好借鉴作用。一、二代框架二代框架采用的是Java语言编写,分中控,认证节点,IM节点,存储节点,网关计算节点,日志节点。后续扩展了全服结构的存储计算一体的 业务节点。随玩家数量增加,网关和计算节点可以在一定量情况下扩展,到达一定量的时候就需要重新分配一个大区机组。二代引擎的用户数据是存放在MySQL中的,玩家登录,数据会从数据库打捞至存储节点(也叫数据中心,简写DC),DC主要负责数据定时落地,先存放至本地磁盘30分钟后写入数据库,用户在DC上的数据结构是未展开的二进制数据。数据打捞至DC后,玩家正式登录的时候会从DC把数据拉到 网关计算节点(我们把它叫做DS),DS负责游戏的所有业务进行,DS和DS之间没有交叉性业务。更多是通过DC管理并转发(三代框架优化了这里),DS的用户二进制数据5分钟就会传一次至DC。二、三代框架三代框架采用的是Erlang构建的集群,理论上数据库和计算节点可以随数据量及业务量的增加无限扩展。它的构成为数据层,计算层,网关层,日志层,我们已经没有指定节点业务执行的情况了,集群允许你指定任何物理机执行指定层的业务。在使用这个框架构建第二个应用时,用户数据的组织没有组织好,反而暴露了很多对这个框架使用的局限
作者:gohuge 发表于
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
https://blog.csdn.net/gohuge/article/details/
一、Socket消息模式Erlang的socket有3种消息接收模式:active、passive和active once;可以在gen_tcp:connect/3或gen_tcp:listen/2里设置{active, true | false |once}来实现,也可以用inet:setopts/2来动态设置。这3种模式的区别是:1. active(主动消息接收):非阻塞。当数据到达时系统会向控制进程发送{tcp, Socket, Data}消息。控制进程无法控制消息流;2. passive(被动消息接收):阻塞。控制进程必须主动调用recv()来接收消息。可以控制消息流;3. active once(混合消息接收):半阻塞。这种模式是主动的,但仅针对一个消息在控制进程收到一个消息后,必须显式调用inet:setopts(Socket, [{active, once}])来重新接收下一个消息。可以进行流量控制。这种模式相对于被动模式来说,优点是可以同时等待多个socket的数据。二、数据封包处理1、{active, false} 方式通过 gen_tcp:recv(Socket, Length) -& {ok, Data} | {error, Reason} 来接收。2、{active, true} 方式以消息形式{tcp, Socket, Data} | {tcp_closed, Socket} 主动投递给线程。第一种方式:gen_tcp:recv/2,3,如果封包的类型是{packet, raw}或者{packet,0},就需要显式的指定长度,否则封包的长度是对端决定的,长度只能设置为0。如果长度Length设置为0,gen_tcp:recv/2,3会取出Socket接收缓冲区所有的数据第二种方式:缓存区有多少数据,都会全部以消息{tcp, Socket, Data} 投递给线程。3、socket的选项里面的{packet,0}和{packet,raw}的区别{packet,2} erlang处理2字节大端包头;{packet,4} erlang处理4字节大端包头;{packet,0} erlang不负责拆包,用户自己处理;{packet,raw} erlang不负责拆包,用户自己处理,t可以处理icmp之类的特殊包。4、粘包处理当client在极短的时间内发送多个包给server,这时server在接收数据的时候可能发生连包问题,就一次性接收这几个包的数据,导致数据都粘连在一起。自己处理粘包的时候,使用{active, N}(还没有到被动模式)和{active, true}选项的时候,在handle_info({tcp,Socket,Data}里面需要好好处理下粘包,三、WS帧数据0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-------+-+-------------+-------------------------------+|F|R|R|R| opcode|M| Payload len | Extended payload length ||I|S|S|S| (4) |A| (7) | (16/64) ||N|V|V|V| |S| | (if payload len==126/127) || |1|2|3| |K| | |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +byte1 (1)Fin代表数据是否结束,WebSocket会把较大的数据分成片发送,最后一片数据的Fin为1,代表数据片完结 (2)RSV1-RSV3是保留为,一般为0 (3)最后4bit代表Opcode,OpCode用来指示数据帧的类型。WebSocket的帧分为两大类型,数据帧和控制帧。 0x0 代表连续帧,也就因为这该帧数据是分片数据中的一片,并不是完整数据 0x1 代表数据是文本内容 0x2 代表数据时二进制流 0x3-0x7 保留给日后的非控制帧使用 0x8 代表该数据时一个关闭命令通知(下面会解释关闭) 0x9 代表Ping帧(同样下面会解释Ping) 0xA 代表Pong帧 0xB-0xF 保留给日后的控制帧使用byte2 (1)Mask代表发来的帧中的

我要回帖

更多关于 新手机怎样注册支付宝 的文章

 

随机推荐