月度归档:2016年01月

2015 年 12 月读书笔记

《失乐园》

真心好看的一本小说,毫不避讳如外科解剖般细致的大段落性描写,恰如其分又极端写实的心理活动的呈现,让久木和凛子直接在你的眼前活了。

你可以看到他俩在酒店里,在出租屋里,在度假别墅里做爱,你能感受到久木与妻子在餐桌上的沉默和尴尬,你能感受到凛子和丈夫在家中的冷暴力。

二人在一次次突破世俗伦理的束缚之后,最终成功的走到了一起。但是两人却选择了在这幸福的最高点赴死,不是向那冰冷坚硬的伦常示威,而是向那琐碎的生活和必然衰减的热情抗议。两人深知结合之后,随着时间的推移,家务琐事的牵绊,二人之间的这种热情和默契会日渐磨灭,正如他们与自己的原配之间那般,也无奈找不到破解之道,最终选择在激情燃烧得最为旺盛的刹那走向毁灭,双双赤裸狠狠地抱在一起,满足地离开了这个让他们失望透顶的世界。

《把时间当作朋友》

得知李笑来这个人已经有些年头了,之前只是知晓他是新东方的名师,喜欢写博客,然后就没有了,大概我还一直有订阅他的博客。

再次关注到他,就是因为成功的比特币投资者这个巨大的光环吧。我就是这么俗,哈哈。关注了他的微信公众号之后呢,想着有必要读读这本屡次出现在他文章里头的这本听起来很正能量和主旋律的书(我就是这么装逼,书还没读呢,就先给人定性了),读了之后,有点相见恨晚。

书中反复讲了很多很棒的道理,也非常刻薄地指出了我们绝大多数人的致命弱点,打脸真的好疼,至少我很疼。

疼完了之后呢,我开始尝试去做一些事情了,我继续坚持每天的晨跑(当然北京的这个天气,想必大家也都知道,雾霾天是个很操蛋而且高发的天气,所以会造成一些小间断),然后开始写每天的流水账时间日志。

效果好不好,目前还不能说,我想先坚持个一年再回来看。不论做什么计划,关键在于实践和坚持嘛,书里头就是这么说的,所以,咱们走着瞧。

《甘南纪事》

这是我读的杨显惠先生的第二本书,第一本是《夹边沟纪事》,读完了非常震惊和感动,深为杨先生这种非虚构类文学作品的风格吸引。在叙实之余不落入一种纯文本性的记录和纯对话式的段落,而是适当地通过一些虚拟想象还原每个故事的主人公和出场人物的心理状态和活动变化,又不过分渲染这些私人的揣度,这是一种恰到好处的还原和超越。

此次读《甘南纪事》显然较之前读《夹边沟纪事》要来得轻松许多,心情也没有那么沉重。书中不是穿插一些甘南地区的水美山美,朴实多彩的甘南藏人的风土人情,都让自己有点沉醉其中了,有机会定要去扎尕那探寻一番在书中多次出现的白石头山。

书中多次指明了村民调解委员会在藏人生活中的重要性,特别是在村民公共事务中扮演的重要角色,其中关于僧侣、老人,甚至以前的头人等等在委员会中处理相关事务时所遵循的社会组织原则以及所体现出来的智慧,甚至可以说是迷人的。而这些流传下来的传统和智慧,与现今的社会组织结构难免会产生一些冲突,例如公安局这个角色在藏民们心中和生活中扮演的是一个什么角色,以及藏民们是如何对待各种新型的事物,例如保暖内衣和防风服等等,书中那些淳朴可爱的藏民们会用他们每天的生活给你最真实的答案。

《你的灯亮着吗?》

这本书我第一次看到的是一个在网上流传的 PDF 版本,当时自己拿到打印店去打印了全书,自己也读了一部分,应该是把电梯的那个案例的部分给读完了,也许后面的部分没有继续读下去,也许读完了之后完全忘光光了,因为这次重新再读一回,发现自己隐约能记起来的也就是电梯的案例。

关于这个案例,在我自己身上还有个小事情蛮有趣的。08 年 10 月份前后,腾讯来我们学校校招,然后笔试通过之后,我就去参加面试了,面试的时候,面试官是个年轻帅气的小伙子,然后他先让我尝试着解决一个算法编程问题,我尝试了十分钟左右,没有什么思路,然后面试官考虑到时间有限(当时计划我是下午4点半开始面试,实际上我面试的时间已经6点多了,实际上我已经饿了),换了一个开放式的题目就是这个电梯的案例。我想了大概几秒钟吧,我觉得我还是应该坦白告诉面试官这本书我读过,我要是直接把书里头的内容给搬出来,这个就是抄袭,我也不能确定我能怎么样把我看过这本书的事实掩盖住,又不露痕迹地展现我自己独立思考的能力自己和看问题的视角的独特性。最终,我很快地就告诉我的面试官,我说这本书我看过,希望能换一个题目,面试官笑了笑,然后给我换了另一个开放式的题目。这回的题目是一个智力题,然后我就被我的智商打败了,最终我就这么灰头土脸地结束了我的腾讯校招面试了。

