UiStatus 一个简单且强大的Android Ui视图状态控制库. 我一直觉得程序员就像诗人一样,敲代码就像写诗,好的代码何尝不是一首优美的诗!
所以我把项目中的视图控制部分抽取出来,单独封装成了UiStatus这个库。希望可以帮助大家简化这部分操作,也希望对大家有用。
部分效果演示
优点:
1.轻量:简单且够用!!!
2.省内存:使用ViewStub,所有未使用到的状态均不会初始化,减少视图初始化开销。
3.解耦、封装:降低各种视图状态和业务层耦合,使用者只需要关心业务层逻辑而无需知道视图状态管理内部逻辑。
4.自由:不提供任何状态视图,高度可配置,完全由开发者自己定义。
5.强大:可以使用在任何Activity、Fragment、View当中。
目前该库包含的状态有9种:
常量名称 含义 使用场景
LOADING | 加载中 | 页面加载中状态 NETWORK_ERROR | 网络错误 | 当网络连接错误时展示的界面 |
---|
LOAD_ERROR | 加载失败 | 接口请求失败时展示给用户的界面 | EMPTY | 空布局 | 当获取的数据为空时展示的界面 | NOT_FOUND | 未找到内容布局 | 有时获取的内容已被服务器删除,此时展示给用户一个404内容被删除的界面 | CONTENT | 内容 | 真正需要展示给用户的内容视图,也是开发者在layout里实际布局的视图 | WIDGET_NETWORK_ERROR | 网络错误小部件 | 一个类似于QQ、微信断网时顶部显示的网络错误提示视图 WIDGET_ELFIN | WIDGET_FLOAT 小精灵(提示布局) | 底部Float 一种顶部出现的提示布局,可以做类似于简书刷新时提醒推荐内容条数的小部件 | 一种底部弹出的浮动布局,可以实现底部弹出小组件的功能 | 在这里我们将前6种状态称之为普通状态视图,后面3种统称为Widget小部件。下文不再赘述! 其中,普通状态视图不会同时显示,Widget小部件状态的显示和前6种状态不冲突(可以同时显示), 视图层次上Widget在普通状态视图之上,WIDGET_ELFIN在WIDGET_NETWORK_ERROR之下。 使用步骤 1.添加依赖 step one: allprojects { repositories { maven { url 'https://www.jitpack.io' } } } step two : dependencies { implementation 'com.github.FengChenSunshine:UiStatus:{version}' } 2.全局配置 UiStatus库不提供任何状态的视图,完全由开发者自己自定义提供。 ①.获取全局视图控制管理者. UiStatusManager.getInstance() ②.配置状态视图. /** * @params uiStatus 视图状态. * @params layoutResId 开发者自定义的该状态视图. */ addUiStatusConfig(@UiStatus int uiStatus, @LayoutRes int layoutResId); /** * @params uiStatus 视图状态. * @params layoutResId 开发者自定义的该状态视图. * @params retryTriggerViewId 该状态视图上点击事件的触发控件id. * @params retryListener 点击事件触发后的回调. */ addUiStatusConfig(@UiStatus int uiStatus, @LayoutRes int layoutResId, @IdRes int retryTriggerViewId, OnRetryListener retryListener); /** * 该方法只针对Widget相关状态有效. * @params uiStatus 视图状态. * @params topMarginPx widget相对于内容视图的顶部距离,比如可能需要预留出顶部Toolbar高度的距离. * @params bottomMarginPx widget相对于内容视图的底部距离,比如可能需要预留出底部导航栏高度的距离. */ setWidgetMargin(@UiStatus @IntRange(from = 7L,to = 9L) int uiStatus, int topMarginPx, int bottomMarginPx); ③.配置网络状态提供者. 考虑到开发者项目中都有自己的网络状态监听、判断的工具类, 所以UiStatus没有必要额外编辑这段功能代码,仅需要开发者配置一个回调即可。 配置网络状态提供者后,在请求显示普通的状态时如果是没有网络那么将自动重定向到NETWORK_ERROR网络错误状态界面。 具体配置方法如下: UiStatusNetworkStatusProvider .getInstance() .registerOnRequestNetworkStatusEvent(OnRequestNetworkStatusEvent networkStatusEvent); 3.具体使用 UiStatus可以使用在任何Activity、View以及Fragment中. ①.Activity中: UiStatusController.get().bind(activity); ②.View中: UiStatusController.get().bind(view); ③.Fragment中: 由于Fragment的特殊性,所以这里稍微麻烦一点点: public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view; //你的视图初始化... mUiStatusController = UiStatusController.get(); return mUiStatusController.bindFragment(view); } 看以看出这里是使用bindFragment(view)方法将原本的内容视图传递给UiStatus,并将该方法返回的控件作为onCreateView的返回值即可。 ④.视图状态切换: 对于普通视图切换使用UiStatusController.changeUiStatusIgnore(uiStatus)切换; 对于Widget视图使用UiStatusController.showWidget(uiStatus)进行显示; 或者使用其重载方法showWidget(uiStatus,duration)方法进行显示,使用该方法时会在duration时间后自动隐藏; 也可以使用UiStatusController.hideWidget()方法进行隐藏Widget小部件。 4.成功 经过上面的步骤之后您已经成功的集成并且可以使用UiStatus库了,并且您肯定也看到了想要看到的各种状态下的视图成功切换。 如果您不需要某些页面的个性化定制,那么到这里就OK了;否则请看下面的步骤会满足你的需求。 5.个性化配置 一般情况下使用在Application中的全局配置已经能满足大部分页面视图状态的需求, 如果某些个别界面需要特殊配置那么UiStatus也是支持的。 只需要使用持有的UiStatusController对象,调用其addUiStatusConfig()或其他任何可以在全局配置中使用的方法重新配置即可。 6.优化 一般的开发者只需要在自定义的Application中全局配置一次即可,并且对于LOADING、LOAD_ERROR、EMPTY、NOT_FOUND、CONTENT这些普通视图的切换可以下沉到 统一在基类Activity、Fragment或者网络加载框架中处理,具体使用大同小异,可参考Demo,这里不再赘述。 7.UiStatus可实现效果部分展示 注:图片来自其他APP,仅供参考实现效果使用。其他未列出效果不代表不能实现,具体可实现效果尽情发挥想象!!!
最后给大家分享一份非常系统和全面的Android进阶技术大纲及进阶资料,及面试题集 想学习更多Android知识,请加入Android技术开发企鹅交流 7520 16839 进群与大牛们一起讨论,还可获取Android高级架构资料、源码、笔记、视频 包括 高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思维导图,和BATJ面试题及答案! 群里免费分享给有需要的朋友,希望能够帮助一些在这个行业发展迷茫的,或者想系统深入提升以及困于瓶颈的朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我在这免费分享一些架构资料及给大家。希望在这些资料中都有你需要的内容。
相比文字和图片,直播提供了人与人之间更丰富的沟通形式,其对平台稳定性的考验很大,那么倡导“ 以技术驱动娱乐 ”的虎牙直播(以下简称“虎牙”)是如何在技术上赋能娱乐,本文将为您介绍虎牙在DNS、服务注册、CMDB和服务配置中心等方面的实践。 文章整理自虎牙基础保障部中间件团队负责人张波(社区ID:zhangjimmy)在Dubbo Meetup 广州站沙龙上的分享,阿里巴巴中间件授权发布,分享议题如下: 为什么选用Nacos DNS-F的技术价值和应用场景 服务注册的实践 CMDB的应用和实践 服务配置的实践 为什么选用 Nacos 虎牙关注 Nacos 是从v0.2 开始的( 最新版本:Pre-GA v0.8 ),我们也参与了社区的建设,可以说是比较早期的企业用户。 Nacos 是一个更易于帮助构建云原生应用的动态服务发现、配置和服务管理平台,提供「注册中心」、「配置中心」和「动态DNS服务」三大功能。 首先,在虎牙的微服务场景中,起初有多个注册中心,每一个注册中心服务于某一部分微服务,缺少一个能融合多个注册中心,并把他们逐一打通,然后实现一个能管理整个微服务体系的大的注册中心。 以下内容摘自我们考虑引入Nacos时,在服务注册中心方案上的选型对比: Nacos提供 DNS-F功能, 可以与K8S、Spring Cloud和Dubbo等多个开源产品进行集成,实现服务的注册功能。 其次,在服务配置中心方案的选型过程中,我们希望配置中心和注册中心能够打通,这样可以省去我们在微服务治理方面的一些投入。因此,我们也同步比较了一些服务配置中心的开源方案: 例如Spring Cloud Config Server、Zookeeper和ETCD,总体评估下来,基于我们微服务体系现状以及业务场景,我们决定使用Nacos作为我们的服务注册和服务发现的方案。使用过程中,我们发现,随着社区版本的不断更新和虎牙的深入实践,Nacos的优势远比我们调研过程中发现的更多,接下来,我将围绕DNS-F、Nacos-Sync、 CMDB和负载均衡4方面来分享虎牙的实践。 DNS - F 的技术价值 Nacos 提供的DNS-F功能的第一个技术价值在于,弥补了我们内部微服务没有一个全局动态调度能力的空白。 刚才提到,虎牙有多个微服务体系,但并没有一个微服务具备全局动态调度的能力,因为它们各自都是独立的。目前,我们通过Nacos已经融合了四个微服务体系的注册中心,最终目标是把所有的微服务都融合在一起,实现全局动态调动的能力。 第二,DNS-F解决了服务端端到端面临的挑战,即延时大、解析不准、故障牵引慢的问题。 如何去理解呢? 当内部有多个微服务体系的时候,每一个体系的成熟度是不同的。例如,有一些微服务框架对同机房或CMDB路由是不支持的,当一个服务注册到了多个IDC中心,去调用它的服务的时候,即便是同机房,也可能调用到一个不是同机房的节点。这样就会无端的造成服务的延时和解析不准。 即使我们基于DNS做一些解析的优化,但仍然无法完全解决服务的延时和解析不准。这是因为DNS都是IP策略的就近解析,无法根据服务的物理状态、物理信息进行路由。此外,当一个核心服务出现问题,如果缺少一个融合了多个调用方和被调用方的信息的统一的注册中心,就很难去准确判断如何去牵引,从而导致故障牵引慢。有了Nacos后,就可以接入一个统一的注册中心以及配置中心,去解决这些问题。(目前,虎牙还在微服务体系的改造过程中,未完全实现统一的注册中心) 第三,提供专线流量牵引能力。 虎牙的核心机房的流量互通,是使用专线来实现的。专线的特性就是物理建设的,而且我们的专线建设可能不像BAT那么大,例如我们专线容量的冗余只有50%,假设某个直播异常火爆,突发流量高于平常的两百倍,超过了专线的建设能力,这时候一个服务就有可能会导致全网故障。但是,通过全局的注册中心和调动能力,我们就可以把流量牵引到其他地方,例如迁移到公网,甚至牵引到一个不存在的地址,来平衡一下。即便某个服务出现问题,也不会影响我们的全局服务。 第四,支持服务端的多种调度需求 ,包括同机房路由、同机器路由,以及同机架路由,Nacos都可以去做适配。此外,基于Nacos 的DNS-F功能,我们还实现了加速外部域名解析和服务故障牵引秒级生效。 DNS - F 的应用场景 这张图是Nacos DNS-F的一个具体实现,实际上是拦截了OS层的DNS请求。如果经过DNS的域名是内部服务,它就会从Nacos Server 获取结果,如果不是,就会转发到其它的LocalDNS进行解析。 以数据库高可用的应用场景为例,我们的数据库切换效率比较低,依赖业务方修改配置,时效不确定,通常需要10分钟以上(备注:我们的数据库实际上已经实现了主备的功能,但当一个主服务出现问题的时候,总是要去切换IP。)切换IP的过程中,依赖运维和开发的协作,这是一个比较长的过程。 引入DNS后,当主出现问题的时候,就可以很快的用另外一个主的IP来进行替换,屏蔽故障,而且节点的故障检测和故障切换都可以自动完成,并不依赖运维和开发的协作,节省了时间。当然,这个场景的解法有很多,比如说使用MySQL - Proxy也可以去解这个问题,但我们的MySQL - Proxy还在建设中,想尽快的把这个问题解决,所以采用了DNS的方式。 下面我们再着重分享下基于DNS-F对LocalDNS的优化。虎牙还没有去建设自己的LocalDNS,大部分使用的是一些公共的DNS,大致有以下这些组成。 这种组成方式会存在一个问题。假设服务突然一下崩溃后,之后服务又马上正常了,这种情况我们无法重现去找到崩溃原因。因为很多场景下,是一个公共DNS的请求超时导致的,甚至一个解析失败导致的,在那一刻,因为无法保留现场的,所以就发现不了问题。 以我们的监测数据来看,DNS解析错误的比例达到1‰左右,超时比例将更高。意思是在使用公共DNS的情况下,服务有1‰的几率是会超时或失败,如果服务没有做好容错,就会出现异常。同时,一些公共DNS解析的延时都是不定的,比如在亚马逊上一些比较不好的节点,它的延时会比较高,平均超过三四十毫秒。 然后我们基于DNS-F对LocalDNS做了一些优化。优化结果如下: 平均解析时间从之前的超过两百毫秒降低到两毫秒以下; 缓存命中率从92%提升到了99%以上; 解析失败率之前是1‰,现在基本上没有了。 优化的效果也体现在我们的风控服务上,平均延迟下降10ms,服务超时比例下降25%,降低了因延迟或服务超时导致的用户上传的图片或文字违规但未被审核到的风险。 服务注册的实践 虎牙的核心业务是跑在Tars上的。 Tars:腾讯开源的一款微服务框架。 Tars主要是支持C++,但对Java、PHP等开发语言的支持力度比较差,这就使得我们非C++的业务方去调用它就会很别扭。引入Nacos以后,我们通过Nacos支持的DNS协议来实现服务发现过程中对全语言的支持。 当然,Nacos不只是一个注册中心,它具备了融合多个数据中心的能力,支持多数据源的同步,例如,我们目前已经支持了Taf(虎牙内部的一个重要微服务体系)、Nacos自身、ZooKeeper、以及K8S上一些服务注册的同步。 同时,基于Nacos集群的双向同步功能(Nacos-Sync),我们实现了国内的两个可用区,以及国外的多个可用区之间的数据值同步,最终实现了一处注册、多地可读。 Nacos-Sync是事件机制,即同步任务通过事件触发,可以灵活地开启和关闭你要同步的任务,然后根据服务变化事件触发监听,保证实时性,最后通过定时的全量突发同步事件,保证服务数据的最终一致。同时,Nacos-Sync也支持服务心跳维持,即多个数据中心的心跳,可以使用Nacos-Sync代理要来实现远端同步。此外,也支持心跳与同步任务绑定,便于灵活控制。 由于Taf上有数万个注册服务,同步的量特别大,所以我们在Nacos-Sync做了一些改造,通过任务分片来实现数万服务同步的可用性保障。改造步骤是先以服务为粒度定义任务,然后在多个分片上分散任务负载,最后以单分片多副本来保证任务可用性。 对接 CMDB,实现就近访问 在服务进行多机房或者多地域部署时,跨地域的服务访问往往延迟较高,一个城市内的机房间的典型网络延迟在1ms左右,而跨城市的网络延迟,例如上海到北京大概为30ms。此时自然而然的一个想法就是能不能让服务消费者和服务提供者进行同地域访问。 Nacos定义了一个SPI接口,里面包含了与第三方CMDB约定的一些方法。用户依照约定实现了相应的SPI接口后,将实现打成Jar包放置到Nacos安装目录下,重启Nacos即可让Nacos与CMDB的数据打通。 在实际的落地过程中,我们是在DNS-F接入Taf,在DNS-F上实现Taf的中控接口,无缝对接Taf的sdk。DNS-F提供缓存负载均衡和实例信息,Nacos则提供负载均衡信息的查询接口。 服务配置的实践 虎牙的域名(www.huya.com)会接入华南、华中、华北多个IDC机房,每个机房都会建设一个Nginx去做负载均衡,经过负载均衡的流量会通过专线返回到我们的后端服务器上。在这个过程中,如果我们去修改一个在中间的配置,需要下发到多个机房的上百个负责负载均衡的机器上,如果出现配置下发不及时,或下发配置失败,极大可能会出现故障,同时,负责均衡服务的机器对弹性能力的要求较高,在业务高峰如果不能快速扩容,容易出现全网故障。 传统的配置下发方式是通过服务端下发文件更新配置,更新配置生效时间长,由于需要预先知道负责均衡集群的机器信息,扩缩容需要等元信息同步以后才能接入流量,扩容流量的接入时间较长。 引入Nacos后,我们采用了配置中心监听方式,通过客户端主动监听配置更新,配置便可秒级生效,新扩容服务主动拉取全量配置,流量接入时长缩短3分钟+。 虎牙对 Nacos 改造和升级的总结 引入Nacos的过程中,我们所做的改造和升级总结如下。 一是在 DNS-F 上,我们增加了对外部域名的预缓存的支持,Agent的监控数据对接到公司的内部监控,日志输出也对接到内部的日志服务,然后和公司的CMDB对接,并实现了DNS-F Cluster集群。我们之所以去构建一个DNS-FCluster集群,是为了避免内存、硬盘或版本问题导致的DNS服务无效,有了DNS-F Cluster集群,当本地Agent出现问题的时候,就可以通过集群去代理和解析DNS请求。 二是在 Nacos-Sync 上,我们对接了TAF注册服务和K8S注册服务,以及解决了多数据中心环形同步的问题。 三是在 Nacos CMDB 上,我们对Nacos CMDB进行了扩展,对接了虎牙自己的CMDB,并对接了内部的负载均衡策略。 原文链接 本文为云栖社区原创内容,未经允许不得转载。 一、打开Egret Launcher安装Egret 5.3.2引擎。 进入白鹭引擎GitHub(https://github.com/egret-labs/egret-core)下载Egret5.3.2,解压放到某个目录(如D:\Program Files\Egret),然后将egret-core-5.3.2目录拖拽到“安装本地版本”,进行自动安装。 在命令行执行 egret versions,如果安装引擎列表里包含 5.3.2 则代表安装成功: 二、打开一个终端,执行 npm install @egret/paper-cli -g 安装命令行工具。 我直接进入项目目录,D:\EgretProjects 三、创建一个新项目 paper create HelloWorld3D 四、将项目导入到Egret Launcher,使用Wing打开进行操作,提示自动创建编译、调试和资源配置参数点确定。
五、项目安装最新3D库 在Wing的终端执行 paper install egret3d 安装最新3D库。 安装完成后,左侧目录会出现egret3d文件夹。 六、安装最新的 Egret3d 可视化调试库 在Wing的终端执行 paper install inspector 安装最新的Egret3d可视化调试库,完成后执行egret clean。 七、烘焙资源 在Wing的终端中执行 egret bake 烘焙资源。 八、构建项目 九、调试运行项目 一、介绍 JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于白盒测试和回归测试。 白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人 员是公开的; 回归测试:软件或环境修复或更正后的再测试; 单元测试:最小粒度的测试,以测试某个功能或代码块。一般由程序员来做,因为它需要知道内部程序设计和编码的细节; JUnit GitHub地址: https://github.com/junit-team 二、JUnit使用 开发环境: Spring Boot 2.0.4 RELEASE JUnit 4.12 Maven IDEA 2018.2 2.1 检测JUnit依赖 如果是Spring Boot项目默认已经加入了JUnit框架支持,可在pom.xml中查看: org.springframework.boot spring-boot-starter-test test 如果Maven项目中没有添加JUnit依赖,可参照如上代码,手动添加。 2.2 基础使用 简单的测试代码如下: @RunWith(SpringRunner.class) @SpringBootTest public class SimpleTest { @Test public void doTest() { int num = new Integer(1); Assert.assertEquals(num, 1); } } 在测试类中邮件运行项目,效果如下: 从控制台可以看出测试通过了。 2.3 注解说明 2.3.1 注解列表 @RunWith:标识为JUnit的运行环境; @SpringBootTest:获取启动类、加载配置,确定装载Spring Boot; @Test:声明需要测试的方法; @BeforeClass:针对所有测试,只执行一次,且必须为static void; @AfterClass:针对所有测试,只执行一次,且必须为static void; @Before:每个测试方法前都会执行的方法; @After:每个测试方法前都会执行的方法; @Ignore:忽略方法; 2.3.2 超时测试 代码如下,给Test设置timeout属性即可,时间单位为毫秒: @Test(timeout = 1000) 2.4 断言测试 断言测试也就是期望值测试,是单元测试的核心也就是决定测试结果的表达式,Assert对象中的断言方法: Assert.assertEquals 对比两个值相等 Assert.assertNotEquals 对比两个值不相等 Assert.assertSame 对比两个对象的引用相等 Assert.assertArrayEquals 对比两个数组相等 Assert.assertTrue 验证返回是否为真 Assert.assertFlase 验证返回是否为假 Assert.assertNull 验证null Assert.assertNotNull 验证非null 代码示例如下: @Test public void doTest() { String[] string1 = {"1", "2"}; String[] string2 = string1; String[] string3 = {"1", "2"}; Assert.assertEquals(string1, string2); Assert.assertEquals(string2, string3); Assert.assertSame(string1, string2); Assert.assertSame(string2, string3); //验证不通过,string2、string3指向的引用不同 } 2.5 Web模拟测试 在Spring Boot项目里面可以直接使用JUnit对web项目进行测试,Spring 提供了“TestRestTemplate”对象,使用这个对象可以很方便的进行模拟请求。 Web测试只需要进行两步操作: 在@SpringBootTest注解上设置“ebEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT”随机端口; 使用TestRestTemplate进行post或get请求; 示例代码如下: @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class UserControllerTest { @Autowired private TestRestTemplate restTemplate; @Test public void getName() { String name = restTemplate.getForObject("/name", String.class); System.out.println(name); Assert.assertEquals("Adam", name); } } 其中getForObject的含义代表执行get请求,并返回Object结果,第二个参数设置返回结果为String类型,更多的请求方法: getForEntity:Get请求,返回实体对象(可以是集合); postForEntity:Post请求,返回实体对象(可以是集合); postForObject:Post请求,返回对象; 2.6 数据库测试 在测试数据操作的时候,我们不想让测试污染数据库,也是可以实现的,只需要添加给测试类上添加“@Transactional”即可,这样既可以测试数据操作方法,又不会污染数据库了。 示例代码如下: @Test @Transactional public void saveTest() { User user = new User(); user.setName("Adam"); user.setAge(19); user.setPwd("123456"); userRepository.save(user); System.out.println("userId:" + user.getId()); Assert.assertTrue(user.getId()>0); } 执行效果如下: 我们可以看到Id有了,也测试通过了,说明数据是添加是正常的,但查看数据库发现数据里面是没有这条数据的。 如果把“@Transactional”去掉的话,数据库就会正常插入了。 2.7 Idea快速开启测试 在Idea里面可以快速的添加测试的方法,只需要在要测试的类里面右键选择“GoTo”点击“Test”,选择你需要测试的代码,点击生成即可,如果是Windows 用户可以使用默认快捷键“Ctrl + Shift + T”,效果如下图: 选完方法之后,点击OK按钮,就生成了对应的测试代码,用户只需要完善框架里面的具体测试逻辑就可以了。 iOS Xcode 调试OpenGL shader 步骤详解 注意Apple的官方文档详细的描述了要使用GPU Frame Capture功能,必须使用EAGLContext作为上下文,而且要使用CAEAGLLayer或者GLKView,同时还要保证使用定时器等,去循环调用 [_context presentRenderbuffer:GL_RENDERBUFFER];//方法 否则不能使用GPU Frame Capture功能。 当不能使用GPU Frame Capture功能时,此按钮是灰色不可点击状态 下面介绍调试步骤: 首先设置Options里面的GPU Frame Capture为OpenGL ES默认为Automatically Enabled默认设置其实也可以调试,但是此处为了过程更符合OpenGL ES的流程,此处设置为OpenGL ES。 然后运行程序 运行过程中点击Capture按钮捕获一帧OpenGL ES数据。捕获过程较慢。 捕获完毕之后:
但是到目前并不能看到shader的内容,已经shader内的变量的值,调试shader变量的值才是我们的目的。
在选择All GPU Objects -> All 之后调试shader的界面就出现了调试OpenGL ES shader的界面为: 此处如果想一边查看frameBuffer一边调试OpenGL ES shader可以这样操作: 界面变为: 下面打开frameBuffer界面: 点击后我们想要的界面就出现了一边为frameBuffer界面一边为调试OpenGL ES shader的界面 下面查看OpenGL ES在渲染此帧时shader内的各个变量值是多少 此处以查看shader的变量的值为示例,双击Vertex Array Objective #0 shader 内的变量的值为: 使用的shader为:
异常为: 此异常是为了测试调试信息故意这样写的,去掉此处的异常只需要将GLfloat jeffasdColors[]改为: //GLfloat jeffasdColors[] = { 1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f,1.0f }; 至于原因估计属性OpenGL ES的都清除此处不再解释。 萌布猫咪是我跟我妹的基于兴趣做出的作品,我负责产品设计、技术实现还有部分UI设计工作,我妹主要是原画设计。 「萌布猫咪」是一款非常萌跟好玩的猫咪收集游戏,可以放置猫咪送的宝物和制作甜点,宝物甜点会吸引新的猫咪过来玩,每只猫咪都有一段暖心的小故事喔。 实现技术通过cocos2dx 更多详情可以到官网下载体验 萌布猫咪官网 萌布猫咪安卓版下载 github地址 欢迎大家提建议~
一款游戏会用到很多图片资源,通常我们会使用TexturePacker工具进行图片的拼接、压缩,为了考虑性能问题,单个SpriteSheet的尺寸不会设置的太大(最大1024 * 1024),这样就可能会有多个SpriteSheet。每当有图片资源需要更新时,操作起来会有点不方便。所以希望能通过bat来批量重新生成新的SpriteSheet 首先我们需要获取TexturePacker的安装目录,一般我们都会安装TexturePackerGUI工具,它实际运行起来还是调用TexturePacker.exe工具。因为软件环境因人而异可能存放的目录各不相同,这里通过读取注册表相关信息来避免此问题 @echo off echo "获取TexturePackerGUI注册表安装信息..." & echo. setlocal enabledelayedexpansion for /f "skip=4 tokens=1,* delims=REG_SZ " %%a in ('reg query "HKEY_CLASSES_ROOT\Applications\TexturePackerGUI.exe\shell\Open\command" /ve') do ( set "str=%%b" echo !str! & echo. set "str=!str:"%%1"=!" echo TexturePackerGUI的安装路径:!str! & echo. for /f "tokens=1,* delims=" %%i in (!str!) do ( set "str=%%i" set "TexturePacker_Path=!str:TexturePackerGUI.exe=!TexturePacker.exe" echo !TexturePacker_Path! & echo. ) ) pause 在dos窗口下执行查询指令,得到结果如下图所示: 实际注册表的相关信息: 我们会发现,返回的结果中,前4行是无用的信息。然后以“REG_SZ ”作为分隔符进行字符串分隔。得到包含空格的路径信息:"G:\Program Files\TexturePacker\bin\TexturePackerGUI.exe" "%1" 通过set指令替换掉%1 set "str=!str:"%%1"=!" 然后通过for /f "tokens=1,* delims=" %%i in (!str!) do () 来去除””及空格信息 之后就直接调用了TexturePacker.exe echo "TexturePacker安装路径:%TexturePacker_Path%" & echo. "%TexturePacker_Path%" batch_btn.tps "%TexturePacker_Path%" batch_kuang.tps "%TexturePacker_Path%" batch_left.tps "%TexturePacker_Path%" batch_spt.tps "%TexturePacker_Path%" batch_text.tps "%TexturePacker_Path%" batch_expression.tps 下面附几张Texture Packer 的常用设置截图, 通常情况下,我们导出的纹理图片是RGBA8888格式,R、G、B、A各需要占用8个比特(1字节),所以一个像素总共需要4个字节表示。通常我们使用RGBA444来保存图片,不过需要注意设置一个参数来保证压缩后的图片尺寸大小不会变化。勾选“Allow free sizes”,否则使用导出的图片后界面会错位(因为空白区域被裁剪掉了>_<) 使用RGBA4444还需要注意的一个问题,就是选择压缩算法(抖动+通道) texturepacker官网上有相应的对比图>> 在Command Line中使用TexturePacker 使用TexturePacker批量生成SpriteSheet bat批处理 教程 (译)在cocos2d里面如何使用Texture Packer和像素格式来优化spritesheet 时间20180508,使用的unity版本2017.2,unity中的.net 4.6平台还是不稳定版本。 示例代码地址: https://github.com/hiramtan/HigRPC_unity 估计还不会有人用在商业手游项目中,原因: .net 4.6还是实验性版本,并不能保证稳定性。 编译可用的gRPC类库到移动平台。 需要验证在Android iOS平台上所有逻辑的稳定性。 在编辑器中使用流程如下: How to use in unity 设置 .net 4.6 导入 gRPC dll Google.Protobuf.dll Grpc.Core.dll grpc_csharp_ext.x64.dll System.Interactive.Async.dll 创建测试逻辑. Extention 用户自定义协议及代码生成 Grpc.Tools.1.11.x\tools\windows_x86\protoc.exe -I../../protos --csharp_out Greeter --grpc_out Greeter ../../protos/helloworld.proto --plugin=protoc-gen-grpc=packages/Grpc.Tools.1.11.x/tools/windows_x86/grpc_csharp_plugin.exe Note: Haven't test on mobile platform 点击链接加入QQ群【83596104】: https://jq.qq.com/?_wv=1027&k=5l6rZEr 一:多进程 import os import time from multiprocessing import Process def func ( args , args2 ) : print ( args , args2 ) #打印参数 time.sleep( 3 ) print ( '子进程 :' , os.getpid()) #查看子进程进程号 print ( '子进程的父进程 :' , os.getppid()) #查看子进程的父进程,多了一个p print ( 12345 ) if __name__ == '__main__' : p = Process( target = func , args = ( '参数' , '参数2' )) # 注册 # p是一个进程对象,还没有启动进程 p.start() # 开启了一个子进程 print ( '*' * 10 ) print ( '父进程 :' , os.getpid()) # 查看当前进程的进程号 print ( '父进程的父进程 :' , os.getppid()) # 查看当前进程的父进程 # 进程的生命周期 # 主进程 # 子进程 # 开启了子进程的主进程 : # 主进程自己的代码如果长,等待自己的代码执行结束, # 子进程的执行时间长,主进程会在主进程代码执行完毕之后等待子进程执行完毕之后 主进程才结束 打印结果: 先打印主进程,然后打印子进程。主进程结束执行子进程。 ********** 父进程 : 3832 父进程的父进程 : 2556 参数 参数2 子进程 : 5416 子进程的父进程 : 3832 12345 二:多进程的中的几个方法(多进程即可指子进程,父进程这两个进程,也可指多个实例化两个进程) import time from multiprocessing import Process def func ( arg1 , arg2 ) : print ( '*' * arg1 ) time.sleep( 5 ) print ( '*' * arg2 ) if __name__ == '__main__' : p = Process( target = func , args = ( 10 , 20 )) p.start() print ( 'hahahaha' ) # p.join() # 是感知一个子进程的结束,将异步的程序改为同步 print ( '====== : 运行完了' ) 这样就是正常打印,主进程执行完毕执行子进程 hahahaha ====== : 运行完了 ********** ******************** 如果加上join之后, 是感知一个子进程的结束,将异步的程序改为同步。也就是等待子进程结束后主进程关闭 打印结果: hahahaha ********** ******************** ====== : 运行完了 如果子进程是一个while循环的死循环,那么主进程就一直不会关闭。 import os import time from multiprocessing import Process def func () : while True : print ( '子进程开始' ) time.sleep( 1 ) print ( '***我还在运行' ) if __name__ == '__main__' : p = Process( target = func) p.daemon = True p.start() p.join() print ( '程序结束' ) 打印结果: 子进程开始 ***我还在运行 子进程开始 ***我还在运行 总是在子进程这里循环 小总结: process小知识: Process([group [ , target [ , name [ , args [ , kwargs]]]]]),由该类实例化得到的对象, 表示一个子进程中的任务(尚未启动) 强调: 1. 需要使用关键字的方式来指定参数 2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号 参数介绍: 1 group参数未使用,值始终为None 2 target表示调用对象,即子进程要执行的任务 3 args表示调用对象的位置参数元组,args = ( 1 , 2 , 'egon' , ) 4 kwargs表示调用对象的字典 , kwargs = { 'name' : 'egon' , 'age' : 18 } 5 name为子进程的名称 进程常见关键字 1 p.start():启动进程,并调用该子进程中的p.run() 2 p.run() : 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 3 p.terminate() : 强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程, 使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁 4 p.is_alive() : 如果p仍然运行,返回True 5 p.join([timeout]) : 主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。 timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 开启多个子进程 import os import time from multiprocessing import Process def func ( filename , content ) : with open ( filename , 'w' ) as f : f.write( content * 10 * '*' ) if __name__ == '__main__' : p_lst = [] for i in range ( 10 ) : p = Process( target = func , args = ( 'info%s' % i , i)) p_lst.append(p) p.start() for p in p_lst : p.join() # 之前的所有进程必须在这里都执行完才能执行下面的代码 print ([i for i in os.walk( r'F:\python10期\day37\day37' )]) # 同步 0.1 * 500 = 50 # 异步 500 0.1 = 0.1 # 多进程写文件 # 首先往文件夹中写文件 # 向用户展示写入文件之后文件夹中所有的文件名 解析:他会在所有进程结束之后才能完成下面的print。 开启多个子进程的第二种方式: import os from multiprocessing import Process class MyProcess (Process) : def __init__ ( self , arg1 , arg2 ) : super (). __init__ () self .arg1 = arg1 self .arg2 = arg2 def run ( self ) : print ( self .pid) print ( self .name) print ( self .arg1) print ( self .arg2) if __name__ == '__main__' : p1 = MyProcess( 1 , 2 ) p1.start() p2 = MyProcess( 3 , 4 ) p2.start() # 自定义类 继承Process类 # 必须实现一个run方法,run方法中是在子进程中执行的代码 这种方式需要第一定义一个类,让他继承process,第二种是传参,需要初始化,然后是定义一个run方法,因为定义run方法之后才能执行start,然后在实例化二个对象。分别执行。 打印结果: 6356 MyProcess-1 1 2 3136 MyProcess-2 3 4 因为此进程是异步,所以process1和process2不确定谁最先打印,随机。super是执行process所有方法。 20180312 ECS编程思想 这种编程思想很早前就已经提出,ECS分别是Entity,Component,System的缩写. Entity是实例,作为承载组件的载体,也是框架中维护对象的实体. Component只包含数据,具备这个组件便具有这个功能. System作为逻辑维护,维护对应的组件执行相关操作. ECS和传统的OOP相比更看重的是组件,附加组件即具备功能. C#是面向对象语言,从语言层面上就支持面向对象的编程思想:集成封装和多态. 面向对象:我是什么,我有属性,我有方法,我要继承然后重写成自己独有的. 面向组件:我有什么,我有组件便具备这个功能,我由很多组件组成了实体. 按照ECS的思路,功能拆分成组件,然后把这些细小的组件组合便组成了具备所需功能的对象.实际上Unity也是这种实现思路,但是它的组件式属性和逻辑的合集. 最近看了ECS编程思想,看了Entitas逻辑,写了小Demo,感觉: 编程思路/框架没有最好只有更合适,可能A框架适合RPG游戏,B框架更适合FPS游戏. Entity的时序 开发中经常会遇到控制时序的逻辑,比如某个Entity必须在另一个Entity逻辑完成后再执行,当然可以通过添加组件标识来控制时序执行,但是如果存在大量这种逻辑势必会增加很多对应的组件作为标识. System时序 同样可以添加Component作为控制标识作为过滤条件,A系统要在B系统结束之后,C系统启动之前,D系统挂起时...开发到后期怎么办.(待机,前摇,蓄能,施法,表现,结束,)太多太多状态一帧/分帧维护起来估计也会乱吧. System更合适维护一组Entity 比如FPS游戏,MOBA游戏用ECS思想拆分合适,每个玩家的角色功能对等(都是人物,都在移动,战斗)不同的角色的技能枪支拆分单独实现. 如果是大型RPG游戏,每个门派角色,大厅npc,商城商人,敌人角色的状态动作各不相同,这种ECS当然能实现,每个功能,拆分成细小组件,创建对应System类维护,但是这样归根到底一个System经过遍历筛选其实只维护了一个Entity上面的组件.因为角色功能独有,以后这个组件也不会被其他Entity组合使用. 功能解耦 功能之间解耦,对应的组件和系统只关心本身的功能,不会对其他功能造成影响. 重构 不方便重构 数据维护 数据分散到对应的组件,比如服务器下发一份最新数据,需要一一维护对应组件的属性,数据收集也是如此. 客户端服务器共享代码 客户端拆分表现组件后,只包含纯逻辑功能. 数据层表现层分离 在不同的组件和系统中维护,不必像oop再拆分表现逻辑. 方便扩展 如果设计合理,可以通过现有组件组合新的Entity. 1- 性能 数据在内存中线性存在,Entity复用. ECS框架:Entitas 基于ECS的框架有很多,Entitas便是其中一个. 官方的ECS也会在2018.1beta版本中推出. 根据wiki很容易就能够搭建起项目,运行起工程,但是有些也需要开发者注意,比如Component作为标识还是作为数据存储;Component生成代码后改变类型或者删除字段后的生成报错,Group/Feature/Unit使用等等. 关于代码生成,它提供两个版本: 免费版 收费版 免费版根据反射生成代码,需要无编译错误下运行;收费版不需要,即使有代码报错也可以生成代码. 自己扩展的代码可以分为两部分: Components 其他代码(System,Service...等其他逻辑) 这样做的好处是重新生成代码时,方便的把这两个目录移除项目(或者StreamingAssets),生成基础代码,然后移回Component代码,然后生成组件代码,然后移回其他代码. Entitas实践 最简单的输出hello world 源码: https://github.com/hiramtan/test/tree/Entitas_Example_HelloWorld 创建一个Component: public class HelloWorldComponent : IComponent { public string s; } 生成代码后创建两个system,一个用来创建entity并附加组件,另一个用来输出日志: public class CreatSystem : IInitializeSystem { private Contexts _contexts; public CreatSystem(Contexts contexts) { _contexts = contexts; } public void Initialize() { var e = _contexts.game.CreateEntity(); e.AddHelloWorld("hello world"); } } public class HelloWorldSystem : ReactiveSystem { private Contexts _contexts; public HelloWorldSystem(Contexts contexts) : base(contexts.game){ } protected override ICollector GetTrigger(IContext context) { return context.CreateCollector(GameMatcher.HelloWorld); } protected override bool Filter(GameEntity entity) { return entity.hasHelloWorld; } protected override void Execute(List entities) { foreach (var e in entities) { Debug.Log(e.helloWorld.s); } } } 上面的代码已经实现了输出功能,但是为了方便管理上面的这两个System,我们添加一个RootSystems专门管理游戏中的各种System: public class RootSystems : Feature { public RootSystems(Contexts contexts) { Add(new CreatSystem(contexts)); Add(new HelloWorldSystem(contexts)); } } 所有的功能都实现了,在unity工程中添加一个入口方法挂载到场景中,Main.cs: public class Main : MonoBehaviour { private RootSystems s; // Use this for initialization void Start() { s = new RootSystems(Contexts.sharedInstance); s.Initialize(); } // Update is called once per frame void Update() { s.Execute(); } } 运行,正常输出,因为输出实现的是reactivesystem,当值发生变化时便会输出. Unity ECS 今天untiy在beta版本中公布了自己的ECS系统,Entity,ComponentData,ComponentSystem,随着unity官方的引入,势必会被推广起来. 点击链接加入QQ群【83596104】: https://jq.qq.com/?_wv=1027&k=5l6rZEr 参考资料 https://github.com/sschmid/Entitas-CSharp https://blog.codingnow.com/2017/06/overwatch_ecs.html http://www.ecsframework.com/ https://youtu.be/OJmVBo5HGOY http://t-machine.org/index.php/category/entity-systems/ http://www.benmutou.com/archives/2421 https://www.cnblogs.com/yangrouchuan/p/7436533.html http://blog.csdn.net/u012632851/article/details/75370836 http://blog.csdn.net/langresser_king/article/details/46324977 http://blog.csdn.net/wuxiaoming1733/article/details/79037812 http://blog.lmorchard.com/2013/11/27/entity-component-system/ http://piemaster.net/2011/07/entity-component-primer/ http://www.richardlord.net/blog/ecs/what-is-an-entity-framework.html http://blog.csdn.net/qq992817263/article/details/78155443 TODO AssetBundle加载的时候 会导致类型错误 非图集 会有Sprite 和Texture2D 图集只有Sprite 只有他们在同一父级下面才会合并为一个batch 网络上的代码, 读的是PNG 与 jpg ,但我这里读不了 研究了一下, 发现能读rbg文件 这里是改变模型的贴图, 还看不出是什么原理 #include #include #include #include #include #include #include #include #include void main() { osgViewer::Viewer viewer; osg::ref_ptr root=new osg::Group(); osg::ref_ptr node=osgDB::readNodeFile("cow.osg"); osg::ref_ptr image=osgDB::readImageFile("Images\\lz.rgb");// osg能读取rgb格式 , data文件夹里带有这张图 if (image.get()) { osg::ref_ptr texture=new osg::Texture2D(); texture->setImage(image.get()); //设置自动生成纹理坐标 osg::ref_ptr texgen=new osg::TexGen(); texgen->setMode(osg::TexGen::SPHERE_MAP); //设置纹理环境,模式为BLEND osg::ref_ptr texenv=new osg::TexEnv; texenv->setMode(osg::TexEnv::Mode::BLEND);// ADD texenv->setColor(osg::Vec4(0.6,0.6,0.6,0.0)); //启动单元一自动生成纹理坐标,并使用纹理 osg::ref_ptr state=new osg::StateSet; state->setTextureAttributeAndModes(1,texture.get(),osg::StateAttribute::ON); state->setTextureAttributeAndModes(1,texgen.get(),osg::StateAttribute::ON); state->setTextureAttribute(0,texenv.get()); node->setStateSet(state.get()); } else { osg::notify( osg::FATAL ) << "Unable to load data file Exiting." << std::endl; } root->addChild(node.get()); viewer.setSceneData(root.get()); viewer.realize(); viewer.run(); } 游戏服务器设计之属性管理器 游戏中角色拥有的属性值很多,运营多年的游戏,往往会有很多个成长线,每个属性都有可能被N个成长线模块增减数值。举例当角色戴上武器时候hp+100点,卸下武器时HP-100点,这样加减逻辑只有一处还比较好控制,如果某天有个特殊功能当被某技能攻击时,角色武器会被击落,这样就会出现减数值的操作不止一处。如果逻辑处理不当,比如击落的时候没有恰当的减数值,再次穿戴武器就导致属性值加了两边,也就是玩家经常说的刷属性。这种bug对游戏平衡性影响很大,反响很恶劣,bug又很难被测试发现。本文将介绍一种管理属性的思路,最大限度的避免此类bug,如果出现bug,也能够很好的排查。 设计思路 刷属性bug的核心原因是某功能的模块数值加了N次,所以各个模块加的属性要被记录,加过了必须不能重复加。设计这样的数据结构。 //!各个属性对应一个总值 //!各个属性对应各个模块的分值 template class PropCommonMgr { public: typedef T ObjType; typedef int64_t (*functorGet)(ObjType); typedef void (*functorSet)(ObjType, int64_t); struct PropGetterSetter { PropGetterSetter():fGet(NULL), fSet(NULL){} functorGet fGet; functorSet fSet; std::map moduleRecord; }; void regGetterSetter(const std::string& strName, functorGet fGet, functorSet fSet){ PropGetterSetter info; info.fGet = fGet; info.fSet = fSet; propName2GetterSetter[strName] = info; } public: std::map propName2GetterSetter; }; 关于数据结构的get和set,我们为每个属性命名一个名字,这样处理数据的时候会非常方便(比如道具配增加属性等等),角色属性有很多种,这里不能一一定义,所以属性管理器只是映射属性,并不创建属性值。通过regGetterSetter接口,注册get和set的操作映射。为什么不需要提供add和sub接口能,因为add和sub可以通过get和set组合实现。get和set的接口实现如下: int64_t get(ObjType obj, const std::string& strName) { typename std::map::iterator it = propName2GetterSetter.find(strName); if (it != propName2GetterSetter.end() && it->second.fGet){ return it->second.fGet(obj); } return 0; } bool set(ObjType obj, const std::string& strName, int64_t v) { typename std::map::iterator it = propName2GetterSetter.find(strName); if (it != propName2GetterSetter.end() && it->second.fSet){ it->second.fSet(obj, v); return true; } return false; } 关于add和sub,前面提到要避免刷属性,就必须避免重复加属性。所以每个模块再加属性前必须检查一下是否该模块已经加了属性,如果加过一定要先减后加。因为每次模块加属性都记录在属性管理器中,那么减掉的数值一定是正确的。这样可以避免另外一种常见bug,如加了100,减的时候计算错误减了80,也会积少成多造成刷属性。add和sub的代码如下: int64_t addByModule(ObjType obj, const std::string& strName, const std::string& moduleName, int64_t v) { typename std::map::iterator it = propName2GetterSetter.find(strName); if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){ int64_t ret =it->second.fGet(obj); std::map::iterator itMod = it->second.moduleRecord.find(moduleName); if (itMod != it->second.moduleRecord.end()){ ret -= itMod->second; itMod->second = v; } else{ it->second.moduleRecord[moduleName] = v; } ret += v; it->second.fSet(obj, ret); return ret; } return 0; } int64_t subByModule(ObjType obj, const std::string& strName, const std::string& moduleName) { typename std::map::iterator it = propName2GetterSetter.find(strName); if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){ int64_t ret =it->second.fGet(obj); std::map::iterator itMod = it->second.moduleRecord.find(moduleName); if (itMod == it->second.moduleRecord.end()){ return ret; } ret -= itMod->second; it->second.moduleRecord.erase(itMod); it->second.fSet(obj, ret); return ret; } return 0; } int64_t getByModule(ObjType obj, const std::string& strName, const std::string& moduleName) { typename std::map::iterator it = propName2GetterSetter.find(strName); if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){ int64_t ret =it->second.fGet(obj); std::map::iterator itMod = it->second.moduleRecord.find(moduleName); if (itMod != it->second.moduleRecord.end()){ return itMod->second; } } return 0; } std::map getAllModule(ObjType obj, const std::string& strName) { std::map ret; typename std::map::iterator it = propName2GetterSetter.find(strName); if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){ ret = it->second.moduleRecord; } return ret; } 如上代码所示,addByModule和subByModule必须提供模块名,比如穿装备的时候加血量:addByModule('HP', 'Weapon', 100),而卸下武器的时候只要subByModule('HP', 'Weapon'),因为属性管理器知道减多少。 总结 属性提供一个名字映射有很多好处,比如装备配属性,buff配属性的,有名字相关联会特别方便 提供一个get和set接口的映射,这样属性管理器就和具体的对象的属性字段解耦了。即使是现有的功能模块也可以集成这个属性管理器。 属性的add和sub操作,都在属性管理器中留下记录,这样即使出现问题,通过getByModule getAllModule两个接口亦可以辅助查找问题。 属性管理已经集成到H2Engine中,github地址: https://github.com/fanchy/h2engine 近来有空,打算写点游戏编程的文章。 今天第一篇,关于第三人称摄像机的。 先看效果 按下鼠标中键,左右可旋转,上下可倾斜。 下面是代码 #include #include // strcpy_s #include using namespace irr; using namespace core; using namespace scene; using namespace video; using namespace io; using namespace gui; #ifdef _IRR_WINDOWS_ #pragma comment(lib, "Irrlicht.lib") #endif #include "my3rdcamera.h" My3rdCamera* myCamera; class EventReceiver: public irr::IEventReceiver { public: virtual bool OnEvent (const irr::SEvent& e){ if(myCamera) myCamera->OnEvent(e); //传入事件到camera里处理 return false; } }; EventReceiver e; int _tmain(int argc, _TCHAR* argv[]) { // create device IrrlichtDevice *device = createDevice(EDT_OPENGL, core::dimension2d(640, 480), 16, false); if (device == 0) return 1; // could not create selected driver. device->setEventReceiver(&e); device->setWindowCaption(L"Irrlicht Engine Demo"); video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager(); scene::IAnimatedMesh* roomMesh = smgr->getMesh("plane.ms3d"); scene::ISceneNode* room = 0; if (roomMesh) { room = smgr->addMeshSceneNode(roomMesh); room->setScale(vector3df(1000,1000,1000)); room->setMaterialFlag(video::EMF_LIGHTING, false); } scene::IAnimatedMeshSceneNode* node = 0; video::SMaterial material; // Add an MD2 node, which uses vertex-based animation. node = smgr->addAnimatedMeshSceneNode(smgr->getMesh("faerie.md2"), 0 ); node->setPosition(core::vector3df(0,22,0)); // Put its feet on the floor. node->setScale(core::vector3df(1.6f)); // Make it appear realistically scaled node->setMD2Animation(scene::EMAT_POINT); node->setAnimationSpeed(20.f); material.setTexture(0, driver->getTexture("faerie2.bmp")); material.Lighting = false; material.NormalizeNormals = true; node->getMaterial(0) = material; // setup the camera myCamera=new My3rdCamera(device,node); //传入device和要跟随的角色节点 //// add fps camera //scene::ICameraSceneNode* fpsCamera = smgr->addCameraSceneNodeFPS(); //fpsCamera->setPosition(core::vector3df(-50,50,-150)); u32 frames=0; while(device->run()) { driver->beginScene(true, true, video::SColor(0,100,100,100)); myCamera->update(); //处理camera smgr->drawAll(); driver->endScene(); if (++frames==100) { core::stringw str = L"Irrlicht Engine ["; str += driver->getName(); str += L"] FPS: "; str += (s32)driver->getFPS(); device->setWindowCaption(str.c_str()); frames=0; } } device->drop(); return 0; } My3rdCamera 在github 有, https://github.com/344717871/3drpgdemo/tree/master/client/My3rdCamera.h https://github.com/344717871/3drpgdemo/tree/master/client/My3rdCamera.cpp faerie.md2 是irr自带的3d模型,plane.ms3d就是一个矩形平面,作为游戏的地面。 光照计算 光照计算主要目的是 为了得到 高频 中频 低频 信息 高频信息是细节,中低频是整体控制 例如低模 本身是中低频, normal map diffuse map 增加了高频信息 高光也是高频信息 AO 遮蔽,高频 例如在计算雾效果时,简单的线性距离深度变化是 低频, 再加上基于高度变化,基于太阳光朝向颜色,基于距离的 mipmap, 增加了高频细节 光照系统分为: 预先计算 实时计算 光照系统的预先计算整体流程 设置光源,光源分为直接光和间接光, 直接光光源分为 平行光,点光 ,探照灯,区域光,基于图像的光源 3.间接光 包括物体互相反射光,来自light probe,来自环境光,来自雾,来自反射探头,间接光通常需要使用烘焙技术来计算 4.烘焙技术包括 基于 辐射度的 和 基于光线追踪 5.烘焙产生光照贴图,或者light probe 数据 6:在游戏运行时,预先计算的间接光信息,阴影信息,简化参与到物体渲染中 光照烘焙涉及到4个方面 光源,物体,材质,UV或者probe 光照烘焙贴图,基于对场景中静态物件进行UV展开,将光照信息写入 写入信息,可以包括直接光, 间接光,阴影,的颜色和强度 光照贴图还可以包括主要光方向, directional lightmap 模式中,包括了主要光方向信息,用于实时计算的时候,结合normalmap 计算diffuse color , 而不是将light map 作为环境光处理 有了主要光方向信息,结合normal map就可以进行更多的计算 整体技术涉及到: 材质系统,光源,烘焙技术 光照最终效果,涉及到阴影,直接光,间接光 三个效果 在Unity中材质的光照模型主要可以分为,lambert phong 和 最新的 pbr 物理基于着色系统 材质模型: half lambert 0.5*(N * L) + 0.5 材质也可以toon shading normal * L normal * V 在texture 上采样光带 pbr 微表面, fresnel 光模型 Unity中的光,分为三种,bake烘焙光,实时光,和mixed模式光 Unity中的烘焙系统,有Enlighten 基于辐射度的烘焙计算,和 progressive 基于光线追踪的烘焙计算 其中enlighten 有两种烘焙,纯静态光照贴图烘焙和 实时GI烘焙 其中烘焙GI光照,可以使用光照贴图,也可以使用light probe来收集,其中光照贴图,分为纯Diffuse贴图和 支持 normal map 的 directional lightmap。 light probe 和 光照贴图区别就是,light probe 收集的是空间光照信息,而light map 收集的是 物体表面的光照信息 reflection probe 反射光照 probe 光照效果: 软影子 宽高光, AO Unity中的光照方案: 全烘焙 ,只使用enlighten 烘焙光照贴图,使用纯烘焙光,光照贴图上包含 直接光,间接光,阴影信息 全实时光 ,使用 cascade shadow map 技术产生4级实时阴影, 使用pbr 材质模型,实时光进行光照计算 mixed light , 分为三种模式 只烘焙 间接光 , 烘焙阴影, 烘焙距离阴影,近处使用实时阴影 subtractive 模式,直接光 间接光 阴影都烘焙,实时阴影和烘焙阴影采用 subtractive模式混合 纯 light probe + 实时光照 , light probe 提供间接光照信息 在上面的基础上继续延伸 战斗服管理器横向扩展 管理器全互联后就不用依赖redis了, 可以相互发消息,本服不存在的房间号,根据房间号混淆加密 房间号加密混淆可逆 算法 直接找出目标组,然后转发过去即可,但是为了不再继续转发 简单的方式是写2个rpc函数来区分。 跨服加入房间问题解决了,那么另外两个个问题,就涉及到负载均衡了。 当本服创建房间失败 或者单人匹配失败。 那么就要把请求转发给其余的管理器去处理(即跨服开房间 或单人匹配)。 单人匹配 和创建房间目前是仅在本服,跨服操作的话那么就要分开构思了。 单人匹配:单人匹配的跨服操作理论上讲 应该尽可能的 全服填坑式 一个服务器组 一个服务器组的塞满。尽可能的让全服玩家都可能相互匹配到 出现这个的原因是管理器有多个,如果全服只有一个的话,那么就简单多了,比如全服只有一个管理器,但是这样吞吐量不够,因为管理器还承担着 战斗服的负载均衡的功能,全服的战斗服都给他去管理的话,压力很大。 方案1:新添加一个服务器角色(匹配服Match),全服只有一台或者仅限的几台,同台可以相互匹配。那么设计上功能尽可能的简单。网络IO协程尽可能的大,因为匹配时间也许会很长,服务器处理能力有限。 TODO 简要: 各种走弯路, 矩阵还不是很会. osg的自带漫游器对像机有影响, 起初不知道. 现在还有一些未解之迷, 比如90帧 的帧率是怎么来的? SteamVR提供的? 手柄模型目前依然用面的绘制方式, 不好看, 不会openGL,没办法. TXT: 找到了加载手柄的代码: error = vr::VRRenderModels()->LoadRenderModel_Async( pchRenderModelName, &pModel );//1: lh_basestation_vive 2:vr_controller_1_5 但这个函数LoadRenderModel_Async里有源码 CGLRenderModel::Draw() RenderModel把自己画出来, 问题是通过什么方式把自己画出来 里面调用的方法, 是来自glew.h的 那么, 能否用原生的OpenGL 在OSG中绘图? 1>main.obj : error LNK2001: 无法解析的外部符号 __imp___glewActiveTexture 1>main.obj : error LNK2001: 无法解析的外部符号 __imp___glewBindVertexArray 1>MSVCRT.lib(crtexe.obj) : error LNK2001: 无法解析的外部符号 main 因为文件SDL.h, 出现了无法解析main的错误. LNK2001 是平台问题 sample示例用的是Debug win32 Debug与Release的区别可能没关系,因为Sample项目就是加载Release版的lib,但用Debug版编译 这个Sample是在OSG64文件夹内的,却是32位 编译通过, 现在要加载dll openvr108\samples\bin\win64 这里面也有个glew32.dll 出现已停止工作 , 出错位置是0x0000 没有出错的代码处 没有OpenGL的基础, 这个错误我解决不了. ############################################################################################ 如果, 显示手柄是SteamVR或HTC VIVE内置的api. 那么,是没有模型文件的. 但, 在OpenGL里, 与Unity里, 手柄的表现是不一样的. OpenGL里没有扳机按压的过程. renderModelName 与OpenGL里的 一样是:vr_controller_vive_1_5 ############################################################################################ https://github.com/ValveSoftware/openvr/wiki/API-Documentation#initialization-and-cleanup IVRSystem 两只眼睛的映射, 头盔手柄的跟踪, 事件的响应 Compositor排字工人 在updatePose里有被用到 WaitGetPoses IVOverlay 疑似在三维场景中显示二维图像的API 有例子, 但例子是QT的 如果要做模型程序,现有的VR场景要能跑通 Screenshots 我猜是截屏截成图片, 两个sample工程都没用到, 不知具体是什么 那么, 桌面屏幕的投影是怎么实现? HMD的投影是怎么实现的? IVRSystem 扣代码, 直接扣过来用啊. 目前需求是, 在VR里显示osg的场景,就这一点而已. 把OpenVRDll64那个工程再复制一遍过来后, 就没有头文件冲突的那一情况了. 之前封装的函数, 不能正常显示 现在想封装成一个inline函数 , 各种出错 #### 为了拷手柄模型获取代码, 还要加引入头文件的目录 glew32.lib 出现不能解析的外部符号bool __cdecl CGLRenderModel::BInit 代码还没拷完 把glew32.dll拷过来, 这个dll在x64下也能用. GetRenderModelCount() 这个函数取出来的模型数是51 ,太多了, 且不受设备开关的影响,可能是一个常量 helloVR是怎么取到设备名的, helloVR是怎么取到设备id的 helloVR中, 通过 GetTrackedDeviceString这个函数取设备名 手柄的设备号是3或4 osg这边取到的设备名是: generic_controller 与 generic_hmd 分别是3与4 设备数只有4个, 没找到更多设备 vr::k_unTrackedDeviceIndex_Hmd 算上跳过的这个,五个 又要建全局变量, 还好在模块内,其他模块调取不到. 还要应对手柄中途关掉或没电的情况. 在openvrdevice里没法取得viewer所以绘图依赖外部调用. #### 取到的3与4并不是vr手柄模型, 输出所有模型, 没有之前的 lh_basestation_vive 2:vr_controller_1_5 vr 手柄在16个模型之外 大于4的模型名:5 : generic_tracker 大于4的模型名:6 : hard_bounds 大于4的模型名:7 : laser_pointer 大于4的模型名:8 : lh_basestation_01_boysandgirls 大于4的模型名:9 : lh_basestation_02_batman 大于4的模型名:10 : lh_basestation_03_robin 大于4的模型名:11 : lh_basestation_vive 大于4的模型名:12 : lighthouse_ufo 大于4的模型名:13 : mptam_hmd_model 大于4的模型名:14 : oculus_cv1_controller_left 大于4的模型名:15 : oculus_cv1_controller_right 能显示手柄模型了 虽然不完美 ################################################################### 现在手柄的移动与现实中不匹配. 看看hellovr里怎么做的.RenderControllerAxes里作了处理 m_rmat4DevicePose这数据也是从mDeviceToAbsoluteTracking里取的. 手柄的旋转乱遭遭的, 是不是左手坐标系? 在hellovr_opengl_main.cpp的1145行做矩阵的试验 所以, 那矩阵的最后一行就是坐标 这行不受 还可以看看头盔是怎么移动的. 头显的移动, 也是受角度影响的, 却没问题. 有一个不同点是, 头显是作用于像机,而手柄是作用于场景中的物体. 环境不一样, hellovr里的取到的矩阵, 取到的最后一行不因角度的改变而改变. hellovr输出: 1.918463, 0.884200, 0.470154 , 1.000000 osg输出: 0.179302, 0.00267851,0.0541984, 1 #################################################################### 要么学会计算, 要么找出坐标不同的原因 #################################################################### openvrdevices是如何刷帧获取设备坐标的? 在OpenVRUpdateSlaveCallback里 updatePose被调用 osg这边, 与openGL有没有什么初始化参数上的区别? 从openvrdevices里, 看不到什么参数 有没有90帧这个参数?没有! 那么updateSlave在哪被调用? slave中有调用updateSlaveImplementation这个是osg的函数,可能是由这个触发osg刷新的, 不然osg只有60帧 OpenVRUpdateSlaveCallback : public osg::View::Slave::UpdateSlaveCallback 是用OSG的规范 m_view对象只,有两个slave视口 gc->setSwapCallback(swapCallback); 是不是这句有影响? 最后,在swapBuffersImplementation失去线索 #################################################################### 例子的代码可读性是有的, OpenVR文档是有的, 但我该怎么做? 如何由VR设备进行刷帧 , 我找不到, 只能由osg触发刷帧 那么, 先做出一个模型程序, 看能否取得View的刷帧事件回调函数. 就先用NodeCallback StackOverflow上,也是这么讲的 现在输出了整个矩阵数组, 发现m[0][3] ~ m[2][3]是可以的. 那为什么Matrix44不行? 建几个立方体来作参照物 跟据参照物, 手柄的X与Z没问题 参照物,大小为.3的, 为z向 1. 对于手柄, 有个轴是反的, 倒底是手柄模型要倒置还是坐标要倒置? 2. 像机的坐标规范, 但能否用手柄的规范 状态1:放桌上 0.957936 0.0671016 0.279027 0 -0.129168 0.969042 0.210413 0 -0.25627 -0.237603 0.936947 0 -0.0621173 0.0138077 -0.157989 1 <状态2:放椅子上 0.865644 0.010904 0.500541 0 -0.125182 0.972721 0.195301 0 -0.484757 -0.23172 0.843396 0 -1.19622 -0.439365 3.71773 1 <状态3: 椅子靠窗 0.945089 0.0206033 0.326164 0 -0.101473 0.967185 0.232931 0 -0.310661 -0.253237 0.916166 0 0.0451128 -0.428728 3.79893 1 <状态4:椅子离窗 0.956137 0.0306015 0.291316 0 -0.106346 0.962934 0.247889 0 -0.272932 -0.267997 0.923951 0 -2.19937 -0.433549 3.14582 1 <完成了两只手柄开关状态的模型显示 用时10天 2篇前置博文: Http服务器-第十步加入基于Mono平台的C#脚本支持 https://my.oschina.net/kkkkkkkkkkkkk/blog/4322402 架构9-引入C#混合编程-服务端概述 https://my.oschina.net/kkkkkkkkkkkkk/blog/4335583 C#自带GC算法在独立使用时无需额外处理,cpp的话各种内存管理技术也很多如智能指针。 在这里混合编程下,c#层作为脚本使用时,很容易在cpp层书写核心代码,几乎2倍内存的消耗,但是这个不是本文的重点,本文阐述他们之间的内存关系和混合管理方式。这里的方案是cpp层通过handle来指向c#,c#端进行handle和实际的对象的引用。下面通过案例来阐述这种方式。 C#层 public class GameObject : object { private long _native_handle;//cpp ptr for }; //提供全局c# 对象 c++引用 static class CppReferenceInternal { public static Dictionary global_cpp_ref = new Dictionary(); public static void Retain(object obj) { int hash = obj.GetHashCode(); if (global_cpp_ref.ContainsKey(hash) == false) { global_cpp_ref.Add(hash, obj); } } public static void Release(object obj) { int hash = obj.GetHashCode(); if (global_cpp_ref.ContainsKey(hash)) { global_cpp_ref.Remove(hash); } } } C++层 class GameObject { public: static GameObject*Create() { GameObject *ret = new GameObject(); return ret; }; GameObject() { AllocMonoObject(); } ~GameObject() { FreeMonoObject(); } private: bool FreeMonoObject() { MonoObjectRelease(mono_object_handle); mono_object_handle = nullptr; return true; } bool AllocMonoObject() { MonoClass *cls = mono_class_from_name(mono_assembly_get_image(mono_stick_engine_dll), "", "GameObject"); mono_object_handle = mono_object_new(vm, cls); //call default ctor mono_runtime_object_init(mono_object_handle); MonoObjectRetatin(mono_object_handle); return true; } private: MonoObject * mono_object_handle = nullptr; }; 这里只是其中一种管理方式的基本示例。 在UGUI的控件属性之中,最上方的Rect Transform一栏可以看到锚点和中心点: 锚点Anchors 控件用于定位自身的基准点 可以点击左上角的方框,在其中选择锚点的不同方式: 注意图中,黄色的小点即为锚点位置示意,比如最右下角的图表示四个锚点分别位于父控件的四个角落: 设置完之后锚点在视图之中是这样的,父控件四个角的四个三角形即为锚点: 左上角的图则表示四个锚点分别位于父控件的上方边沿(其中左边两个重合,右边两个也重合):
锚点还可以手动填写数字来设置,可以自行尝试:
2. 中心点 Pivot 中心点可以通过点击方框,按住shift来设置: 可以看到,蓝色的即为中心点,控件设置的XY坐标即为中心点到锚点的坐标,比如我们按住shift将锚点设为子控件的左上角,然后不按shift键将锚点设为父控件的中心:
此时我们修改x,y坐标,可以看到子控件的左上角(即中心点)离父控件的中心距离根据我们的设置不同而发生变化:
如果将中心点设为右下角,则xy表示子控件右下角到父控件中心的距离:
中心点还是物体旋转的中心,比如将物体的中心点设为左上角,则改变Rotation之中Z的数值,控件就会以左上角为中心旋转: 3. Alt键的功能 按住Alt键可以设置子控件的位置,比如子控件本来位置是这样的: 按住alt键,选将子控件设为父控件的中间靠右的位置: 效果就是这样: 此外,还可以让子控件填充父控件的宽度高度以及全部:
以此类推。这几个功能配合使用,能够组合出各种布局方案,可以多尝试一下 |