所以这次重新读这本书的过程中,其实我有多次已经读不下去了,但是我还是不断地告诉自己必须读下去,最终读完了。我想说的是,本书的趣味性并不是那么高,有几个还蛮有趣的案例分析但是不多,文中也有不少比较别致的观点,但是说真的读起来确实蛮无趣的。也许以后得某天再读会有新的感受和收获吧。

《21 世纪资本论(精华本)》

原本以为这是全本,后来读了总感觉特别不对,很多的结论和数据扑面而来,完全消化不了,主要阐述的观点貌似是这样的「21 世纪里头,继承遗产的重要性将会更为突出,社会将会再次形成明显的阶级分层,而且财富将会更加集中化」,大概就是这样吧。不建议读这个所谓的精华本,我是在「多看阅读」上面读的,完全不建议,想读的话还是找全本来读吧。

Unity3D 中粒子特效不在摄像机渲染范围内 Play 之后导致特效延时播放的解决方法

从项目启动之初,我们就引进了 PoolManager 插件来做诸多对象的缓存,防止游戏中对象的频繁 Instantiate 和 Destroy,造成不必要的性能问题。PoolManager 是一个非常成熟的插件,也提供了很多非常友好的接口以供开发者使用,其中也有针对 ParticleSystem 对象的 Spawn/Despawn 机制,所以我们就很自然的使用了 PoolManager 插件来管理整个游戏中的所有特效对象。

particle_system_spawn_pool_management

对于特效对象,我们游戏中的管理逻辑大体如下:

也就是说我们的粒子特效对象是重用的,在每次重新被 Spawn 出来的时候都会重新调用一次 Play 方法来重新播放这个粒子特效,这个方案看上去很正常有木有?确实没有啥硬伤对吧。

但是在我们实际的测试过程中,我们很早就发现了一个很奇怪的问题,就是偶尔场景中会出现一些不应该出现的技能特效。例如,在场景中偶遇到一个小怪,小怪在攻击的过程中玩家控制主角快速跑开了(主摄像机一直跟随主角),然后灯小怪的攻击结束之后,主角再次跑回到小怪面前进行战斗,然后此时偶尔就会看到小怪并没有在进行任何攻击,只是在战斗待机或者移动,但是屏幕上会凭空播放一些这个小怪的攻击技能对应的特效(其实找到这个问题重现的方法,也就是这个 Bug 的规律也是费尽心思啊,想想看这货都存在快 TMD 一年了,直到现在我才真正地捉到它)。

既然已经找到了重现的方法,也清楚了 Bug 出现的规律,也就是当小怪释放技能时(技能动作带有技能粒子特效),由于主角的移动导致摄像机跟着主角在场景中移动,最终小怪释放的技能粒子特效并不在摄像机的 Culling 区域中,而此时我们通过了 PoolManager 插件 Spawn 出来了一个新的粒子特效,然后调用了 ParticleSystem 的 Play 方法来播放粒子特效。我们预想的是该粒子特效在我们调用 Play 之后就会播放,然后就交给 PoolManager 自行管理就好了(PoolManager 插件会监听粒子是否已经完全发射/播放结束,如果是,就会将这个粒子对象 Despawn 掉,放回到 PoolManager 中对应的 SpawnPool 中以备后续再次使用)。

问题就出在这里,PoolManager 中监听粒子对象是否完全发射/播放结束的方法是通过轮询 ParticleSystem 的 IsAlive 方法来进行判断的,代码如下:

// ParticleSystem (Shuriken) Version...
private IEnumerator ListenForEmitDespawn(ParticleSystem emitter)
{
    // Wait for the delay time to complete
    // Waiting the extra frame seems to be more stable and means at least one 
    //  frame will always pass
    yield return new WaitForSeconds(emitter.startDelay + 0.25f);

    // Do nothing until all particles die or the safecount hits a max value
    float safetimer = 0;   // Just in case! See Spawn() for more info
    while (emitter.IsAlive(true))
    {
        if (!PoolManagerUtils.activeInHierarchy(emitter.gameObject))
        {
            emitter.Clear(true);
            yield break;  // Do nothing, already despawned. Quit.
        }

        safetimer += Time.deltaTime;
        if (safetimer > this.maxParticleDespawnTime)
            Debug.LogWarning
            (
                string.Format
                (
                    "SpawnPool {0}: " +
                        "Timed out while listening for all particles to die. " +
                        "Waited for {1}sec.",
                    this.poolName,
                    this.maxParticleDespawnTime
                )
            );

        yield return null;
    }

    // Turn off emit before despawning
    //emitter.Clear(true);
    this.Despawn(emitter.transform);
}

这个 IsAlive 判断就是问题的关键,经过测试我们发现了一个 Unity3D 的机制,当我们首次将 PartileSystem 对象 Instantiate 出来时,不论其是否在摄像机的 Culling 区域内,调用 ParticleSystem 的 Play 方法或者将其 playOnAwake 设置为 True 都会播放该 ParticleSystem 对象。而在其首次 Instantiate 之后,再次调用 Play 方法来播放该粒子特效时,如果粒子不在任何一个 Camera 的Culling 区域内(包括 Culling Layer Mask 是否相符以及是否在 Camera 的可视范围内),实际上粒子并不会发射,直到其进入到某个 Camera 的 Culling 区域内才会触发粒子的发射(实际在 Unity Editor 中测试的时候,最终发现粒子进入 Scene Window 的可视区域和 Game Window 的可视区域都会触发粒子的发射,实际上 Scene Window 的 Preview 就是有一个我们不可见的 Camera 在进行渲染)。此处需要说明的是以下两点:

  1. Unity 版本为 4.6.9f1;
  2. 粒子特效的 Renderer Mode 是 Billboard。

至此,我们终于完全弄明白了为什么会出现某些技能的粒子特效无端地在场景里播放了,原因就是因为该特效在 Instantiate 出来调用过一次 Play 方法之后(由于我们所有的特效默认 playOnAwake 属性都是 true,所以我们通过 PoolManager 加载的粒子特效的 Prefab 实际上在首次 Spawn 之前都已经自动调用过一次 Play 方法了),再次调用 Play 方法来播放该粒子特效的时候,该粒子特效所处的坐标位置并不在主摄像机的 Culling 区域内,所以根本就不会触发粒子的发射而是在等候下次进入摄像机的 Culling 区域时再触发。在我们游戏中的最终表现就是在屏幕边缘与小怪发生战斗,当小怪释放技能的时候快速往小怪的反方向移动,等着小怪来追击,我们再折返跑回刚刚与小怪发生战斗的地方,这个时候我们就能看到几个小怪的技能特效凭空在场景中自动播放了,真乃煞是奇妙啊。

好吧,说了这么多,那么我们既然知道是这个原因了,那么怎么解决呢?我们看到了  ListenForEmitDespawn 方法中有一个针对粒子特效在 Spawn 出来之后时间的检测,其检测粒子对象自 Spawn 以来的时长是否超出 SpawnPool 中定义的最大 Despawn 时长,跟实际的粒子对象并无直接关系,而且即便该粒子对象 Spawn 出来的时长已经超出最大的 Despawn 时限,也只是会简单地输出了一个警告的 Log 信息。

那么我们自己的处理逻辑应该是怎样的呢?我的处理方法是在粒子对象被 Spawn 出来之后,除了通过其自身的 IsAlive 方法来检测该粒子对象是否还是激活状态,在其 Spawn 出来的同时就启动一个 Coroutine 用来检测其自 Spawn 出来的时长是否超出 ParticleSystem 的 duration 设置值,如果已超出,那么就将其 Clear 并且 Stop,然后再将其 Despawn 掉。依次修改后的 ListenForEmitDespawn 方法如下:

// ParticleSystem (Shuriken) Version...
private IEnumerator ListenForEmitDespawn(ParticleSystem emitter)
{
    // Wait for the delay time to complete
    // Waiting the extra frame seems to be more stable and means at least one 
    //  frame will always pass
    yield return new WaitForSeconds(emitter.startDelay + 0.25f);

    // Do nothing until all particles die or the safecount hits a max value
    float safetimer = 0;   // Just in case! See Spawn() for more info
    while (emitter.IsAlive(true))
    {
        if (!PoolManagerUtils.activeInHierarchy(emitter.gameObject))
        {
            emitter.Clear(true);
            yield break;  // Do nothing, already despawned. Quit.
        }

        safetimer += Time.deltaTime;
        if (safetimer > emitter.duration) {
	        Debug.LogWarning
	        (
	            string.Format
	            (
	                "SpawnPool {0}: " +
	                    "Timed out while listening for all particles to die. " +
	                    "Waited for {1}sec.",
	                this.poolName,
	                emitter.duration
	            )
	        );
			break;
		}
            
        yield return null;
    }

    // Turn off emit before despawning
	emitter.Stop(true);
    emitter.Clear(true);
    this.Despawn(emitter.transform);
}