Gu Lu's Blog https://gulu-dev.com/ Recent content on Gu Lu's Blog Hugo -- gohugo.io en-us Sat, 11 May 2024 00:00:00 +0000 2024.05 手机游戏对孩子的影响和应对 https://gulu-dev.com/post/2024-mobile-games-and-children/ Sat, 11 May 2024 00:00:00 +0000 https://gulu-dev.com/post/2024-mobile-games-and-children/ <p>此文为近期分享的材料。</p> <img src="slides/1.jpeg" width="720"> <img src="slides/2.jpeg" width="720"> <img src="slides/3.jpeg" width="720"> <img src="slides/4.jpeg" width="720"> <img src="slides/5.jpeg" width="720"> <img src="slides/6.jpeg" width="720"> <img src="slides/7.jpeg" width="720"> <img src="slides/8.jpeg" width="720"> <img src="slides/9.jpeg" width="720"> <img src="slides/10.jpeg" width="720"> <img src="slides/11.jpeg" width="720"> <img src="slides/12.jpeg" width="720"> <img src="slides/13.jpeg" width="720"> <img src="slides/14.jpeg" width="720"> <img src="slides/15.jpeg" width="720"> 2023.01 怎样当好一名师长 (notes) https://gulu-dev.com/post/2023-01-07-commander/ Sat, 07 Jan 2023 00:00:00 +0000 https://gulu-dev.com/post/2023-01-07-commander/ <p>此文为 <a class="link" href="#f1" >&ldquo;怎样当好一名师长&rdquo;</a> 一文的快速记录和短评。</p> <p>对于部队来说,师长是一个非常重要的位置,需要很强的领悟力和执行力,很好地衔接指挥官与一般的中下层军官。如果用人体来打比方,师长可以说是部队的<strong>颈动脉</strong>。学习优秀师长所应具有的素质,对我们的工作和学习会有很大的启发。</p> <hr> <ol> <li><strong>要勤快</strong> <ul> <li>懒会带来危险,带来失败</li> <li>没有思想准备,没有组织准备,工作没做到家,一个字 “懒”,一遇到情况就会变成机会主义者</li> <li>不管本事多大,不勤快就不是好干部</li> <li>评: <ul> <li>勤快的人往往坦率,不需要花时间找借口,思想包袱是最小的,可以轻装上阵。</li> <li>偷懒会导致低质量的判断,行动上的迟疑,伙伴间的推诿,很容易整体上陷入被动。</li> </ul> </li> </ul> </li> <li><strong>要摸清上级的意图</strong> <ul> <li>对上级的意图要真正理解,真正融会贯通 (重点词:<strong>意图</strong>)</li> <li>真正认识自己所领受的任务,在战役战斗全局中的地位和作用(大局观)</li> <li>这样才能充分发挥自己的主观能动性,才能打破框框,有敢于和善于在新情况中找到新办法的创造性。</li> <li>评: <ul> <li>往错误的方向跑,越拼命错的越多。</li> <li>不要蛮干,看问题要全面,勿因小失大。</li> <li>明白底线在哪,边界在哪,才能不束手束脚,放得开,能够运用灵活有效的手段</li> </ul> </li> </ul> </li> <li><strong>要调查研究</strong> <ul> <li>没有调查就没有发言权 (了解情况越少,就越难以做出有效的判断与决策)</li> <li>平时积累和掌握的情况越多,越系统,在紧张复杂的情况下,就越沉着,越有办法。</li> <li>摸不清楚情况,就会犹豫不定,勉强下了决心,一遇风吹草动,听到不正确的建议就容易动摇。</li> <li>多读书,而且要多读敌人的书,研究敌人,不断总结经验教训。(站在对手角度分析问题)</li> <li>评: <ul> <li>要善于从正方两方面去寻找线索,分析问题。</li> </ul> </li> </ul> </li> <li><strong>要有活地图</strong> <ul> <li>指挥员和参谋必须熟悉地图,常读,熟读,闭上眼睛眼前就有战场,离开地图也能指挥作战。</li> <li>评:对于商业组织来说,就是熟悉产品,团队,竞品,行业的情势</li> </ul> </li> <li><strong>要把各方面的问题想够想透</strong> <ul> <li>要让大家提出各种可能发现的问题,并一同寻找答案,直到找无可找,答无可答</li> <li>问题很多,不可能一次提完,整个战役战斗的过程,就是不断的提出问题和回答问题的过程</li> <li>想不通,无法立刻解决的问题,要心里有数,不能提过就忘</li> <li>评: <ul> <li>要深入思考具体的情况,提前发现和解决问题;看问题不要浮于表面,问题一出现就傻眼</li> </ul> </li> </ul> </li> <li><strong>要及时下达决心</strong> <ul> <li>以最大努力去组织准备工作,有把握才动手。但任何一次战斗都不可能完全具备各种条件,不可能有100%的把握。一般来说有80%左右的把握就很不错了,就要坚决打,放手打。</li> <li>评: <ul> <li>动如风。</li> <li>行动坚决果断,避免好谋无决。</li> </ul> </li> </ul> </li> <li><strong>要有一个很好的很团结的班子</strong> <ul> <li>领导班子的思想认识一致,行动要协调、合拍。</li> <li>要千方百计解决问题,完成任务,不要互相扯皮,互相干扰,把自己当成旁观者。</li> </ul> </li> <li><strong>要有一个好的战斗作风</strong> <ul> <li>好的战斗作风是英勇顽强,不怕苦,不怕牺牲,猛打猛冲猛追。要像铁锤一样,砸到哪里,哪里就碎。</li> <li>做工作也要有好的作风,说了就要做,说到哪里就做到哪里。要做到干净利索,要一竿子插到底,一点不含糊,不做好不撒手。</li> <li>好的作风养成,关键在于干部。强将手下无弱兵。</li> </ul> </li> <li><strong>要重视政治,亲自做政治工作</strong> <ul> <li>要干活,就要把干劲鼓得足足的。战术,技术也要练好,艺高人胆大。(士气和能力的提升)</li> <li>政治工作持续做,注重思想觉悟的提高。(团队气质打磨,公司文化锤炼)</li> <li>军事指导员任何时候都不能忘记政治,要亲自做政治工作。(技术专家也要懂管理)</li> </ul> </li> </ol> <p>小结:</p> <ul> <li>1/3/4/5 是关于 “事” 的,要勤快能干,要深入调查,要熟知情势,要不断分析和解决问题。</li> <li>2/6/7/8/9 是关于 “人” 的,对领导充分领会意图不跑偏,对同事要团结和协调不扯皮。对团队平时要培养作风觉悟和士气,提高技战术能力。要有决心。</li> </ul> <h2 id="references">References</h2> <ul> <li><b id="f1">1. </b> <a class="link" href="lin-be-a-commander.pdf" >怎样当好一名师长 (PDF)</a> <a class="link" href="#a1" >↩</a></li> </ul> 2023.01 8760 小时 https://gulu-dev.com/post/2023-01-01-8760-hours/ Sun, 01 Jan 2023 00:00:00 +0000 https://gulu-dev.com/post/2023-01-01-8760-hours/ <img src="proxy.php?url=https://gulu-dev.com/post/2023-01-01-8760-hours/2023-s.png" alt="Featured image of post 2023.01 8760 小时" /><blockquote> <p>一年之计在于春。</p> </blockquote> <h2 id="什么是8760小时">什么是8760小时?</h2> <p>先说一下什么是8760小时,一年365天,每天24小时,乘起来就得到了这个数。</p> <p>生命虽然短暂,但我们仍然可以对接下来的 8760 小时认真规划,并在这段旅程中达成希望实现的目标。</p> <p>只需要做一份清晰的蓝图,我们就可以用它来理解过去的轨迹,构造合适的框架,做出必要的优化,从而在新的一年里得以保持在正确的方向上,稳定地向目标前行。</p> <p>这份蓝图非常简单,只包含三个步骤:</p> <ol> <li>画一张清晰的全景图 (A Big Picture)</li> <li>列一份具体的计划清单 (A Concrete Plan)</li> <li>在清单上按照优先级整理出主次 (Prioritizing)</li> </ol> <p>这样就得到了一份清晰的蓝图,来帮助我们更好地度过接下来这 8760 个小时。</p> <h2 id="准备">准备</h2> <h3 id="计划的目的">计划的目的</h3> <p>计划的目的,并不是锁定一条预设的路线,而是提供一条通向目标的(在当前条件和视野下相对较优的)默认路径。</p> <p>(有点反直觉的是) 人们通常不善于,甚至有些排斥策略性思考,对于一个确定的目标,如果不挖掘,我们通常只是带着一个相对模糊的感觉上路。</p> <p>这时候就需要追问自己:</p> <ul> <li>我希望达成的确切目标是什么?</li> <li>衡量该目标已达成的标准是什么?</li> <li>它是否已被拆分为若干可控的子目标?</li> <li>我是否反复确认过该目标的合理性和有效性?</li> <li>在确认目标的过程中,我受到(主观上的)担心,恐惧或不确定性的影响了吗?</li> </ul> <p>追问自己这些问题,就能让我们对目标的界定更清晰。</p> <h3 id="光有决心是不够的">光有决心是不够的</h3> <p>新年计划很难做好,只要做过的人都懂(失败的次数远超成功的次数)</p> <p>有哪些失败的情况呢?</p> <ul> <li>提出一个很模糊,不确定的目标,如:“减肥”,“阅读更多的书”</li> <li>制定的目标过于宏大,以至于完全无法启动,这样就很容易直接压垮自己的信心</li> <li>制定的目标其实并不是你真正在乎的事情,导致实际上被丢到一边,从未得到执行和落地</li> </ul> <p>设定目标的时候,要注意避免这些情况。</p> <h3 id="往年回顾">往年回顾</h3> <p>回顾去年的情况,能让我们在考虑今年的时候,有一个基准的参考。毕竟有昨天才有今天,鉴往事才能知将来。</p> <ul> <li>对于那些已经完成的事情: <ul> <li>哪一块做得还不错,哪一块不太好?</li> <li>在什么事情上下了大功夫,在什么事情上功夫下得不够?</li> </ul> </li> <li>对于特定的事情: <ul> <li>过去的一年取得了什么进展和成就,有什么重要的事件发生?</li> <li>评价如何 (1-7),此前的优势得到强化了么,此前的弱点有改善么?</li> <li>目前情况如何,如何就当前的形势简短地向自己汇报?</li> </ul> </li> <li>对于特定的领域: <ul> <li>(在这个方面)最重要的问题是什么?手头在忙什么?为什么没有在搞(前面说的)重要问题?</li> <li>(在这个方面)个人遇到的瓶颈是什么?因为什么原因没有发挥出更好的潜能?</li> <li>(在这个方面)如果做对了哪一件事,能产生最重大的积极效果?</li> </ul> </li> </ul> <p>这些问题能让我们在定今年的计划的时候,定一个 &ldquo;<strong>跳一跳 能够得着</strong>&rdquo; 的计划,不至于偏离自己的实际能力太远。</p> <h2 id="计划">计划</h2> <h3 id="关注这-12-个方面">关注这 12 个方面</h3> <ul> <li><strong>价值和目的</strong> Values &amp; Purpose <ul> <li>我的生活有明确的目的和方向感吗?对生活的期望是什么,有变化吗?</li> <li>我的价值观是什么,有变化吗?生活哲学是什么,有变化吗?</li> </ul> </li> <li><strong>贡献和影响</strong> Contribution &amp; Impact <ul> <li>我创造的价值是什么?我把钱花在什么事情上,什么人身上,产生了想要的效果吗?</li> <li>我的所作所为,是否 make a difference?对其他的人、事、物产生的影响是积极还是消极?</li> </ul> </li> <li><strong>物品和居所</strong> Location &amp; Tangibles <ul> <li>我的物质条件整体上充裕吗?我对自己生活的位置和条件感到满意吗?</li> <li>我拥有的东西(过)多么,生活中产生过杂乱的感受么?</li> </ul> </li> <li><strong>资产和财务</strong> Money &amp; Finances <ul> <li>我的储蓄状况健康么,有配置紧急资金么,这些资金的存储位置可靠么?</li> <li>我知道自己的钱花到哪儿去了么,我的预算靠谱么,我能把钱花得更有效率吗?</li> <li>我有债务和相关的还款计划么,我的财务在未来的一年可持续吗?</li> <li>我的资产和投资状况如何,投资计划需要完善和更新么?</li> </ul> </li> <li><strong>职业和工作</strong> Career &amp; Work <ul> <li>我通过什么赚钱?这种方式可持续么?</li> <li>我投入么,适应自己的角色么,在目前的工作中能提升和精进么?</li> <li>在行业内的长期发展趋势如何,有感到长期或短期的压力么?</li> </ul> </li> <li><strong>身心的健康</strong> Health &amp; Fitness <ul> <li>我的饮食规律和多样化么?快餐和碳水的依赖度高么?</li> <li>我常常感到疲劳/生过病么,是主动还是被动的?</li> <li>我锻炼和健身么,频率和节奏如何?睡眠,体重,和心率状况如何?</li> <li>我会在意和改善身体存在的隐患么?</li> </ul> </li> <li><strong>教育和成长</strong> Education &amp; Skill Development <ul> <li>我清楚地知道自己的才能,并花时间在个人提升上么?我有学会新东西么?</li> <li>我的阅读状况如何,我有系统地总结自己的收获,并输出到合适的地方么?</li> <li>在过去的一年中,我的视野中出现了哪些值得关注的新事物?存在无效关注么?</li> </ul> </li> <li><strong>社交与关系</strong> Social Life &amp; Relationships <ul> <li>我的家庭生活还顺利么?</li> <li>我与我的朋友圈相处如何,希望更开阔一些还是更紧凑一些?</li> <li>在社会交往中,我对他人(如合作伙伴)的需求足够关注么,我感受到自己的价值了么?</li> </ul> </li> <li><strong>心态与感受</strong> Emotions &amp; Well-Being <ul> <li>我享受自己目前的生活状态么,能相对稳定且自然地保持一个轻松和愉悦的基本心态么?</li> <li>我的心态稳定么,情绪大起大落得多么,常常过度乐观/悲观么?</li> <li>我的思考,判断和决定会受到情绪的影响么?我会定期地练习正念么?</li> </ul> </li> <li><strong>人格完整性</strong> Character &amp; Integrity <ul> <li>我有独特的身份认同 (identity) 和标签么,它们来源于哪里,需要校准么?</li> <li>我的核心竞争力是什么,我的弱点是什么?</li> <li>我如何评价自身的信心,勇气,纪律,专注度,责任心,同情心,可靠程度?</li> </ul> </li> <li><strong>效率和组织</strong> Productivity &amp; Organization <ul> <li>我的效率组合(system + tools)是什么?</li> <li>我的日常工作和生活有序么,复杂度高么,需要简化么?</li> <li>我有深度工作的能力么?我常常因为缺乏优先级而陷入身体和心智的忙乱么?</li> </ul> </li> <li><strong>冒险与创造</strong> Adventure &amp; Creativity <ul> <li>我前进的方向是我最想要尝试的么?</li> <li>我有时间发展自己的爱好吗,这段时间觉得好玩的事情是什么?</li> <li>我容易感到快乐吗?</li> <li>我过去的一年中做过的,值得一提的有创造性的事情是什么?</li> </ul> </li> </ul> <h3 id="关注长期愿景">关注长期愿景</h3> <ul> <li>我认为自己可以/应当成为什么样的人?理想的生活是什么样?</li> <li>如果我设立的目标得以完美的实现,我会有哪些方面的收获?</li> <li>有没有什么事情,是我常常觉得很重要,但一直没有机会尝试的?</li> <li>如果我一年后离开这个世界,我会在这一年中做什么?</li> <li>如果我 10 年后离开这个世界,我会在这一年中做什么?</li> </ul> <p>弄明白这些,有助于把明年要做的事情,跟自己的长期愿景关联起来。给自己的人生体验赋予意义。</p> <h3 id="接下来的-8760-小时">接下来的 8760 小时</h3> <p>关于接下来的 8760 小时,第一件可以做的是,给自己一个具体而明确的词语来概括这一年的年度标语 (Yearly Theme)。比如我的 2023 年定义是 The Year of Creations。</p> <p>然后,根据重要性和优先级,确认你的主要目标列表(不要超过三项),并把这个目标列表与所拥有的时间等资源关联起来,确认自己的最优行动路线。</p> <p>最后,不要忘了为提升自己的元技能 (Meta-Skills) 保留一定的时间资源。这些技能包括,如何优化自己的效率系统,如何精简自己的学习系统,如何整理自己的笔记系统,以及如何改进整个计划,评估和回顾系统。</p> <h3 id="年度日历">年度日历</h3> <p>在一份日历上列出以下内容:</p> <ul> <li>一年中最重要项目的里程碑和截止日期</li> <li>已经确认的特定的事件和活动</li> <li>仅含关键字的粗线条的月度/季度/年度工作计划</li> <li>年度标语 (Yearly Theme)</li> </ul> <h2 id="调整">调整</h2> <h3 id="优化的方法">优化的方法</h3> <p>拖延症公式</p> <p><img src="https://gulu-dev.com/post/2023-01-01-8760-hours/formula.png" width="932" height="144" srcset="https://gulu-dev.com/post/2023-01-01-8760-hours/formula_hu5924c02040358e59dea70655d9960483_26006_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2023-01-01-8760-hours/formula_hu5924c02040358e59dea70655d9960483_26006_1024x0_resize_box_3.png 1024w" loading="lazy" alt="The Procrastination Equation (Piers Steel)" class="gallery-image" data-flex-grow="647" data-flex-basis="1553px" ></p> <p>动力发生器</p> <p><img src="https://gulu-dev.com/post/2023-01-01-8760-hours/how-to-get-motivated.png" width="7100" height="4999" srcset="https://gulu-dev.com/post/2023-01-01-8760-hours/how-to-get-motivated_hu536be57df8805a8217d07a7c4fd8b591_1101355_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2023-01-01-8760-hours/how-to-get-motivated_hu536be57df8805a8217d07a7c4fd8b591_1101355_1024x0_resize_box_3.png 1024w" loading="lazy" alt="How to Get Motivated (Alex Vermeer)" class="gallery-image" data-flex-grow="142" data-flex-basis="340px" ></p> <p>对抗不确定性</p> <ul> <li>找到不确定性的根源,在承认事实的基础上,确认是否存在改进可能性</li> <li>通过刻意练习 (Deliberate Practice) 来降低主观阻力和摩擦</li> <li>使用测量系统 (Metrics) ,进展追踪系统 (Tracking System),统计系统 (Stats System) 来提高可见性</li> </ul> <h3 id="回顾和迭代">回顾和迭代</h3> <p>在一年的发展和推进的变化过程中,</p> <ul> <li>【<strong>回顾</strong>】 以自己觉得舒服的周期去做回顾即可 <ul> <li>通常月度/季度回顾就可以了</li> </ul> </li> <li>【<strong>校准</strong>】不断按照实际情形去校准目标,实事求是 <ul> <li>不过度迷恋于原始计划,不要刻舟求剑</li> </ul> </li> <li>【<strong>优先级调整</strong>】时间有限的情况下的主动选择 <ul> <li>当断不断,反受其乱。</li> </ul> </li> </ul> <h2 id="小结">小结</h2> <ol> <li>回顾的重要性</li> <li>12个不同方面的拆分</li> <li>关联和兼顾长期/短期目标</li> <li>应对拖延症和不确定性</li> <li>优化和迭代</li> </ol> <h2 id="references">References</h2> <ul> <li><a class="link" href="https://alexvermeer.com/8760hours/" target="_blank" rel="noopener" >8,760 Hours: How to get the most out of next year</a></li> <li><a class="link" href="https://alexvermeer.com/getmotivated/" target="_blank" rel="noopener" >How to Get Motivated: A Guide for Defeating Procrastination</a></li> </ul> 2022.12 家庭教育心得 9 条 https://gulu-dev.com/post/2022-12-10-family-education/ Sat, 10 Dec 2022 00:00:00 +0000 https://gulu-dev.com/post/2022-12-10-family-education/ <p>前段时间在偶然的场合跟朋友聊起家庭教育的话题,发现无意中彼此有不少的契合。</p> <p>回想起来,这些年确实累积了一点教训与心得。孩子也慢慢大了,是时候梳理和记录一下,做个小结。</p> <hr> <ol> <li>培养自主学习能力 <ul> <li>成长说到底是自己的事,总是引导其对自己的成长负责</li> <li>家长需要分清主客,勿喧宾夺主</li> </ul> </li> <li>锻炼自我反省意识 <ul> <li>获取向自己提问的能力 (自我反馈和调整)</li> <li>是倾听和培育 inner voice 的过程</li> <li>总是区分和利用 “调节回路” 和 “强化回路”</li> </ul> </li> <li>重视扎实的基本功 <ul> <li>避免虚头巴脑的花架子 (勿在浮沙筑高台)</li> <li>不要把 Python 当成编程入门 (捷径不捷)</li> </ul> </li> <li>不如守中 <ul> <li>不要过早剪枝 (珍珠理论)</li> <li>不要过早强化 (技能点理论)</li> <li>不要过分强调批判性思维 (亢龙有悔,过犹不及)</li> </ul> </li> <li>知行合一 <ul> <li>关于 “知”,学会区分事实,观点,立场</li> <li>关于 “行”,学会抽象和还原,总是着手于具体问题的解决</li> <li>对 “实践论” 和 “矛盾论” 的灵活应用</li> </ul> </li> <li>自然科学 <ul> <li>建立框架和体系</li> <li>明确自身坐标和成长方向</li> </ul> </li> <li>人文社科 <ul> <li>以人为鉴 (纵向展开)</li> <li>以特定事件切入 (incident-driven)</li> </ul> </li> <li>学会幽默 <ul> <li>爱笑的孩子,运气都不会差</li> </ul> </li> <li>真正的教育 <ul> <li>上面看起来是在说针对孩子的家庭教育</li> <li>其实这些是自我教育</li> <li>然而自我教育是最好的家庭教育</li> </ul> </li> </ol> <hr> <p>History</p> <ul> <li>2022-12-10 posted</li> <li>2022-11-26 revised</li> <li>2022-11-20 initially written</li> </ul> 2022.07 量子退相干的简要解释 https://gulu-dev.com/post/2022-07-23-quantum-decoherence-explained/ Sat, 23 Jul 2022 00:00:00 +0000 https://gulu-dev.com/post/2022-07-23-quantum-decoherence-explained/ <p>2019 年时,Google 在量子计算的研究上取得一定的进展,曾引起一些人的恐慌,是不是密码学要被破译了,区块链不再安全了。当时我<a class="link" href="https://gulu-dev.com/post/2019-10-28-bitcoin-quantum-resistance/" target="_blank" rel="noopener" >写了一篇短文</a>,来描述我所理解的量子抵抗能力(在不同情况下的安全程度),以及对应的一些具体的安全实践。</p> <p>在那篇文章里,我提到了 <strong>量子退相干</strong> (Quantum Decoherence) 是量子计算所尚未有效解决的重要问题,但并没有说清楚量子退相干是怎么影响量子计算的,以及它究竟是什么,在这里我们展开说明一下。</p> <h2 id="环境对量子计算的干扰">环境对量子计算的干扰</h2> <p>在进行量子计算的过程当中,如果发生了微小的外部扰动,就可能影响和改变正在计算中的量子比特的状态(用行话讲就是 “量子的相干性受到了干扰”),这样的话,得出的计算结果就会不正确。这种量子计算系统受到环境的影响,就是量子退相干的一种体现。所谓退相干 (<strong>De</strong>-coherence),即为相干性的消退 (<em>Quantum decoherence</em> is the loss of <em>quantum coherence</em>)。</p> <p>跟传统计算机相比,量子计算对这种来自环境的影响要敏感得多。周围的电磁场影响,环境温度影响,甚至是量子比特之间的相互影响,都会造成不同程度的干扰,从而使得被用来执行计算和存储任务的量子比特 “变脏”,造成存储的信息丢失。</p> <p>降低这种影响的关键,在于有效地管理和降低退相干,从而使得量子相干性能够不受干扰地演化(undisturbed evolution of quantum coherences)。</p> <p>怎么样保证量子计算的准确性呢?可以通过增加冗余计算的方式来提高容错性。比如对于乘法计算 9 * 9,使用不同方式计算1千次,如果其中绝大部分计算结果都是 81,我们就采纳这个结果。除了简单的冗余计算以外,还有<a class="link" href="https://en.wikipedia.org/wiki/Quantum_error_correction" target="_blank" rel="noopener" >一些高级的校验和矫正方法</a>来确保准确性。</p> <h2 id="退相干的进一步解释">退相干的进一步解释</h2> <p>在上面的解释中,退相干被解释为一种宏观意义上比较笼统的 “退化” 作用。实际上,在量子力学中,量子退相干是一种有别于哥本哈根解释的,一般意义上被认为是逻辑更加严谨的解释。</p> <p>在哥本哈根解释中,被波尔称作 “坍缩” (Collapse) 的过程,是一个瞬间发生的,先验的事件 (Event)。在双缝实验中,坍缩于光子撞墙的那一瞬间发生,转变为背景墙上的一个位置确定的亮点。爱因斯坦所说的 “上帝不掷骰子” 就是针对这个 <strong>没有被确切地解释清楚的坍缩过程</strong>。在坍缩过程中,原本光子有无数多个可能的位置,必须在那一瞬间就丢弃干净,只剩下一个唯一的测量结果,就好像掷骰子那样。</p> <p>我们回过头来,仍以 “双缝实验” 为例,进一步说明退相干是如何解释这个现象的。</p> <p>一句话,相干性仍然存在,只是被稀释了 (Again, <em>Quantum decoherence</em> is the loss of <em>quantum coherence</em>)。</p> <p>(以下引用部分摘自王孟源老师的 “量子去相干详解” 一问)</p> <p>(先看现象)</p> <blockquote> <p>在双缝实验中,被测量粒子是单个入射光子;“测量仪器”则是那面墙,测量的物理量是光子的位置。光子经过双缝的时侯,走A路和走B路的波函数是叠加的,这时A和B就是所谓的“相干”。这和古典粒子随机决定走A路和走B路不同,因为古典随机现象叠加的是机率密度,而量子现象叠加的是波函数。因为在隙缝处两个波函数叠加,所以从隙缝到墙之间,经过薛定谔方程的演化,在墙上的波函数分布仍然同时有A和B的贡献,于是产生了复杂的条纹,这叫做“干涉”。讨论到目前为止,都属于标准量子力学的范畴,可以用实验证明,没有任何争议。</p> </blockquote> <p>(再看解释)</p> <blockquote> <p>量子去相干则不一样。它说整个宇宙只有一个波函数,包含了每个粒子。相互之间没有作用的粒子,原本的多变数波函数可以Degenerate,分解成为个别粒子的波函数的简单乘积,所以光子的飞行过程,可以看作是单粒子波函数的演化。但是光子撞墙,就是它和墙内部的极大数量饱含热噪音的原子有了作用,这个过程绝对不能看作是单粒子波函数的自行演化,而必须是“光子+墙”(也就是”被测量粒子+测量仪器“)这个宏观系统的波函数的共同演化。而且因为墙处于凝态,每个原子的位置都是固定的,没有任何不同可能位置之间的不确定性和相干性,一旦光子和它作用,形成完美的纠缠,就以大欺小,把光子不同位置之间原本的相干性用巨量的原子稀释掉了(到非常接近于零,但不是数学上的零,只是物理上的零,亦即无法用实验与真零分辨出来)。这个过程叫做“去相干”;它不是什么神秘的新机制,而仍然遵守着薛丁格方程,只不过因为是数量极大的多体问题,所以不能有确解。这个过程的结果,不是一个纯态,而是”被测量粒子+测量仪器“之间的纠缠态。</p> </blockquote> <blockquote> <p>Einstein所说的“上帝不掷骰子”,指的是Copenhagen解释里,塌缩的过程中,原本光子有无限多个可能的位置,必须一瞬间丢弃近净,新的波函数里只剩下一个单一的测量结果,就像掷骰子一样。相对的,量子去相干解释里,原本光子的所有不同可能位置,仍然被包含在新的波函数里,并没有被舍弃;但是波函数已经不再能被视为个别粒子的波函数的简单乘积,而是一个宏观系统的波函数,所以不确定性仍然在,只是相干性被稀释光了。换句话说,新的波函数仍然包含着所有光子原本所有可能的位置a,b,c,d,e…,但是它们之间没有相干性,所以波函数在向量空间的表象里,出现了另一个与前不同的简化,是波函数分解成对应a,b,c,d,e…等等可能性的OR和(即只有一个能留存,量子力学里面叫做”Mixed States“),不论现实走上哪一条路,新波函数的演化都好像只对应着一个单一的测量结果。</p> </blockquote> <p>正如王孟源老师所说,“量子去相干不但没有任何定义上的困难,而且逻辑严谨自洽,推演过程自然,应用在没有人类的宇宙中,也完全没有问题”。</p> <hr> <h2 id="结语">结语</h2> <p>从王孟源老师对量子去相干的解释中,我对量子理论有了更进一步的理解,所谓的观察者与坍缩,不再建立在一个神奇的魔法般的孤立事件之上。宏观系统上的可被观测到的现象,可以看作是所有粒子经由他们在某种意义上共有的波函数所共同演化的结果。籍此,我们很容易获得一个一致,完整而超然的视角,去看待那些曾被认为是存在于 “独立系统” 中的所谓 “孤立事件”。</p> <h2 id="参考">参考</h2> <ol> <li><a class="link" href="https://en.wikipedia.org/wiki/Quantum_decoherence" target="_blank" rel="noopener" >Quantum decoherence - Wikipedia</a></li> <li><a class="link" href="https://en.wikipedia.org/wiki/Quantum_error_correction" target="_blank" rel="noopener" >Quantum error correction - Wikipedia</a></li> <li><a class="link" href="https://blog.udn.com/MengyuanWang/108908828" target="_blank" rel="noopener" >量子去相干详解 - 王孟源的部落格</a></li> <li><a class="link" href="https://www.zhihu.com/question/313273016" target="_blank" rel="noopener" >量子退相干到底是什么意思? - 知乎 (zhihu.com)</a></li> </ol> Gu Lu's Library https://gulu-dev.com/library/ Sun, 05 Jun 2022 00:00:00 +0000 https://gulu-dev.com/library/ <p>此库收录了我的大部分公开文档,演讲和随笔,作为一个中央归档库,方便未来的引用。<br> This library contains all categorized public writings created by Gu Lu for future reference.</p> <ul> <li>本页面链接: <a class="link" href="https://gulu-dev.com/library/" target="_blank" rel="noopener" >https://gulu-dev.com/library/</a></li> </ul> <h2 id="top-stories"><strong>Top Stories</strong></h2> <table> <thead> <tr> <th>编号</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>GD-1411</code></td> <td><a class="link" href="https://gulu-dev.com/post/2014-11-16-open-world" target="_blank" rel="noopener" ><strong>2014.11 开放世界游戏中的大地图背后有哪些实现技术?</strong></a> <br /> 2014 年时,关于开放世界的完整描述</td> </tr> <tr> <td><code>BG-1502</code></td> <td><a class="link" href="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics" target="_blank" rel="noopener" ><strong>2015.02 玩的就是资产! - 比特币与游戏货币体系</strong></a> <br /> 2015 年时,对比特币与游戏货币体系结合的可能性分析</td> </tr> <tr> <td><code>GD-1512</code></td> <td><a class="link" href="https://gulu-dev.com/post/2015-12-30-gtav-graphics" target="_blank" rel="noopener" ><strong>2015.12 GTA V 图形分析摘要</strong></a> <br /> 2015 年时,对 GTA V 的整个渲染系统所做的分析和总结</td> </tr> <tr> <td><code>GD-160a</code></td> <td><a class="link" href="https://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes" target="_blank" rel="noopener" ><strong>2016.07 id tech 网络模型演化</strong></a> <br /> 2016 年时,对 id tech 系列网络模型演化的提炼和小结</td> </tr> <tr> <td><code>VR-1612</code></td> <td><a class="link" href="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash" target="_blank" rel="noopener" ><strong>2016.12 VR 的未来五年</strong></a> <br /> 2016 年时,Michael Abrash 在 Oculus Connect 3 上的回顾与展望 (VR 必读)</td> </tr> <tr> <td><code>GD-1701</code></td> <td><a class="link" href="https://gulu-dev.com/post/2017-01-15-game-engine-talk" target="_blank" rel="noopener" ><strong>2017.01 游戏引擎技术点滴</strong></a> <br /> 2017 年时,对游戏引擎过去 10 年 (2006-2016) 发展的提炼和总结</td> </tr> <tr> <td><code>GD-1801</code></td> <td><a class="link" href="https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview" target="_blank" rel="noopener" ><strong>2018.01 Dice (EA) 工作室游戏开发技术概览</strong></a> <br /> 2018 年时,对 Dice 55 份技术资料提炼而成的技术体系小结</td> </tr> <tr> <td><code>B-1808</code></td> <td><a class="link" href="https://gulu-dev.com/post/2018-08-04-huobi-blockchain-game-industry-report" target="_blank" rel="noopener" ><strong>2018.08 火币“区块链+游戏”产业专题报告 (干货版)</strong></a> <br /> 一份超前3年的行业报告的干货版</td> </tr> <tr> <td><code>E-2004</code></td> <td><a class="link" href="https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory" target="_blank" rel="noopener" ><strong>2020.04 Wolfram 万物理论的简要解释</strong></a> <br /> 2020 年时,对 Stephen Wolfram 提出的万物理论的描述</td> </tr> <tr> <td><code>B-2106</code></td> <td><a class="link" href="https://gulu-dev.com/post/2021-06-06-sensible-interview-by-joshua" target="_blank" rel="noopener" ><strong>2021.06 Joshua 对感应合约的采访</strong></a> <br /> 入选 Top Stories 的唯一访谈,是我最喜欢的一篇深度访谈</td> </tr> </tbody> </table> <h2 id="categories"><strong>Categories</strong></h2> <h3 id="c-cc-编程"><strong><code>C++</code></strong> C/C++ 编程</h3> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>Cpp-1403</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-03-14-zhihu-c-books-recommendation" target="_blank" rel="noopener" >2014.03 C 语言学习的经典书籍有哪些?</a></td> </tr> <tr> <td><code>Cpp-1406</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2014-06-28-microsoft-crt" target="_blank" rel="noopener" >2014.06 昔时因 今日意 侃侃微软的CRT</a></td> </tr> <tr> <td><code>Cpp-1408</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2014-08-04-type-deduction" target="_blank" rel="noopener" >2014.08 (C++) template 为什么不能推导返回值类型?</a></td> </tr> <tr> <td><code>Cpp-1409</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2014-09-23-cppcon14" target="_blank" rel="noopener" >2014.09 CppCon2014 分类合辑 &amp; 十大推荐阅读列表</a></td> </tr> <tr> <td><code>Cpp-1507</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-07-22-cpp-multicast" target="_blank" rel="noopener" >2015.07 (C++) 一个可注销的通用多路回调列表</a></td> </tr> <tr> <td><code>Cpp-1510</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2015-10-11-memory-debugging" target="_blank" rel="noopener" >2015.10 CppCon2015 Memory and C++ debugging at EA</a></td> </tr> <tr> <td><code>Cpp-1512</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser" target="_blank" rel="noopener" >2015.12 (C++) 使用 std::tuple 和完美转发解析任意命令行字符串</a></td> </tr> <tr> <td><code>Cpp-1602</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-02-07-lvalue-rvalue" target="_blank" rel="noopener" >2016.02 (C++) 快速辨别左值和右值的两个方法</a></td> </tr> <tr> <td><code>Cpp-1605</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-05-19-cpp-lua-vargs" target="_blank" rel="noopener" >2016.05 类型安全的 C++/Lua 任意参数互调用</a></td> </tr> <tr> <td><code>Cpp-1611</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16" target="_blank" rel="noopener" >2016.11 CppCon 2014-2016 选荐合辑</a></td> </tr> </tbody> </table> <h3 id="sep-软件工程和管理"><strong><code>SEP</code></strong> 软件工程和管理</h3> <p>SEP = Software Engineering &amp; Project Management</p> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>SEP-1403</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-03-22-dont-lie" target="_blank" rel="noopener" >2014.03 (译) 不要说谎 (Don&rsquo;t lie.)</a></td> </tr> <tr> <td><code>SEP-1407</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-07-19-unit-test-fetish" target="_blank" rel="noopener" >2014.07 (译) 单元测试之迷思 (Unit Test Fetish)</a></td> </tr> <tr> <td><code>SEP-1412</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-12-14-efficiency-tradeoff" target="_blank" rel="noopener" >2014.12 开发效率与执行效率,我们应该怎样斟酌?</a></td> </tr> <tr> <td><code>SEP-1502</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2015-02-03-error-handling" target="_blank" rel="noopener" >2015.02 “Abort,Retry,Fail?” - 也谈错误处理</a></td> </tr> <tr> <td><code>SEP-1505</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-05-09-legacy-code" target="_blank" rel="noopener" >2015.05 入职后发现项目组代码异常混乱,是去是留?</a></td> </tr> <tr> <td><code>SEP-1511</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-11-05-evaluating-engineer" target="_blank" rel="noopener" >2015.11 在实际工作中评估你的工程师伙伴</a> <br /> 给非技术向小伙伴的参考</td> </tr> <tr> <td><code>SEP-2011</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-11-14-grasp-the-opportunities" target="_blank" rel="noopener" >2020.11 抓住机会</a></td> </tr> <tr> <td><code>SEP-2107</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-07-18-aero-robustness" target="_blank" rel="noopener" >2021.07 坚韧,易检和渐进故障 - 向航空行业学习健壮性</a></td> </tr> </tbody> </table> <h3 id="gd-游戏开发游戏引擎和架构"><strong><code>GD</code></strong> 游戏开发,游戏引擎和架构</h3> <p>GD = Game Dev &amp; Game Engine Dev</p> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>GD-1403</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-03-15-dynamic-prediction-and-latency-compensation" target="_blank" rel="noopener" >2014.03 客户端动态预测技术和延时补偿技术</a></td> </tr> <tr> <td><code>GD-1404</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-04-01-threaded-rendering" target="_blank" rel="noopener" >2014.04 为什么从本质上讲,渲染逻辑不适合放到子线程中去?</a></td> </tr> <tr> <td><code>GD-1405</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-05-16-plug-in-based-engine-design" target="_blank" rel="noopener" >2014.05 基于插件的引擎设计</a></td> </tr> <tr> <td><code>GD-1407</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-07-28-tech-evaluation" target="_blank" rel="noopener" >2014.07 如何判断一个技术(中间件/库/工具)的靠谱程度?</a></td> </tr> <tr> <td><code>GD-1411</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2014-11-16-open-world" target="_blank" rel="noopener" ><strong>2014.11 开放世界游戏中的大地图背后有哪些实现技术?</strong></a></td> </tr> <tr> <td><code>GD-1503</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue" target="_blank" rel="noopener" >2015.03 一个有趣的交互 bug ——兼谈游戏的引导系统</a></td> </tr> <tr> <td><code>GD-1510</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-10-31-ref-management" target="_blank" rel="noopener" >2015.10 利用文件摘要简化游戏资源的引用管理</a></td> </tr> <tr> <td><code>GD-1511</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-11-14-ref-management-2" target="_blank" rel="noopener" >2015.11 关于文件摘要的引申讨论 - 资源的引用管理之二</a></td> </tr> <tr> <td><code>GD-1512</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2015-12-30-gtav-graphics" target="_blank" rel="noopener" ><strong>2015.12 GTA V 图形分析摘要</strong></a></td> </tr> <tr> <td><code>GD-1602</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-02-01-occlusion-culling" target="_blank" rel="noopener" >2016.02 遮挡剔除的低端解决方案</a></td> </tr> <tr> <td><code>GD-1607</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-07-03-blizzcon2015-talks" target="_blank" rel="noopener" >2016.07 暴雪游戏开发趣闻 (若干则)</a></td> </tr> <tr> <td><code>GD-160a</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes" target="_blank" rel="noopener" >2016.07 id tech 网络模型演化 (1/3) DOOM3 技术点滴</a></td> </tr> <tr> <td><code>GD-160b</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution" target="_blank" rel="noopener" >2016.07 id tech 网络模型演化 (2/3) DOOM/Quake I/II/III 网络模型的演化</a></td> </tr> <tr> <td><code>GD-160c</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture" target="_blank" rel="noopener" >2016.08 id tech 网络模型演化 (3/3) DOOM3 网络架构</a></td> </tr> <tr> <td><code>GD-1701</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2017-01-15-game-engine-talk" target="_blank" rel="noopener" ><strong>2017.01 游戏引擎技术点滴</strong></a></td> </tr> <tr> <td><code>GD-1703</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-03-11-gdc17" target="_blank" rel="noopener" >2017.03 GDC 2017 技术选荐合辑</a></td> </tr> <tr> <td><code>GD-1704</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips" target="_blank" rel="noopener" >2017.04 从 GDC 分享中汲取养分</a></td> </tr> <tr> <td><code>GD-1709</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-09-15-font-pruner" target="_blank" rel="noopener" >2017.09 FontPruner 字体精简工具</a></td> </tr> <tr> <td><code>GD-1801</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview" target="_blank" rel="noopener" ><strong>2018.01 Dice (EA) 工作室游戏开发技术概览</strong></a></td> </tr> <tr> <td><code>GD-2106</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-06-28-roblox-claus-interview" target="_blank" rel="noopener" >2021.06 (Roblox) Claus Moberg 访谈简短记录</a></td> </tr> </tbody> </table> <h3 id="unity-unity-相关"><strong><code>Unity</code></strong> Unity 相关</h3> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>UNITY-1506</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips" target="_blank" rel="noopener" >2015.06 Unity 项目实践点滴</a> <br /> - <a class="link" href="https://github.com/mc-gulu/u3d_practice" target="_blank" rel="noopener" >u3d_practice</a></td> </tr> <tr> <td><code>UNITY-1507</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-07-11-u3d-uquadtree" target="_blank" rel="noopener" >2015.07 UQuadtree - 在 Unity 下实现场景资源的动态管理</a> <br /> - <a class="link" href="https://github.com/mc-gulu/uquadtree" target="_blank" rel="noopener" >uquadtree</a></td> </tr> <tr> <td><code>UNITY-1508</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-08-01-u3d-umetalod" target="_blank" rel="noopener" >2015.08 UMetaLod - 一个通用的增强版 LOD 方案</a> <br /> - <a class="link" href="https://github.com/mc-gulu/umetalod" target="_blank" rel="noopener" >umetalod</a></td> </tr> <tr> <td><code>UNITY-1606</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua" target="_blank" rel="noopener" >2016.06 测量被 Lua 隔断的 Unity C# 代码性能</a> <br /> - <a class="link" href="https://gist.github.com/mc-gulu/fdc154e072055ba9369557acb74461c9" target="_blank" rel="noopener" >slua-codegen-profiler-support</a></td> </tr> <tr> <td><code>UNITY-1608</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-08-30-unity-external-dll-debugging" target="_blank" rel="noopener" >2016.08 为动态加载的 Unity C# DLL 添加调试支持</a> <br /> - <a class="link" href="https://github.com/mc-gulu/gl-bits/tree/master/%282016%29%2001.%20Debugging%20C%23%20External%20DLL%20%28Unity%29" target="_blank" rel="noopener" >Debugging C# External DLL</a></td> </tr> <tr> <td><code>UNITY-1611</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-11-22-unity-string-interning" target="_blank" rel="noopener" >2016.11 Unity 游戏的 string interning 优化</a> <br /> - <a class="link" href="https://github.com/PerfAssist/PA_Common/blob/master/UniqueString.cs" target="_blank" rel="noopener" >UniqueString</a></td> </tr> <tr> <td><code>UNITY-1612</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing" target="_blank" rel="noopener" >2016.12 Unity 协程运行时的监控和优化</a> <br /> - <a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker" target="_blank" rel="noopener" >CoroutineTracker</a></td> </tr> <tr> <td><code>UNITY-1701</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-01-25-unity-memoryprofiler" target="_blank" rel="noopener" >2017.01 Unity MemoryProfiler 的工作机制及可能的改进</a> <br /> - <a class="link" href="https://github.com/PerfAssist/PA_ResourceTracker" target="_blank" rel="noopener" >ResourceTracker</a></td> </tr> <tr> <td><code>UNITY-172</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-02-18-unity-gc-cheatsheet" target="_blank" rel="noopener" >2017.02 Unity GC Cheatsheet</a> <br /> - Tips and practices of Unity GC.</td> </tr> </tbody> </table> <h3 id="vr-虚拟现实相关"><strong><code>VR</code></strong> 虚拟现实相关</h3> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>VR-1510</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash" target="_blank" rel="noopener" >2015.10 Oculus Connect 2 首席科学家 Michael Abrash 发言实录</a></td> </tr> <tr> <td><code>VR-1612</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash" target="_blank" rel="noopener" ><strong>2016.12 Oculus Connect 3 - VR 的未来五年</strong></a> <br /> Michael Abrash 在 OC3 上的回顾与展望</td> </tr> </tbody> </table> <h3 id="dev-一般的开发技术"><strong><code>DEV</code></strong> 一般的开发技术</h3> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>DEV-1406</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-06-08-nanomsg" target="_blank" rel="noopener" >2014.06 nanomsg - zmq 的华丽转身</a></td> </tr> <tr> <td><code>DEV-1505</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-05-01-appveyor-ci" target="_blank" rel="noopener" >2015.05 利用 AppVeyor 实现 GitHub 托管项目的自动化集成</a></td> </tr> <tr> <td><code>DEV-1602</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-02-02-kill-goroutine" target="_blank" rel="noopener" >2016.02 从外部结束一个 goroutine (Go)</a></td> </tr> <tr> <td><code>DEV-1604</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-04-17-premake-for-android" target="_blank" rel="noopener" >2016.04 使用 Premake 自动化 Android 编译脚本的维护</a></td> </tr> <tr> <td><code>DEV-1704</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2017-04-16-visual-assist-tips" target="_blank" rel="noopener" >2017.04 Visual Assist 特性和技巧 (2017)</a></td> </tr> <tr> <td><code>DEV-1705</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-05-18-telemetry" target="_blank" rel="noopener" >2017.05 Telemetry 3 特性指南 (2017)</a></td> </tr> </tbody> </table> <h3 id="b-区块链相关"><strong><code>B</code></strong> 区块链相关</h3> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>B-1502</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics" target="_blank" rel="noopener" ><strong>2015.02 玩的就是资产! - 比特币与游戏货币体系</strong></a></td> </tr> <tr> <td><code>B-1707</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-07-17-one-million-video-cards" target="_blank" rel="noopener" >2017.07 百万张高端显卡的30天集结</a></td> </tr> <tr> <td><code>B-1708</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-08-13-to-da-moon" target="_blank" rel="noopener" >2017.08 To Da Moon (for the 5th time)</a></td> </tr> <tr> <td><code>B-1709</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-09-07-the-power-in-dac" target="_blank" rel="noopener" >2017.09 为什么说在去中心化的系统中,对权力的运用会削弱权力本身?</a></td> </tr> <tr> <td><code>B-1711</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend" target="_blank" rel="noopener" >2017.11 逆转?是的,也许就在本周末</a></td> </tr> <tr> <td><code>B-1808</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2018-08-04-huobi-blockchain-game-industry-report" target="_blank" rel="noopener" ><strong>2018.08 火币“区块链+游戏”产业专题报告 (干货版)</strong></a></td> </tr> <tr> <td><code>B-1907</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2019-07-07-on-chain-computing-evolving-from-eth-to-bsv" target="_blank" rel="noopener" >2019.07 链上运算:从 ETH 到 BSV</a></td> </tr> <tr> <td><code>B-1909</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink" target="_blank" rel="noopener" >2019.09 区块链与游戏结合的再思考</a></td> </tr> <tr> <td><code>B-1910</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2019-10-28-bitcoin-quantum-resistance" target="_blank" rel="noopener" >2019.10 比特币的量子抵抗</a></td> </tr> <tr> <td><code>B-1912</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2019-12-29-why-hate-bsv" target="_blank" rel="noopener" >2019.12 为什么这么多人讨厌 Bitcoin SV?</a></td> </tr> <tr> <td><code>B-2001</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment" target="_blank" rel="noopener" >2020.01 BSV 中国大会的动容一刻</a></td> </tr> <tr> <td><code>B-2005</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2020-05-27-cobra-subjective-money" target="_blank" rel="noopener" >2020.05 主观货币 (Subjective Money)</a></td> </tr> <tr> <td><code>B-2009</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-09-21-use-bsv-in-games" target="_blank" rel="noopener" >2020.09 为什么要把 BSV 用在游戏(这个应用场景)里?</a></td> </tr> <tr> <td><code>B-2010</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2020-10-04-app-layer-protocol" target="_blank" rel="noopener" >2020.10 BSV 线上研讨会:BSV 应用层协议</a></td> </tr> <tr> <td><code>B-201a</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-10-19-kingsoft-visit/" target="_blank" rel="noopener" >2020.10 金山区块链交流</a></td> </tr> <tr> <td><code>B-2104</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2" target="_blank" rel="noopener" >2021.04 BSV 开发技术与工具概览 (v2)</a></td> </tr> <tr> <td><code>B-2105</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-05-01-sensible-intro" target="_blank" rel="noopener" >2021.05 感应合约的简介和基本原理</a></td> </tr> <tr> <td><code>B-2106</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2021-06-06-sensible-interview-by-joshua" target="_blank" rel="noopener" ><strong>2021.06 Joshua 对感应合约的采访</strong></a></td> </tr> <tr> <td><code>B-210a</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-06-29-dark-forest" target="_blank" rel="noopener" >2021.06 Dark Forest - 基于零知识证明与区块链的元宇宙构建</a></td> </tr> <tr> <td><code>B-2107</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs" target="_blank" rel="noopener" ><strong>2021.07 随机漫步 · 零知识证明</strong></a></td> </tr> <tr> <td><code>B-2108</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-08-28-metaverse-intro" target="_blank" rel="noopener" >2021.08 An intro of metaverse</a></td> </tr> </tbody> </table> <h3 id="mmp-心智模型方法论和效率"><strong><code>MMP</code></strong> 心智模型,方法论和效率</h3> <p>MMP = Mindset, Methodology &amp; Productivity</p> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>MMP-1403</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-03-13-zhihu-answer-thinking-method" target="_blank" rel="noopener" >2014.03 人类历史上有哪些思维能力特别强的人?他们有哪些独特的思考方法?</a></td> </tr> <tr> <td><code>MMP-1503</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-03-15-handwriting" target="_blank" rel="noopener" >2015.03 如何对手写笔记进行漂亮和高效的排版?</a></td> </tr> <tr> <td><code>MMP-1604</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-04-09-a-working-day" target="_blank" rel="noopener" >2016.04 我的日常一天</a></td> </tr> <tr> <td><code>MMP-1605</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-05-07-sand-of-time" target="_blank" rel="noopener" >2016.05 时之沙 - 我对时间的理解和领悟</a></td> </tr> <tr> <td><code>MMP-1609</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-09-12-time-division" target="_blank" rel="noopener" >2016.09 我的时间分配变迁记 (原问题:程序员工作中占时间最长的是哪个步骤?)</a></td> </tr> <tr> <td><code>MMP-1611</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-11-07-timestamp" target="_blank" rel="noopener" >2016.11 秒打时间戳 (日常生活中有哪些十分钟就能学会并可以终生受用的技能?)</a></td> </tr> <tr> <td><code>MMP-1612</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload" target="_blank" rel="noopener" >2016.12 无压应对信息过载 - 2016 效率小结</a></td> </tr> <tr> <td><code>MMP-1707</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-07-04-20-questions" target="_blank" rel="noopener" >2017.07 软件工程师的睡前二十问</a></td> </tr> <tr> <td><code>MMP-2202</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2022-02-26-an-understanding-of-work/" target="_blank" rel="noopener" >2022.02 对工作的阶段性理解</a></td> </tr> <tr> <td><code>MMP-2301</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2023-01-01-8760-hours" target="_blank" rel="noopener" >2023.01 8760 小时</a></td> </tr> <tr> <td><code>MMP-230a</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2023-01-07-commander" target="_blank" rel="noopener" >2023.01 怎样当好一名师长 (notes)</a></td> </tr> </tbody> </table> <h3 id="cnc-中国文化"><strong><code>CNC</code></strong> 中国文化</h3> <p>CNC = Chinese Culture</p> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>CNC-1504</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-04-06-chinese-culture-homework" target="_blank" rel="noopener" >2015.04 《中国文化概论》单元作业两则</a></td> </tr> <tr> <td><code>CNC-1506</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-06-14-chinese-culture-homework-2" target="_blank" rel="noopener" >2015.06 《中国文化概论》第十一周作业 - 诗性文体书写实践</a></td> </tr> <tr> <td><code>CNC-1507</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-07-25-chinese-culture-completed" target="_blank" rel="noopener" >2015.07 《中国文化概论》所有相关资源 (附 GitHub 地址)</a></td> </tr> <tr> <td><code>CNC-1604</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-04-24-moment-in-peking" target="_blank" rel="noopener" >2016.04 《京华烟云》 摘录</a></td> </tr> <tr> <td><code>CNC-1702</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2017-02-12-history-the-economic-view" target="_blank" rel="noopener" >2017.02 《王朝的家底》 记录</a></td> </tr> <tr> <td><code>CNC-1708</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2017-08-29-tao" target="_blank" rel="noopener" >2017.08 道可道,非常道;名可名,非常名。</a></td> </tr> <tr> <td><code>CNC-2001</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-01-26-ji-kang/" target="_blank" rel="noopener" >2020.01 《嵇康之死》小记</a></td> </tr> </tbody> </table> <h3 id="essays-散文和随笔"><strong>Essays</strong> 散文和随笔</h3> <table> <thead> <tr> <th>编号</th> <th>推荐</th> <th>标题</th> </tr> </thead> <tbody> <tr> <td><code>E-1403</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai" target="_blank" rel="noopener" >2014.03 迁居 · 珠海</a></td> </tr> <tr> <td><code>E-1405</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-05-31-review-of-weixin-air-combat" target="_blank" rel="noopener" >2014.05 全民飞机大战 - 简评和碎碎念</a></td> </tr> <tr> <td><code>E-1406</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-06-02-gulu-dev-com-available" target="_blank" rel="noopener" >2014.06 独立域名 gulu-dev.com 已经可以访问</a></td> </tr> <tr> <td><code>E-1411</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on" target="_blank" rel="noopener" >2014.11 Surface Pro 3 上手体验</a></td> </tr> <tr> <td><code>E-1506</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil" target="_blank" rel="noopener" >2015.06 “千年故纸空读尽,恨把衣冠祭九州” - 小记《三国志11之血色衣冠》</a></td> </tr> <tr> <td><code>E-1510</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-10-04-evernote-are-you-ok" target="_blank" rel="noopener" >2015.10 Evernote 你还好吗?</a></td> </tr> <tr> <td><code>E-1512</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2015-12-18-two-stories" target="_blank" rel="noopener" >2015.12 小故事二则</a></td> </tr> <tr> <td><code>E-1601</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-01-01-hello-2016" target="_blank" rel="noopener" >2016.01 你好 2016 (统计,汇总及十大)</a></td> </tr> <tr> <td><code>E-1604</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2016-04-20-grow-up-with-your-children" target="_blank" rel="noopener" >2016.04 和孩子一起长大</a></td> </tr> <tr> <td><code>E-2004</code></td> <td>⭐</td> <td><a class="link" href="https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory" target="_blank" rel="noopener" ><strong>2020.04 Wolfram 万物理论的简要解释</strong></a></td> </tr> <tr> <td><code>E-2005</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-05-16-craig-about-property-right" target="_blank" rel="noopener" >2020.05 Craig 关于财产权的说明 (2015)</a></td> </tr> <tr> <td><code>E-2010</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-10-04-blog-migration-to-hugo" target="_blank" rel="noopener" >2020.10 Blog Migration to Hugo</a></td> </tr> <tr> <td><code>E-2011</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2020-11-06-michael-jordan" target="_blank" rel="noopener" >2020.11 《乔丹传奇》 (段旭) 阅读笔记</a></td> </tr> <tr> <td><code>E-2102</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2021-02-19-practical-reading" target="_blank" rel="noopener" >2021.02 《实用性阅读指南》 阅读笔记</a></td> </tr> <tr> <td><code>E-2204</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2022-04-30-build-a-better-blog/" target="_blank" rel="noopener" >2022.04 整理文章并为 gulu-dev.com 更换样式</a></td> </tr> <tr> <td><code>E-2206</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre" target="_blank" rel="noopener" >2022.06 在 Kindle 中国停运前导出所有 Kindle 电子书</a></td> </tr> <tr> <td><code>E-2207</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2022-07-23-quantum-decoherence-explained" target="_blank" rel="noopener" >2022.07 量子退相干的简要解释</a></td> </tr> <tr> <td><code>E-2212</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2022-12-10-family-education" target="_blank" rel="noopener" >2022.12 家庭教育心得 9 条</a></td> </tr> <tr> <td><code>E-2405</code></td> <td></td> <td><a class="link" href="https://gulu-dev.com/post/2024-mobile-games-and-children" target="_blank" rel="noopener" >2024.05 手机游戏对孩子的影响和应对</a></td> </tr> </tbody> </table> <hr> <h2 id="版权声明"><strong>版权声明</strong></h2> <p>除非针对部分内容特别的声明,此库中所有的内容都遵循 <a class="link" href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener" ><strong>CC BY-NC-ND 4.0</strong></a> 。<br> Except where otherwise noted, content in this repository is licensed under a <strong>Creative Commons Attribution 4.0 International license</strong> (<a class="link" href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener" >CC BY-NC-ND 4.0</a>).</p> <p>除非针对部分内容的特别声明,此库中所有的代码(含片段,项目)都遵循 <a class="link" href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener" ><strong>MIT 开源协议</strong></a>。<br> Except where otherwise noted, code (including snippets, projects) in this repository is licensed under <a class="link" href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener" ><strong>The MIT License</strong></a>.</p> <hr> <h2 id="修订历史"><strong>修订历史</strong></h2> <ul> <li><code>2022-06-27</code> 完成分类和索引工作</li> <li><code>2022-06-05</code> 开始整理 blog 文章并建立分类索引</li> </ul> 2022.06 在 Kindle 中国停运前导出所有 Kindle 电子书 https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/ Sat, 04 Jun 2022 00:00:00 +0000 https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/ <img src="proxy.php?url=https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/title.jpg" alt="Featured image of post 2022.06 在 Kindle 中国停运前导出所有 Kindle 电子书" /><h3 id="kindle-中国停止运营">Kindle 中国停止运营</h3> <p>前两天 <a class="link" href="https://www.thepaper.cn/newsDetail_forward_18393771" target="_blank" rel="noopener" >Kindle 将停止运营的消息</a> 上了热搜:</p> <p><img src="https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/kindle.jpg" width="1080" height="530" srcset="https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/kindle_hu9781cc2e0d6912f4ca24012dc179ba8f_88799_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/kindle_hu9781cc2e0d6912f4ca24012dc179ba8f_88799_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="时间表" class="gallery-image" data-flex-grow="203" data-flex-basis="489px" ></p> <p>简单说,你需要及时地把自己账号中的电子书下载到本地设备里,因为一段时间以后,这些数字内容就无法访问和下载。在那以后,如果设备卖出或损坏,这些资源就无法从云端恢复了。</p> <p>我打开自己账号翻了翻,这些年在 Kindle 里还是攒了些好书。虽然现在 Kindle 没有以前用得多了,想想就这么消失了,也蛮惆怅的。借这个机会看能不能导出成开放格式独立保存吧。</p> <h3 id="电子书导出和转换">电子书导出和转换</h3> <p>搜了一下,照着网上的文章试了下,发现是可行的,顺便简化了一下流程。</p> <ol> <li>首先先准备好需要的几个软件 <ul> <li>Kindle for PC (用于获取所有已购买的电子书)</li> <li>Calibre 及对应的 DeDRM 插件(用于转换和保存)</li> </ul> </li> <li>在 Kindle for PC 上 <strong>登录亚马逊账号</strong>,并把所有的电子书下载到本地</li> <li>然后在 Calibre 中,选择 “<strong>添加书籍</strong> | <strong>从文件夹和子文件夹添加</strong>” <ul> <li>此时填入 Kindle 的同步目录 <code>C:\Users\&lt;账户名&gt;\Documents\My Kindle Content</code></li> <li>这样 Calibre 将自动扫描和识别所有的电子书,并添加到你的书库里</li> </ul> </li> <li>等待导入完成后,所有的电子书就可以在 Calibre 内阅读了。</li> </ol> <p><img src="https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/boox.jpg" width="1024" height="768" srcset="https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/boox_huc0a50afbca80711f80fdca6aa642c3da_164887_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2022-06-04-save-kindle-ebooks-into-calibre/boox_huc0a50afbca80711f80fdca6aa642c3da_164887_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="我的阅读器" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>扫描和添加完成后,我顺手把所有的电子书都由 <code>azw3</code> 转为了更加通用,兼容性更好的 <code>epub</code>,就可以随手发到 BOOX 里看了。</p> <h3 id="参考">参考</h3> <ol> <li><a class="link" href="https://zhuanlan.zhihu.com/p/54391402" target="_blank" rel="noopener" >手把手教你导出kindle里的电子书并转成pdf/mobi</a></li> <li><a class="link" href="https://zhuanlan.zhihu.com/p/297715462" target="_blank" rel="noopener" >教你怎么把Kindle电子书导出并且转换为PDF格式</a></li> </ol> 2022.04 整理文章并为 gulu-dev.com 更换样式 https://gulu-dev.com/post/2022-04-30-build-a-better-blog/ Sat, 30 Apr 2022 00:00:00 +0000 https://gulu-dev.com/post/2022-04-30-build-a-better-blog/ <img src="proxy.php?url=https://gulu-dev.com/post/2022-04-30-build-a-better-blog/title.png" alt="Featured image of post 2022.04 整理文章并为 gulu-dev.com 更换样式" /><h2 id="缘起">缘起</h2> <p>我这个博客 <a class="link" href="https://gulu-dev.com" target="_blank" rel="noopener" >gulu-dev.com</a> 的老读者可能知道,我有一些文章(主要是 2014-2020 年间)在 2020 年从 Bitcron 上迁出时,没有移到新的 blog 上来,不少知乎上的回答内的链接也因此失效了。</p> <p>我一直想把这些文章整理出来,并在 <a class="link" href="https://gulu-dev.com/library/" target="_blank" rel="noopener" >https://gulu-dev.com/library/</a> 中清理好。 趁着五一假期,就把这件一直想做都还没来得及做的事完成吧。</p> <h2 id="挑选主题">挑选主题</h2> <p>先选个更合适的主题,可以简化后续的整理。之前用的 hugo-clarity 缺少按照年份索引的 Archives 页面,很不方便,这次要挑一个索引功能更好的。</p> <p>在 <a class="link" href="https://themes.gohugo.io/tags/blog/" target="_blank" rel="noopener" >Hugo Themes</a> 上挑了下,看中了 <a class="link" href="https://themes.gohugo.io/themes/hugo-theme-pure/" target="_blank" rel="noopener" >Pure</a> 这个样式很不错,但看起来似乎有段时间没有维护了。继续找,看到了 <a class="link" href="https://themes.gohugo.io/themes/hugo-theme-stack/" target="_blank" rel="noopener" >Stack</a>,这个挺简洁的,索引和布局看起来也都不错,有完善的年份分隔,还有一点很重要的是不管 Post 是否有图片都可以叠加在一起不违和,就是它了。</p> <p>Stack 的优点:</p> <ol> <li>由年份分隔的 Archive 索引页面</li> <li>对有/无图片的 Post 都很友好</li> <li>右边有方便阅读的大纲视图</li> </ol> <h2 id="开始动手">开始动手</h2> <p>先把 <code>hugo</code> 本地从 0.75 升级到 0.98</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">choco</span> <span class="n">upgrade</span> <span class="n">hugo</span> <span class="n">-confirm</span> </span></span></code></pre></td></tr></table> </div> </div><p>使用 Stack 模板后报错。检查发现是 Hugo Extended 没有同步更新,于是</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">choco</span> <span class="n">upgrade</span> <span class="nb">hugo-extended</span> <span class="n">-confirm</span> </span></span></code></pre></td></tr></table> </div> </div><p>把 <code>hugo-extended</code> 同步从 0.74 升级到 0.98</p> <p>然后 <code>hugo server</code> 就正常启动了</p> <p><img src="https://gulu-dev.com/post/2022-04-30-build-a-better-blog/default_page.png" width="1631" height="930" srcset="https://gulu-dev.com/post/2022-04-30-build-a-better-blog/default_page_hu5836fda9595696ff843fcd96a1b2559b_421439_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2022-04-30-build-a-better-blog/default_page_hu5836fda9595696ff843fcd96a1b2559b_421439_1024x0_resize_box_3.png 1024w" loading="lazy" alt="default looking" class="gallery-image" data-flex-grow="175" data-flex-basis="420px" ></p> <h2 id="内容整理">内容整理</h2> <p>接下来我会把我也出来了整理出的文章都补充到这里。,我们看看五一期间能做到什么程度。</p> 2022.02 对工作的阶段性理解 https://gulu-dev.com/post/2022-02-26-an-understanding-of-work/ Sat, 26 Feb 2022 00:00:00 +0000 https://gulu-dev.com/post/2022-02-26-an-understanding-of-work/ <img src="proxy.php?url=https://gulu-dev.com/post/2022-02-26-an-understanding-of-work/outline.png" alt="Featured image of post 2022.02 对工作的阶段性理解" /><p>在工作了 17 年以后,关于工作本身的一点阶段性的理解。</p> <h2 id="三个阶段">三个阶段</h2> <p>大学毕业以来,经过三个阶段:</p> <ol> <li>第1个阶段是 <code>提升</code> (2005-2011),在工作中不断成长,提升技能,获得相对完整的经验轨迹。</li> <li>第2个阶段是 <code>平衡</code> (2012-2018),寻求和保持工作和生活的动态平衡。</li> <li>第3个阶段是 <code>融合</code> (2018-2021),把工作融入生活,内化为一种生活方式。</li> </ol> <p>不同的阶段之间,并不是变化和迁移,而是一个 <strong>逐渐累加</strong> 的过程。</p> <p>也就是说,第3个阶段实际上是三者的叠加 (提升 + 平衡 + 融合)。</p> <p>在三个阶段里,工作的目的是比较明确的:</p> <ul> <li>努力成长,获得社会性认可</li> <li>挣脱束缚,获得对时间的支配自由</li> <li>回归自我,实现内在的平静</li> </ul> <h2 id="广义的工作">广义的工作</h2> <p>去年以来,我的视角逐渐发生了改变。</p> <p>回过头来看待工作本身,于我而言,工作的内容,性质和外延都不同程度上发生了较大的变化。</p> <p>我不再有那种驴子拉磨,被外物驱赶不断向前的感觉了。</p> <p>之前对工作的狭义理解,也逐渐被广义理解所替代。</p> <p>广义的工作,跟行业,资本,公司,团队,客户,等等 &hellip; 统统没有关系。</p> <p>广义的工作,是指在必要时<strong>以恰当的姿态应对和处理</strong>那些需要认真对待的事 。(即所谓【真常应物】)</p> <h2 id="举个例子">举个例子</h2> <p>举个例子,对孩子的编程教育,在一开始没有找到恰当的方式,抱着寓教于乐,随便讲讲的心态,有效的互动没有建立起来,其实效率是很低的。</p> <p>一年下来都是碎片性的知识合集,而这一类信息完全可以通过阅读百科全书之类的图书获取。</p> <p>因此这种教育不管从方式上还是结果上都是失败的。</p> <p>后来调整为以工作的心态来面对这件事,整个事情就像流水一样自然而然的发生。</p> <p>总得来说,方向对了,花不了多少时间,就能达到好得多的效果。</p> 2021.08 An intro of metaverse https://gulu-dev.com/post/2021-08-28-metaverse-intro/ Sat, 28 Aug 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-08-28-metaverse-intro/ <p><code>Metaverse</code> 系列相关材料第四篇,是周五做的一个简单的分享。</p> <ul> <li><a class="link" href="2021-08-27-metaverse.pdf" >PDF 文件</a></li> <li><a class="link" href="https://mubu.com/doc/FrXNzBhGZ6" target="_blank" rel="noopener" >脚本 (幕布)</a></li> </ul> 2021.07 坚韧,易检和渐进故障 - 向航空行业学习健壮性 https://gulu-dev.com/post/2021-07-18-aero-robustness/ Sun, 18 Jul 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-07-18-aero-robustness/ <img src="proxy.php?url=https://gulu-dev.com/post/2021-07-18-aero-robustness/aero-robustness.jpg" alt="Featured image of post 2021.07 坚韧,易检和渐进故障 - 向航空行业学习健壮性" /><p>在知乎上读到的很好的答案,航空工业的健壮性经验同样适用于大多数人造系统。</p> <h1 id="坚韧易检和渐进故障---向航空行业学习健壮性">坚韧,易检和渐进故障 - 向航空行业学习健壮性</h1> <h2 id="要点">要点</h2> <ol> <li><strong>坚韧性</strong> (反脆弱 - 鲁棒和冗余) <ul> <li>别说有一个两个 “松动”,就是中了几发甚至十几发航炮&hellip;也有相当强韧的生存力</li> <li>&ldquo;先进意味着精密,但精密绝不意味着脆弱。&rdquo;</li> </ul> </li> <li><strong>易检性</strong> (把异常变得易见 - 保险丝和张力线) <ul> <li>把所有的待检项目在逻辑上安排成若干个递进层次——如果最外层的指标没问题,内层将会有非常刚性的逻辑保证你可以无需拆检。</li> </ul> </li> <li><strong>渐进性</strong> (提供应急时间窗口 - 亚健康区间) <ul> <li>当确实出现问题的时候,会有一个性能下降的过程,而不是直接崩溃。</li> </ul> </li> </ol> <hr> <h2 id="原文">原文</h2> <p><a class="link" href="https://www.zhihu.com/question/463612668/answer/1938249553" target="_blank" rel="noopener" >飞机上那么多零件,每一个松动都会导致严重后果,那么每次起飞之前是需要全部检查一遍吗? - 知乎 (zhihu.com)</a></p> <p>作者:John Hexa<br> 链接:https://www.zhihu.com/question/463612668/answer/1938249553<br> 来源:知乎<br> 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</p> <p>#螺栓松了#</p> <p>这里面有很多误解,也对系统化设计思想欠缺了解。</p> <p>首先,最初的假设是错误的——“每一个松动都会造成严重后果”。</p> <p>实际上现代航空器的设计是非常坚韧的。别说有一个两个 “松动”,就是中了几发甚至十几发航炮或者导弹碎片,被打掉一截机翼、失去一组舵面、甚至失去一侧发动机,也有相当强韧的生存力。</p> <p>否则它不是因为拿不到适航证根本不准飞,就是因为维护成本太高、勤务性太差而没有订单。</p> <p>先进当然意味着精密,但精密绝不意味着脆弱。</p> <p>第二,一个合理的设计从一开始就会考虑到关键组件的易检性,这是一个非常基本的设计思想。</p> <p>说简单点,就是会<strong>把异常变得易见</strong>。</p> <p>这种设计思想可以举个通俗的例子——比如在一个电路里安排一根最脆弱的保险丝。在加压之后这根保险丝没断,就可知其它环节一定能耐住压力。</p> <p>又比如一根脆弱的张力线穿起一连串有结构位置要求的点,只要检验这根线有无变形、是否断裂,就知道这一连串的点发生了什么幅度甚至什么方向的变形。</p> <p>设计师会使用这种思想把所有的待检项目在逻辑上安排成若干个递进层次——如果最外层的指标没问题,内层将会有非常刚性的逻辑保证你可以无需拆检。</p> <p>而第一层会摆在最容易快速检查的地方,甚至是直接由传感器监控的。</p> <p>换句话来说——勤务要求高的现代战斗机基本上不太会存在 “要拆开了才知道有没有问题” 这个场景。</p> <p>否则它一开始就可以说是没有可用性的。</p> <p>简单来说,需要被 “检查” 的“螺栓”会被设计成“要坏一定会是它先坏”,而且总是会被摆在打开检修盖板一眼就看得到的地方。</p> <p>至于看它有没有松动,如果不是直接有结构设计使得它非常显眼——比如在螺帽上和孔位旁直接有对位标记或者干脆有易损封条(一转动就会拉断),就是干脆有传感器可以直接自检。</p> <p>这是自带在设计要求里的,用不着担心。</p> <p>第三,影响性能的关键组件,设计上会极力避免出现性能陡降。</p> <p>也就是当确实出现问题的时候,会有一个性能下降到 90%、80%、70%、60% 的过程,而且这个过程不太会非常急促以至于你无任何变通应急。它极少会被设计成一旦出问题就完全失能、像啪嗒一声关灯那样从全亮变成全暗。</p> <p>在绝大多数情况下,航空器的 “问题” 实际上都不能被称为“损坏”,而只能被称为“衰减”,即“没有处在最佳状态”。</p> <p>更类似 “胎压不足”“耗油量略高”,“飞行阻力稍高” 这类 “亚健康” 问题。</p> 2021.07 随机漫步 · 零知识证明 https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/ Sun, 04 Jul 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/ <img src="proxy.php?url=https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/ZKPs.png" alt="Featured image of post 2021.07 随机漫步 · 零知识证明" /><p>对一个给定的秘密信息 x,一方可以在不泄漏任何 x 相关信息(零知识)的情况下,向另一方证明 “自己知道 x” 这个事实。</p> <h2 id="被-重新发现-的零知识证明">被 “重新发现” 的零知识证明</h2> <p>“零知识证明” (<code>Zero-Knowledge Proofs</code>, <code>ZKPs</code>) 这样一个技术向的概念,近期怎么“突然”热起来了?</p> <p>(好吧,确实还没有像 “元宇宙” 那样破圈成功)</p> <p>先看看 <a class="link" href="https://twitter.com/jillruthcarlson/status/1396660616991895555" target="_blank" rel="noopener" >Jill Carlson 的这条推</a>:</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/prediction.png" width="591" height="406" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/prediction_hucfbef495fbcb3654e473fbcb42694ac2_44112_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/prediction_hucfbef495fbcb3654e473fbcb42694ac2_44112_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Jill&rsquo;s Prediction" class="gallery-image" data-flex-grow="145" data-flex-basis="349px" ></p> <blockquote> <p>Jill 的预测:在接下来的 5 年中,我们将看到人们像讨论区块链应用那样讨论零知识的应用。此前的突破所带来的潜力将会被主流认可。</p> </blockquote> <p>另外有两条有趣的推:</p> <blockquote> <ol> <li>在 “零知识证明” 里,“零知识” 是关于隐私/保密 (privacy/confidentiality) 的,而 “证明” 却是关于完整性 (integrity) 的。此技术目前仍是被严重低估的状态。(<a class="link" href="https://twitter.com/zooko/status/1409306242716078081" target="_blank" rel="noopener" >from @zooko</a>)</li> <li>技术成熟时,你对区块链的需求将会极小化,而对零知识的需求将会极大化。区块链将仅提供数据的可用性和共识的保障,所有的执行都将会是对证据的验证。 (<a class="link" href="https://twitter.com/zmanian/status/1396686778443042816" target="_blank" rel="noopener" >from @zmanian</a>)</li> </ol> </blockquote> <p>看起来 ZKPs 虽然概念上 1985 年前就被提出来了,但是确实是通过与区块链结合,在一系列应用中,它的价值被 “重新发现” 了。</p> <h3 id="2021-年度阿贝尔奖">2021 年度阿贝尔奖</h3> <p>今年 3 月份,零知识证明的相关研究者,以色列计算机科学家 Avi Wigderson 与匈牙利数学家拉兹洛・洛瓦兹(László Lovász)<a class="link" href="https://www.nature.com/articles/d41586-021-00694-9" target="_blank" rel="noopener" >一同获得了 2021 年度的阿贝尔奖</a>(据说设立此奖的一个原因也是因为诺贝尔奖没有数学奖项,奖金的数额也大致同诺贝尔奖相近)。</p> <p>值得一提的是 Wigderson 非常厉害,这里跑个题,简单说一下他的两大成就:</p> <p>其一是关于<strong>随机性在计算中的作用</strong>。在很多情况下,比如寻找迷宫的出口,掷骰子往往能让算法更快地找到解答。Wigderson 与合作者在 90 年代证明,如果使用了随机性的算法看起来很高效,那么必定存在另一种同样高效的非随机算法(能达到同样的目的)。这从理论上确保了随机算法确实可以找到高效地找到正确的解答 。</p> <blockquote> <p>“A lot of programs practically run much faster if you allow them to do this random choice.” - Peter Sarnak (a number theorist at the IAS)</p> </blockquote> <p>其二就是<strong>零知识证明与数学的关系</strong>。1991 年,Wigderson 与合作者证明,本质上所有数学论述都可以改写成一个允许被 “零知识证明” 化的版本 (essentially all mathematical statements can be translated in a way that allows a zero-knowledge proof) ——关于这一点,Wigderson 认为,“这可能他最令人惊讶、也最矛盾的结果”。</p> <p>好吧,问题来了,说了半天,究竟什么是 “零知识证明”?</p> <h2 id="究竟什么是-零知识证明">究竟什么是 “零知识证明”?</h2> <h3 id="定义">定义</h3> <p>这里是 Wikipedia 上<a class="link" href="https://en.wikipedia.org/wiki/Zero-knowledge_proof" target="_blank" rel="noopener" >对零知识证明的定义</a>:</p> <blockquote> <p>In cryptography, a zero-knowledge proof or zero-knowledge protocol is a method by which one party (the prover) can prove to another party (the verifier) that they know a value x, without conveying any information apart from the fact that they know the value x.</p> </blockquote> <p>简单讲,对一个给定的秘密信息 x,一方可以在 <strong>不泄漏任何相关信息(零知识)</strong> 的情况下,向另一方证明 “自己知道 x” 这个事实。</p> <p>那么,什么情况下,我们需要用到这种方式的证明呢?</p> <h3 id="永动机和公私钥系统">永动机和公私钥系统</h3> <p>举个例子,我发明了永动机。</p> <p>虽然我迫不及待地想向外界发布这个信息,然而,我既不想让别人看到这个永动机长什么样,也不想让别人看到理论的细节和推导的过程,更不想让别人看到使用的材料和实验的步骤。</p> <p>这个时候,我就需要 “零知识证明” 了。</p> <p>反应敏捷的你,可能会问,在比特币系统中常见的 “A 使用私钥对特定的公开信息签名, B 使用对应的公钥验证该签名是否有效”,是否是对 “该签名者拥有私钥” 这一声明的零知识证明呢?</p> <p>从上面的概念上讲,这是不算的。</p> <p>因为,虽然 A 在未透露私钥的情况下证明了自己持有私钥,但为了使得其他人可以验证,他必须提供与该私钥对应的公钥给验证者,这违反了上面说的 &ldquo;without conveying * <strong>any</strong> * information&rdquo; 这个条件 (公钥和私钥具有相关性)。正因如此, Wikipedia 上接着说,真正的难点在于 “<strong>不能透露该信息,及额外的相关信息</strong>” (the challenge is to prove such possession without revealing the information itself or any additional information)</p> <h3 id="四个例子">四个例子</h3> <p><a class="link" href="https://youtu.be/FuKEpOhiVPg" target="_blank" rel="noopener" >李永乐老师在 Youtube 上的讲解视频</a> 里,举了四个例子。它们分别是</p> <ol> <li>分球 - (在不讨论颜色的情况下) 证明自己并非色盲</li> <li>山洞 - (不展示验证口令过程的情况下) 证明自己已知口令</li> <li>数独 - (不展示具体解法的情况下) 证明一个特定解已知</li> <li>染色 - (不展示具体解法的情况下) 证明特定结构有三色解</li> </ol> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/liyongle.jpg" width="1274" height="641" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/liyongle_hub09c5ab10481fa23cf8b917727e3c177_171148_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/liyongle_hub09c5ab10481fa23cf8b917727e3c177_171148_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="李永乐老师 - 神奇的零知识证明!" class="gallery-image" data-flex-grow="198" data-flex-basis="477px" ></p> <p>李老师的视频紧凑连贯,平易近人,小学生都可以 1.25 倍速无压力看完(小学生亲测有效)。</p> <p>这四个例子前后只用了十来分钟,就基本上把零知识证明展示得明明白白了,推荐你现在就去看。</p> <p>当然,看完了别忘了回来。</p> <h3 id="姚氏百万富翁问题">姚氏百万富翁问题</h3> <p>李永乐老师在视频末尾提出了一个问题,这个问题就是著名的 “姚氏百万富翁问题” (<a class="link" href="https://en.wikipedia.org/wiki/Yao%27s_Millionaires%27_problem" target="_blank" rel="noopener" >Yao’s Millionaire’s Problem</a>)。</p> <p>这里是 <a class="link" href="https://zhuanlan.zhihu.com/p/25770963" target="_blank" rel="noopener" >解决方案</a> 和 <a class="link" href="https://zhuanlan.zhihu.com/p/65564614" target="_blank" rel="noopener" >对应的代码</a> ,具体的就不细说了。</p> <h2 id="真实世界案例">真实世界案例</h2> <p>看完了视频里的几个例子,一起来看看下面这个真实世界的房屋租赁案例吧。</p> <h3 id="租房问题">租房问题</h3> <p>小美在硅谷工作,最近刚跳槽到一家她中意了很久的大公司,打算在附近找套房子安顿下来。</p> <p>公司同事给她介绍了一个中介——小黄。</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/renting.jpg" width="1848" height="1042" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/renting_hu2111d5b117236361e58743894af91497_274190_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/renting_hu2111d5b117236361e58743894af91497_274190_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="小美租房" class="gallery-image" data-flex-grow="177" data-flex-basis="425px" ></p> <p>本来小美对房子挺满意的,可是一想到要一下子把自己的手机号码,银行卡流水,收入情况,社保号码这些隐私信息,一下子给一个头一次见面,才刚认识不久的房产中介,小美不由皱了皱眉头。</p> <p>再说了,万一房产中介保管不善,这些信息被第三方的拿走了,甚至被贩卖给做黑产的怎么办。想到这些,小美打了个寒颤。</p> <p>要是能不透露自己的这些私人信息,又能向合同签订方证明自己的信用记录就好了。</p> <p>我们来一起看看,怎么用零知识证明解决这个问题。</p> <h3 id="完整的证明过程">完整的证明过程</h3> <p>在开始前我们假设,这套房子的租金是 $1k/月,房东要求房客需要出示至少 $4k/月的收入证明。</p> <p>在一个房间里有十个盒子,盒子上分别写着 <code>$1k</code> / <code>$2k</code> / &hellip; / <code>$10k</code>,作为标记。每个盒子上都有一把锁,彼此互相无法开启。每个盒子顶部有一条缝 (可以塞纸条进去),但塞进去的纸条只有用钥匙开启才能看到内容。</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_1.jpg" width="1665" height="648" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_1_hu21838bb9ec9df217caa4d7ecfe8af39a_112100_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_1_hu21838bb9ec9df217caa4d7ecfe8af39a_112100_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="第一步" class="gallery-image" data-flex-grow="256" data-flex-basis="616px" ></p> <p>这个时候,中介小黄先进去,把第四个写着 <code>$4k</code> (也就是房东要求的最低收入证明) 的盒子钥匙拿走,并把其他所有盒子的钥匙都销毁,也就是说,之后小黄只能看 4 号盒子的内容了。</p> <p>完成后,小黄离开房间。</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_2.jpg" width="2068" height="681" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_2_hue203f834aebe8f9bb94609ff2efc4d09_127877_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_2_hue203f834aebe8f9bb94609ff2efc4d09_127877_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="第二步" class="gallery-image" data-flex-grow="303" data-flex-basis="728px" ></p> <p>此时小美进入房间。小美需要准备一些上面有 “+” 和 “-” 的卡片,并按照盒子上的金额,把她收入能覆盖的盒子全部放入 “+” 卡片,而超出她收入的盒子放 “-” 卡片。在这个例子里,你可以看到小美的收入为 $7k/月。完成后小美离开房间。</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_3.jpg" width="2058" height="681" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_3_hu2e22272dbfb3e33ff0ff989b2481a705_114039_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_3_hu2e22272dbfb3e33ff0ff989b2481a705_114039_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="第三步" class="gallery-image" data-flex-grow="302" data-flex-basis="725px" ></p> <p>最后一步,小黄回到房间,用他的 4 号钥匙开启 4 号盒子。</p> <p>这个时候,如果他看到 “+”,就是说,小美满足了条件,看到 “-”,就是小美不满足条件。</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_4.jpg" width="2054" height="664" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_4_hu584394cfd22530815ac83c96b9d7c74c_93972_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/rent_4_hu584394cfd22530815ac83c96b9d7c74c_93972_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="第四步" class="gallery-image" data-flex-grow="309" data-flex-basis="742px" ></p> <p>你可能已经注意到了,整个验证过程中,小黄得到了自己想要的答案,小美也没有暴露任何信息,而这就是零知识证明的神奇之处。</p> <p>在真实世界里,情况比这会略微复杂一些,比如我们会使用自动化数据获取而不是依赖手动标记,会使用哈希而不是明文的标识,但核心逻辑是类似的。</p> <p>(注:这个案例更多的讨论见文末)</p> <p>总得来说,对于目前我们正在使用的绝大多数互联网应用,大部分情况下,我们都选择了<strong>牺牲一些隐私换来便利</strong>。然而,通过应用零知识证明,应用程序得以避免存储用户的隐私数据 (如身份证正反面的照片) 来规避“保管敏感信息导致泄漏”的风险,而总是可以选择在需要的时候去验证。</p> <p>换句话说,通过使用零知识证明,我们从<strong>一开始就避免了敏感数据的传送</strong>,也就自然不再需要像以前那样被迫接受关于隐私的折衷与妥协。</p> <h2 id="区块链应用">区块链应用</h2> <p>在区块链行业,零知识证明得到了极大的发展。下图集中展示了 ZKPs 在区块链场景下的应用。</p> <p><img src="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/ZKPs-in-blockchain-opt.png" width="3026" height="1542" srcset="https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/ZKPs-in-blockchain-opt_hu5286578e49589c5eb568a12cab22da2c_324771_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2021-07-04-zero-knowledge-proofs/images/ZKPs-in-blockchain-opt_hu5286578e49589c5eb568a12cab22da2c_324771_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ZKPs applications in blockchain" class="gallery-image" data-flex-grow="196" data-flex-basis="470px" ></p> <p>值得注意的是,去年以来,<strong>通过 ZKPs 来为区块链扩容</strong> 成为新的应用场景。以下这两个的方案值得关注:</p> <ol> <li><a class="link" href="https://docs.ethhub.io/ethereum-roadmap/layer-2-scaling/zk-rollups/" target="_blank" rel="noopener" >以太坊 zk-rollup 方案</a></li> <li><a class="link" href="https://masked.medium.com/the-coda-protocol-bbcb4b212b13" target="_blank" rel="noopener" >Mina Protocol — A Succinct Blockchain</a></li> </ol> <p>其中,Mina Protocol 本质上回答了下面这个问题:</p> <blockquote> <p>How can we be convinced of the current state of the blockchain, without seeing the entire history of that state?</p> </blockquote> <p>在 Mina 的方案中,通过使用递归的 zk-SNARKs,最终得到了一个 <strong>恒定尺寸只有 22KB</strong> 的区块链,被称为史上最轻量的区块链。</p> <h2 id="国防应用">国防应用</h2> <p>不仅个人的隐私可以得到保护(联系方式,收入,社保等敏感信息),组织 (跨部门资源管理,背景调查),社会 (个人征信),甚至国家也一样可以从零知识证明中受益。</p> <p>在 2016 年,普林斯顿等离子物理研究室和普林斯顿大学的研究人员<a class="link" href="https://www.nature.com/articles/ncomms12890" target="_blank" rel="noopener" >演示了一个可以用于核裁军协商的技术</a>。通过这个技术,观察员可以在无法记录,分享和泄漏机密的内部机制的前提下,确认一个给定的物体是否是核武器。</p> <p>2019年7月,美国国防部高级研究计划局(Defense Advanced Research Projects Agency)宣布了一项名为“保护用于加密核查和评估的信息”(Securing Information for Encrypted Verification and Evaluation)的新举措,旨在将零知识证明应用于美国军方。在实践中,这可能意味着有能力证明数据的来源或出处,而不说明具体是如何获得的。这可能涉及证明数字系统存在安全漏洞,而无需披露漏洞的细节或利用漏洞的方法。SIEVE 最具体的例子涉及到,将网络攻击归因于一个特定的人群、实体或国家。在这种情况下,目标将是能够证明归属,而不需要透露机密情报或任何一方的具体黑客能力。如果零知识证明可以以这种方式使用,这项技术将大大简化处理网络安全的“归因问题”。</p> <h2 id="游戏应用">游戏应用</h2> <p>在隐私,安全等考虑之外,还有其他你可能想不到的应用。</p> <p>把零知识证明用在一个开放式游戏里?</p> <p>是的,ETH 上的游戏 <a class="link" href="https://zkga.me/" target="_blank" rel="noopener" >Dark Forest</a> 正是这么做的。</p> <p>区块链上的数据是公开,透明,可追踪,可审计的,但零知识证明的应用,使得玩家在一个公开透明的环境下,可以在 <strong>不向外部透露自己的坐标等关键信息</strong> 的情况下与合约交互,参与游戏(像不像大刘笔下的黑暗森林?)。这大大拓展了游戏的深度和广度。</p> <p>此前他的开发者 Brian Gu 做了一篇访谈,我<a class="link" href="https://gulu-dev.com/post/2021-06-29-dark-forest" target="_blank" rel="noopener" >把这个访谈的要点记在了这篇 blog 里</a>。</p> <h2 id="参考材料">参考材料</h2> <ol> <li><a class="link" href="http://people.csail.mit.edu/silvio/Selected%20Scientific%20Papers/Proof%20Systems/The_Knowledge_Complexity_Of_Interactive_Proof_Systems.pdf" target="_blank" rel="noopener" >(1989) The Knowledge Complexity of Interactive Proof Systems</a> (原始论文)</li> <li><a class="link" href="http://pages.cs.wisc.edu/~mkowalcz/628.pdf" target="_blank" rel="noopener" >(1998) How to Explain Zero-Knowledge Protocols to Your Children</a></li> <li><a class="link" href="https://youtu.be/OcmvMs4AMbM" target="_blank" rel="noopener" >(2019.01) Zero Knowledge Proof - ZKP (Simply Explained)</a></li> <li><a class="link" href="https://www.wired.com/story/zero-knowledge-proofs/" target="_blank" rel="noopener" >(2019.09) Hacker Lexicon: What Are Zero-Knowledge Proofs?</a></li> <li><a class="link" href="https://youtu.be/FuKEpOhiVPg" target="_blank" rel="noopener" >(2021.05) 李永乐老师 - 神奇的零知识证明!既能保守秘密,又让别人信你!</a></li> <li><a class="link" href="https://www.notboring.co/p/zero-knowledge" target="_blank" rel="noopener" >(2021.06) Zero Knowledge - by Packy McCormick</a></li> </ol> <h2 id="补关于小美的诚实问题">补:关于小美的诚实问题</h2> <p>以下讨论摘自我昨晚的朋友圈评论:</p> <p><strong>小A</strong><br> 问题是怎么证明小美是诚实的<br> 我的意思是小美不考虑自己的收入,在所有盒子里无脑放+不就行了…</p> <p><strong>小B</strong><br> 可以分下面两种情况讨论:</p> <ol> <li>可以很容易地限制小美的可选操作(就像在自动柜员机上,你能做的事情很有限)比如在真实场景里,小美只能选择授权还是不授权,</li> <li>如果强制小美可以自由的提交或涂改任何牌子的加号减号,接收数据方也很容易增加一道(无需敏感数据)的验证环节,比如说可以获取小美提交数据的hash,和有效的第三方hash做个交叉验证就行了~</li> </ol> <p><strong>小A</strong><br> 是,但这样就是额外信息了。还是wiki上的例子比较严谨。</p> <p><strong>小B</strong><br> 不对,我重新想了想,证明者是否诚实,应该是跟零知识证明正交的~ <br> 比如说色盲换球那个问题,证明者同样可以主动混入错误答案~<br> 山洞那个,阿里巴巴也有可能就不按常理出牌,乱跑一通或压根不跑。<br> 证明者需要保持诚实,应该是业务的需求,而不是证明过程的需求。</p> <p><strong>小A</strong><br> 换球那个没法作弊的,因为球在色盲手里。色盲自己知道球有没有变过,对方说错答案就失败了</p> <p><strong>小B</strong><br> 有道理,可以这么改一下,超出所需证明的部分按百分比加到房租里</p> <p><strong>结论:</strong></p> <p>在真实世界中,我倾向于采用讨论一开始时的方案。小美的信用记录 (可以类比为支付宝里的芝麻信用分,身份证扫描件,房产证扫描件,等等),不仅其他人不能随意读取,连她自己也不<strong>应该</strong>有机会可以修改或伪造。也就是说,“如何保证个人信用数据的真实有效” 这个问题,与 “使用零知识证明确保这些敏感信息不被传递即可得到验证” 在理想情况下应该是互不影响的两个独立问题。</p> <p>文中出于简化确实没有考虑此欺诈问题。如果基于简化的仅为说明问题的考虑,可以直接加一个条件,如讨论末尾提出的 “超出所需证明的部分按百分比加到房租里” 来引导小美提供符合她自己真实情况的选择。</p> 2021.06 Dark Forest - 基于零知识证明与区块链的元宇宙构建 https://gulu-dev.com/post/2021-06-29-dark-forest/ Tue, 29 Jun 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-06-29-dark-forest/ <img src="proxy.php?url=https://gulu-dev.com/post/2021-06-29-dark-forest/DarkForest.png" alt="Featured image of post 2021.06 Dark Forest - 基于零知识证明与区块链的元宇宙构建" /><p><code>Metaverse</code> 系列相关材料第三篇,Software Engineering Daily 对 Brian Gu 的访谈,内容是区块链游戏如何利用零知识证明。</p> <ol> <li>在公开透明的区块链上通过零知识证明玩不完全信息的游戏 (麻将/炉石) 本质上是通过合约模拟了典型游戏服务器的不透明性。 (同时获得了&quot;可信&quot;, &ldquo;无许可&quot;和&quot;不透明&rdquo;)</li> <li>CryptoKitties 里的小猫难以被用来做出好玩的游戏,部分原因是因为透明性 “谁有什么猫一清二楚”</li> <li>黑暗森林是战争迷雾的极端情况,这里实际上是一个类似文明的生成式回合制策略宇宙开放游戏</li> <li>敏感数据 (如本轮你的移动坐标) 的 hash 被用作公开的 zk proof,服务器可以通过规则推断出某个玩家的 hash 是否有效,欺诈会通过监控合约被 ban</li> <li>扩展性: a) 类似一个开放后端 API 的游戏,玩家可以通过自制客户端来自动化 b) 本质上是数据转移,可以随便换中世纪或 WOW 皮</li> <li>测试阶段跑在 xDai 上省 gas</li> <li><strong>metaverse 不应被自顶向下设计,没有 roadmap,它的开发过程是参与者与开发者共同完成的,是进化版的 EVE</strong></li> <li>合约被拆分解耦后,可升级性很重要。immutability 导致的僵化。通过 proxy contract 来解决。代理合约还能用来调试 (需要时走调试路径)</li> <li>scalability 很重要 (但不是更多的 tx 那种扩展性) 每次玩家连上 xDai 时需要同步巨量的游戏状态 (更好的数据查询和同步机制,Redis for xDai? 目前正在尝试 The Graph (相当于全局统一的 GraphQL) 索引链上数据)</li> </ol> <p>附:</p> <ul> <li>Source: <a class="link" href="https://softwareengineeringdaily.com/2021/06/21/dark-forest-transparency-on-blockchains-with-zero-knowledge-proofs-with-brian-gu/" target="_blank" rel="noopener" >Dark Forest: Transparency on Blockchains with Zero-Knowledge Proofs with Brian Gu</a></li> <li>Transcript: <a class="link" href="http://softwareengineeringdaily.com/wp-content/uploads/2021/06/SED1283-Brian-Gu.pdf" target="_blank" rel="noopener" >PDF</a></li> </ul> 2021.06 (Roblox) Claus Moberg 访谈简短记录 https://gulu-dev.com/post/2021-06-28-roblox-claus-interview/ Mon, 28 Jun 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-06-28-roblox-claus-interview/ <img src="proxy.php?url=https://gulu-dev.com/post/2021-06-28-roblox-claus-interview/Roblox.jpg" alt="Featured image of post 2021.06 (Roblox) Claus Moberg 访谈简短记录" /><p><code>Metaverse</code> 系列相关材料第二篇,Software Engineering Daily 对 Claus Moberg 的访谈。</p> <h2 id="简介">简介</h2> <p>Roblox 是整个游戏行业的一个微缩版(啥游戏都有),以 9-13 岁孩子为主,在美国和欧洲英语系国家,超过 50% 的 9-13 岁孩子玩 Roblox。</p> <p>Roblox 在14年前 (2006) 一开始时是用来让孩子在虚拟环境下做物理实验用的,后来做了 Roblox Studio 允许用户自制,一直不温不火到 2015 年开始爆发性增长,目前月活超过1亿。</p> <p>Roblox Studio 类似 Unity, lua 在脚本沙盒里写逻辑,在编辑器里编地形和模型角色。</p> <p>与其他编辑器从空白开始新项目不同,Roblox 提供所有的默认值,合适的重力,默认角色行为脚本,良好的物理支持。</p> <p>Roblox 是完全的用户生成内容,平台做好了兼容移动 (最老支持 iPhone 4S),PC 和主机 (最老支持 XBox 1) 的支持。</p> <p>完全的自制引擎,流式 3D 体验,服务端真 3D,C++ 实现,客户端只用考虑渲染优化,数据层一致性。Unity / Unreal 需要提前下载资源,而 Roblox 是连上服务器后才按重要性下载 (类似 h5)</p> <h2 id="物理">物理</h2> <p>物理引擎:客户端主导,有交互发生时服务器逻辑再介入</p> <blockquote> <p>In Roblox, we have a really complex set of code that basically says, “Well, if there&rsquo;s no other players near you, obviously we can have physics owned locally on your client.” Then as other players and other dynamic interactions come closer and closer to you, this essentially physics ownership codebase kicks in and tries to determine the optimal placement of stuff either in the cloud or on your local client and which version of the world takes precedence.</p> </blockquote> <h2 id="技术原则">技术原则</h2> <p>在一开始就叫自己的机房 RCC Roblox compute cloud 那时候还没有云计算这个说法,然后一直都自建数据中心。然后有了一个抽象层用于隔离内外部的底层区别。</p> <p>技术指导原则:确保技术投入的合理性,以免陷入技术负债。简单说就是不要上技术杠杆。</p> <blockquote> <p>“Make sure you doing the engineering investment necessary to avoid any sort of unnecessary technical debt.”</p> </blockquote> <p>一份重要的内部记录用于确保服务不断地被从 monolith(巨大单一源) 外迁,解耦并使得每个小组对自己的服务负责。</p> <p>重要原则:roblox 是一个平台,游戏行业里唯一的,做的时候无需考虑何时,甚至是是否需要交付的(这样一个项目)</p> <blockquote> <p>the Roblox operating principle from an engineering perspective is, “Look, we’re a platform. We’re very unique in the game industry and that nothing we are building today has to ship tomorrow.”</p> </blockquote> <p>没有交付周期,随便任何人都可以随时出去度假 6 个月。因为游戏都是用户做的。所以对于平台来讲无比重要的事情从来不是时间期限而是做正确的事。所以没有技术负债非常重要。设定目标并不断迭代,持续发布。如果短期目标偏离了终极目标,或造成技术负债,那宁肯不发布。</p> <h2 id="工程细节">工程细节</h2> <p>游戏相关的都是 C++,Web 相关的用 .Net,有的功能用 go/python 也 ok。</p> <p>对于不同的平台直接让 C++ 跑在对应的硬件上就行了 可以干掉中间层 (metal on iOS, Vulcan on android)</p> <p>之前不少用 WebView 实现的应用信息平台界面全部换成游戏引擎渲染,应用端极小,登录后下载各种信息,全部用一致的方式交互。结果是 2D 交互再复杂也足够流畅。</p> <p>而且看着所有别的公司都在为不同平台的不同 UI 折腾得要死简直爽飞了。</p> <h2 id="组织">组织</h2> <p>组织扁平化,600雇员,80% 是研发,分成30个组左右。整个软件大体上由两块组成:一边是为17岁少年创造内容服务的各种工具链;另一边是为全世界的玩家连上来的体验服务。他负责的 5 个团队里,一个是玩家社交,一个团队负责游戏发现(Roblox 的内置类似 Steam Store)一个 Universal Lab Team 用 Lua 把所有的东西整合到一起(吃自己3D引擎的狗粮)未来规模再大都会保持每个小组 10-30 个人,有自己的完整配套,类似一个单独的创业公司。</p> <h2 id="经济模型">经济模型</h2> <p>开发者创造内容,培养玩家群体,卖数字资产,比如游戏内特定的 VIP 区域。</p> <p>玩家用美元买 Robux,然后买游戏内容,各种服务。只有卖出东西的开发者才能用赚到的 Robux 通过 DevEx 换钱,费率高于苹果的 30% 但可以在所有平台上玩</p> <h2 id="行业趋势">行业趋势</h2> <p>20年前 100% 本地,而现在大量计算发生在其他地方。在未来,良好的游戏体验是由基础设施保证的,客户端无关性会加强。</p> <blockquote> <p>This is something that’s been part of Roblox’s vision for, again, literally 12 to 14 years at this point. The idea that we need an architecture that basically produces the best possible experience for every player regardless of the actual client-side hardware that&rsquo;s running that experience.</p> </blockquote> <p>不可能是完全的流式处理。完全的服务器化,延迟和响应始终是体验的障碍。</p> <p>VR 沉浸式体验的落地 (how far and how soon do we get to full immersion)</p> <p>附:</p> <ul> <li>Source: <a class="link" href="https://softwareengineeringdaily.com/2019/12/18/roblox-engineering-with-claus-moberg/" target="_blank" rel="noopener" >Roblox Engineering with Claus Moberg</a></li> <li>Transcript: <a class="link" href="https://softwareengineeringdaily.com/wp-content/uploads/2019/12/SED973-Roblox-Claus-Moberg.pdf" target="_blank" rel="noopener" >PDF</a></li> </ul> 2021.06 Joshua 对感应合约的采访 https://gulu-dev.com/post/2021-06-06-sensible-interview-by-joshua/ Sun, 06 Jun 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-06-06-sensible-interview-by-joshua/ <img src="proxy.php?url=https://gulu-dev.com/post/2021-06-06-sensible-interview-by-joshua/2021-06-06-sensible-logo.png" alt="Featured image of post 2021.06 Joshua 对感应合约的采访" /><p>本月月初接受了 Joshua 的线上采访,这是接受的几次 CoinGeek 采访中比较走心的一次。主要是提问的几个问题比较好 (能做到什么别人做不到的 / 溯源的实质 / 协议变更的误会 / 脚本尺寸上限 / 接下来的产品发布 / 感应合约全球推广 / 分层的实质 / sat token 的实质 / 方案竞争 / 与 ETH 的模型比较 / 合规与成本 / 文化与商业环境)。这篇采访从 Sensible Contract 的独有特点,开发进展,聊到了其他更开阔的视角,是我接受的采访里,既有一些深度和广度,也是信噪比较高的。</p> <ul> <li><a class="link" href="https://coingeek.com/token-protocols-on-bsv-sensible-contract/" target="_blank" rel="noopener" >CoinGeek 采访链接:Token protocols on BSV: Sensible Contract</a></li> </ul> <p>以下是采访的正文。</p> <hr> <h2 id="sensible-contract"><strong>Sensible Contract</strong></h2> <h3 id="what-can-the-sensible-contract-protocol-provide-that-no-other-token-protocol-can">What can the Sensible Contract protocol provide that no other token protocol can?</h3> <p>Gu Lu: Please let me clarify that <a class="link" href="https://sensiblecontract.org/" target="_blank" rel="noopener" >Sensible Contract</a> is not a token solution which competes with other ones (one use case of it does). It’s a contract model which provides on-chain back-tracing and collaborating for existing bitcoin scripts. By enabling these capabilities, we can implement a handy token/NFT system like BCP01 &amp; BCP02 without any hassle. In BCP03 you can also find out how Sensible Contract simplifies the creation of an ordinary on-chain swap by formulating a collaborative global state in a graceful way.</p> <p>Sensible Contract is not here to compete and try to outperform a given token system since day one. On the contrary, it could be used to make the given token system simpler and easier to write and maintain, if that system is written in sCrypt. It helps you on two aspects, specifically.</p> <ol> <li>First, by hiding details of cumbersome legitimacy backwards tracing and validating, it helps you to focus on your business logic with your tokens or other assets you defined without worrying forged inputs from unknown parties.</li> <li>Second, by providing a collaboration mechanism, you are able to access more business-relevant data fields which are not available in the current ordinary transaction context.</li> </ol> <p>The collaboration feature opens a new and very expansive territory to explore. Contract communication could be quite useful to implement automated multi-phases function for non-trivial business logic. The collaboration mechanism consists of three levels:</p> <ol> <li>Multiple inputs identifying each other within the same transaction (which is elaborated in the whitepaper)</li> <li>Multiple modules in the same locking script; (this works in a “function dynamic folding/unfolding feature” enabled context, using state-jumping to minimize the runtime script size dynamically by folding those functions not being used and unfolding them as needed)</li> <li>By propagating specific fields of the data segment inside the locking script body, a contract can collaborate asynchronously with another contract not in the same context.</li> </ol> <p>As you may see, this provides many flexibilities from different dimensions which benefits a much larger spectrum of applications, and it goes far beyond the whitepaper which contains only the first level at that time. We are in a very active development cycle in this no-man’s territory to improve both the contract model itself and the applications it incubates.</p> <h3 id="the-sensible-whitepaper-did-a-great-job-highlighting-how-all-token-protocols-face-the-backwards-tracing-aka-back-to-genesis-problem--why-it-is-so-important-token-protocols-to-mitigate-this">The Sensible Whitepaper did a great job highlighting how all token protocols face the backwards tracing (aka Back to Genesis) problem – why it is so important token protocols to mitigate this?</h3> <p>Gu Lu: Before talking about the tracing problem, I would say the said Layer 2 solutions (by using an op-return based solution like Omni-layer used by USDT and using external service to validate transactions) shouldn’t have this issue at all in the first place because you can always ensure the legitimacy by improving the external off-chain service. But, by using that, you probably don’t need the bitcoin programmability at all, and it’s not “programmable money” anymore, it literally says “let’s create a markup system on cash and enforce the rules by interpreting the markups together.”</p> <p>By utilizing Bitcoin script to ensure the on-chain legitimacy for your assets, backwards tracing is inherently an unavoidable issue if you want to solve the problem inside the boundary of bitcoin virtual machine. Because the value of satoshis comes from the node software. The system itself ensures that satoshi is such a thing that is not creatable, duplicatable, delete-able, etc. But all script-created resources don’t share that “first-class privilege,” there are thousands of ways of hacks to break into a token system by creating a not-easily-recognizable forged input, if you cannot trace backwards to its genesis in a very fast and effective way.</p> <p>Frankly speaking, backwards-tracing patches the largest hole but not all of them. The programmer still needs to be careful not writing bad code to accidentally ruin the asset in an unexpected way in their code. This (the assets could be broken if the contract contains overlooked mistakes) is the same case in Ethereum’s solidity.</p> <p>However, in modern environments like Cadence in Flow, this is a solved problem by utilizing the ‘resource-oriented’ paradigm. Assets created in those environments are well-defined “first class citizens” like the value of satoshis mentioned above and are impossible to be accidentally duplicated or deleted.</p> <p>The details of backwards tracing could be found <a class="link" href="https://gulu-dev.com/post/2021-05-01-sensible-intro/" target="_blank" rel="noopener" >in this presentation</a>.</p> <p>Here you’ll find the pdf and the full content in Chinese (sorry but it may be auto-translated).</p> <h3 id="are-the-advocates-for-sensible-contract-still-asking-for-a-protocol-change-to-solve-the-backwards-tracing-issue">Are the advocates for Sensible Contract still asking for a protocol change to solve the backwards tracing issue?</h3> <p>Gu Lu: No.</p> <p>After we knew the attitude of nChain we will no longer call for a node upgrade anymore as soon as we knew, because we want to put our efforts into building prototypes and products more effectively, not arguing and advocating repeatedly to be “accepted.” The most important thing is whether it helps the development of applications, with or without the so-called “protocol change” (which technically it isn’t).</p> <p>Sensible Contract works well with 100% features it promised without upgrading PreImage. The next version of the whitepaper would be revised heavily with much more valuable pieces being added, and the ‘PreImage upgrading’ being moved into the footnote area of the implementation section to avoid unnecessary arguing.</p> <h3 id="do-you-think-the-backwards-tracing-issue-can-be-solved-without-a-protocol-change">Do you think the backwards tracing issue can be solved without a protocol change?</h3> <p>Gu Lu: It could be in the future.</p> <p>People are still working on this actively and it’s too early to say it’s a dead-end.</p> <h3 id="why-is-it-a-requirement-to-increase-the-bitcoin-script-size-of-transactions-to-enable-applications-like-tokenswap-to-be-able-to-launch">Why is it a requirement to increase the Bitcoin script size of transactions to enable applications like TokenSwap to be able to launch?</h3> <p><a class="link" href="https://github.com/bitcoin-sv/bitcoin-sv/issues/201" target="_blank" rel="noopener" >The 201 issue: increase the default script size limit from 10KB to 500KB</a></p> <p>Gu Lu: We want a bigger size limit for a single locking script so that we don’t have to selectively remove a lot of business code and functions to punch it through the 10KB gate. Just like we simply want bigger blocks than 1MB to have more transactions. It’s that simple.</p> <p>We worked with TAAL and had 500KB running these recent months. It works well so we think it’s probably a good proposal candidate to ask node software maintainer to lift the limit to 500KB so that other miners can also mine these transactions in their blocks and we could get a generally shortened average confirmation time.</p> <p>However, the size issue can be optimized, both on the toolchain side and application side. We are in an era that both toolchains and application contract code are not matured yet. In earlier days of Visual Studio, developers complained heavily about the executable size generated by Visual C++ on Windows platform using earlier versions of Visual Studio: “Why does a one-line ‘hello-world’ program generate such a huge executable (a 2-5MB .exe file)?”</p> <p>Microsoft listened to them and optimized their compilers in later iterations. Just now I created a new project in Visual Studio 2017 minutes ago and made an experiment. The .exe size result is 47KB in Debug and 10KB in Release (both of them use default optimization options) It’s 10-100x smaller.</p> <p>If you look at the generated bitcoin script of sCrypt output, you will realize that the same thing would/should happen. Xiaohui also confirmed that it has plenty of room to optimize toward a much more compact compilation but it’s generally low-prioritized because the scripted transaction is still uncommon nowadays. So we’ll work together to have a generally smaller locking script to make scripted transactions more compact in future iterations.</p> <h3 id="what-are-you-most-excited-to-see-be-built-atop-sensible-contract">What are you most excited to see be built atop Sensible Contract?</h3> <p>Gu Lu: SatoPlay NFTex (NFT exchange), SensibleSwap (previously known as TokenSwap), SensibleScan (Asset Browser like Etherscan). We don’t see them being built, we put ourselves into building them with their developers. These are the three projects close to launch, and we know there are more projects in development.</p> <p>We are actively testing against the interoperability that the assets and tokens can be transferred across these products smoothly. This will be the most exciting features in the very near future.</p> <h3 id="sensible-is-relatively-lesser-known-within-the-bsv-space-due-to-its-recent-release-and-origination-in-the-chinese-market-how-do-you-plan-to-gain-more-overall-adoption-of-the-protocol">Sensible is relatively lesser known within the BSV space due to its recent release and origination in the Chinese market. How do you plan to gain more overall adoption of the protocol?</h3> <p>Gu Lu: We generally don’t do too much to gain the attention because we have limited time and resources. We have to put them into the most critical parts of our research and engineering to push things forward, to try and fail quickly, to experiment with many possibilities so that we have generally more progress in a given timeframe.</p> <p>People who know well-enough about Bitcoin script will find us somehow and they are generally willing to have great conversations with us. That would be much more productive for all of us.</p> <h2 id="token-protocols"><strong>Token protocols</strong></h2> <h3 id="can-you-give-your-thoughts-on-the-layer-labels-for-tokens-ex-stas-as-l0-scrypt-as-l1-and-tokenized-as-l2">Can you give your thoughts on the ‘Layer’ labels for Tokens? (ex. STAS as L0, sCrypt as L1 and Tokenized as L2)?</h3> <p>Gu Lu: We heard different “layering” approaches. I don’t plan to compare them in detail like I did using a diagram from L2 to L1 in earlier talks in February. Because no matter how the token system is implemented, it’s ok.</p> <p>Each of them deserves a chance to be used by others. It’s whether the system is continuously widely used that matters. It’s not about whether your design is clean and elegant, it’s about whether you are going to put far more dirty work into it in the future, to have more and more projects believing that your solution is a strong cornerstone that is reliable and stable and will be well maintained and iterated in the foreseeable future.</p> <h3 id="what-are-your-thoughts-on-using-satoshis-for-tokens">What are your thoughts on using satoshis for tokens?</h3> <p>Gu Lu: By doing this, satoshis are “sliced/staked/burnt” into tokens which could be recovered as needed (or not).</p> <p>It’s viable, but very hard (if not impossible) to do rich contracted financing features that could generally be found on other chains. So yes, probably in some use cases that would be good enough to have a simple satoshi-based token.</p> <h3 id="do-you-think-multiple-token-protocols-atop-bsv-can-exist-if-so-why-how-is-that-different-than-claiming-multiple-crypto-currencies-can-co-exist">Do you think multiple Token protocols atop BSV can exist? If so, why? How is that different than claiming multiple crypto currencies can co-exist?</h3> <p>Gu Lu: Frankly I don’t know. I don’t generally over-simplify things, especially when it comes to such a globally evolving complex system. It could take much longer time than most bitcoiners think to have “one chain to rule them all.”</p> <p>Engineers introduce various concepts and methods to deal with different situations and make different trade-offs among many aspects, using different token protocols (or maybe even combined in some cases) to form different solutions, just like they use different programming languages, frameworks, libraries to express their idea effectively.</p> <p>Ignoring the complexity and the diversity and forcing people to unify ‘something’ may not be a good idea.</p> <p>I’d rather encourage a more inclusive atmosphere that enables people to express their ideas and show their talents without worrying that being isolated.</p> <h2 id="ethereum"><strong>Ethereum</strong></h2> <h3 id="how-can-bsv-out-compete-ethereum">How can BSV out-compete Ethereum?</h3> <p>Gu Lu: This is a huge question, and I haven’t been prepared to answer it yet. Generally, BSV has potential to contain much more transactions in orders of magnitude than ETH. But can all applications running on EVM be migrated to run well enough on bitcoin VM?</p> <p>That’s not clear and obvious. Solidity and the account and event model surely isn’t sophisticated well enough but they do bring value that cannot be easily replaced by UTXO-based contracted applications. However, we know too little about bitcoin and we are still learning, I hope I can be more confident next time I’m asked this question.</p> <h3 id="how-can-bsv-profit-from-cooperating-with-eth-and-defi">How can BSV profit from cooperating with ETH and DeFi?</h3> <p>Gu Lu: ETH/Defi gives us excellent use-cases to explore what we can do with Sensible Contract. We’ll do more research and gain more experiences by analyzing what happens in these projects (what went right and what went wrong) After we built a prototype on BSV which runs a similar business logic, we can use ETH as a benchmark for our apps. How the fee compares, how transaction throughput compares, etc., that generally saves a lot of time.</p> <h2 id="general"><strong>General</strong></h2> <h3 id="what-impact-do-you-think-the-law-narrative-has-had-on-bsv-with-respect-to-tokenization">What impact do you think the LAW narrative has had on BSV with respect to tokenization?</h3> <p>Gu Lu: The political correctness answer to this is “code is code” and “law is law”. But using blockchain itself to perform automatic and immediate validation doesn’t disable “the law” from functioning. You can still use “the law” to defend yourself when it’s needed, just like any scenario you’ll meet in life. By using contract correctly and wisely, you lose nothing (in aspect of “the law”) but gain automated validated transaction efficiently. That’s why we use bitcoin script in the first place.</p> <p>If you are referring to using op-return to mimic a ledger, and using an authority or external validators or such (and rely on “the law” to catch them), you will probably have a significant higher cost than your opponent, who use the automated contracted method approach.</p> <p>Yes, you can fall back to the traditional ways of doing things and keep telling people that “the law” would help. But the point is, “the law” would/should always help, regardless of how you do your business. If the law helps anyway, why not choose the cost-effective method using contracted model automated that gets executed and validated on-chain?</p> <p>Having that said, I think it’s worth it to mention that a well-formed KYC and AML process is very valuable to people who want to build their business on BSV seriously. We are also very active working in this area.</p> <h3 id="if-you-could-wave-a-magic-wand-and-change-something-about-the-bsv-culture-or-business-environment-what-would-it-be">If you could wave a magic wand and change something about the BSV culture or business environment, what would it be?</h3> <p>Gu Lu: I’ve been a veteran in game industry for 15 years. I learnt that the culture and the environment of a given sector consists of many aspects which affect each other in a very complicated way, which is worth respecting. It’s wise to adapt yourself to it in different ways and see if you can contribute in an effective manner or not.</p> <p>Additionally, I tend to think in such a way that utilizing the attributes that BSV can afford uniquely to build something good for my purpose, not build something that the BSV ecosystem ‘consider’/ ‘accept’ that it’s worth doing. I force myself to always think from such an “external view angle” to the community. Because the world doesn’t/shouldn’t ‘carefully’ help BSV to grow and succeed. On the contrary, BSV should survive and provide value to other industries (which in my case, the game industry) to legitimatize its existence.</p> <p>Thanks for reading this.</p> <p>I’ve attached a Sensible Contract ecosystem diagram here for your reference below.</p> <p><img src="https://sensiblecontract.org/ecosystem-v0.1.6.png" loading="lazy" alt="ecosystem" ></p> 2021.05 感应合约的简介和基本原理 https://gulu-dev.com/post/2021-05-01-sensible-intro/ Sat, 01 May 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-05-01-sensible-intro/ <img src="proxy.php?url=https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/01.jpg" alt="Featured image of post 2021.05 感应合约的简介和基本原理" /><p>本篇是我在 2021-04-15 BSV Bootcamp 上,关于 <strong>感应合约的简介和基本原理</strong> 的相关分享的文稿版。</p> <p>演讲幻灯片 PDF 可以在此下载: <a class="link" href="2021.04-bootcamp-sensible-contract-intro.pdf" >2021.04-bootcamp-sensible-contract-intro.pdf</a></p> <h1 id="202105-感应合约的简介和基本原理">2021.05 感应合约的简介和基本原理</h1> <h2 id="背景">背景</h2> <p>在今年 4 月,比特币协会在东澳岛主办了长达一周的 BSV 第一届训练营 (Bootcamp)。这次训练营上的第四天是 Sensible Day,全天都是感应合约相关的内容。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/bootcamp_sensible.jpeg" width="800" height="438" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/bootcamp_sensible_huae08aa8c7468661313165de302db2104_86405_480x0_resize_q75_box.jpeg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/bootcamp_sensible_huae08aa8c7468661313165de302db2104_86405_1024x0_resize_q75_box.jpeg 1024w" loading="lazy" alt="Bootcamp Sensible Day" class="gallery-image" data-flex-grow="182" data-flex-basis="438px" ></p> <p>当日所有分享的幻灯片可以在<a class="link" href="https://sensiblecontract.org/docs/sensible/3.-Presentations/" target="_blank" rel="noopener" >感应合约的官网上的这个页面</a>获取。</p> <p>本篇是当天的第一讲,是关于 <strong>感应合约的简介和基本原理</strong> 的相关分享的文稿版。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/01.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/01_hu5c709a6c33e8f742fa48157fab121fc4_34023_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/01_hu5c709a6c33e8f742fa48157fab121fc4_34023_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a01" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>大家好,我是顾露,是比元科技的创始人,也是小聪游戏的开发者,也是 Sensible Contract,也就是感应合约的参与者。今天早上的第一场演讲,我会给大家讲一下感应合约的简介和它的基本原理。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/02.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/02_hu480464a7477e51de1de2f704c0efb015_31016_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/02_hu480464a7477e51de1de2f704c0efb015_31016_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a02" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>(你可以看到)有一个副标题是 “fix the past and build the future in a sensible way”,这实际上就是我们感应合约最核心的两点。也就是说,如果你今天只一页只打算看一页的话,那看一下这一页就可以了,因此我们叫它 Sensible Contract in a nutshell。</p> <p>感应合约有两个基本出发点,一个是溯源,一个是协作。</p> <ul> <li><strong>溯源</strong> 能够帮助你从当前的交易往回看,用以确认以前曾经发生的交易跟自己是不是有关系。这个功能是感应合约一开始的出发点,所以我们叫它 <strong>Fix the past</strong>。</li> <li><strong>协作</strong> 是帮助不同的交易之间建立关系,准确的说,同一笔交易上的不同的输入,是否能够准确地识别对方并做出响应。通过协作,我们得以在未来构造更复杂的项目,所以叫它 <strong>Build the future</strong>。</li> </ul> <p>昨天蒋杰也谈到,协作有多层的含义,既可以是多笔交易,多个比特币脚本之间的合作,也可以是同一个合约内部,不同的代码段之间的协作。由于单个合约,也就是单个解锁脚本,能做的事情非常有限,想要达到 <strong>“通过 (容易被定义的) 多个交易步骤来实现的契约自动化”</strong> 的效果(也就是我们周一时说的 “automation of agreements with easily definable transaction step”),就需要在多层次上的协作,才能完成比较复杂的工作。</p> <h2 id="正文">正文</h2> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/03.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/03_hu6931d99813b4cd8bc31941298f2e26e3_28455_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/03_hu6931d99813b4cd8bc31941298f2e26e3_28455_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a03" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,那么今天的内容我分成了三块:</p> <ol> <li>第一部分是,开始深入感应合约前,你需要了解的一些预备的知识。</li> <li>第二部分里,我会把整个过程拆分成单个独立的小步骤。为什么要分步?分步的好处就是,万一中间哪步没弄明白,你可以跳过它,直接看下一步,前后步骤之间没有那么强的相关性。</li> <li>最后一个部分,我们上线以来,有不少同学在微信群里,或者私下里找我了解情况。问的不少问题是相似的,我把常见的问题整理了一下放在这里。这样的话,对于一些基本问题,我们的看法可以更明确一些。</li> </ol> <h3 id="准备知识">准备知识</h3> <p>现在我们先看准备知识。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/04.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/04_hu2c715cc3fdf8b5d73605900fd09df2c2_37581_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/04_hu2c715cc3fdf8b5d73605900fd09df2c2_37581_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a04" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>首先,是比特币脚本的简短历史中,几个比较重要的时间节点。</p> <p>一开始,比特币脚本允许我们用可编程的方式来对比特币进行操作。</p> <p>后来,sCrypt 工具出现了之后,开发效率提高了,我们得以使用一种高级语言的方式来实现比特币编程。</p> <p>在去年,scrypt 为我们提供了一个机制,最开始是由 nChain 发现的 <code>OP_PUSH_TX</code> 技术,scrypt 把它做到了内置的功能里面,让我们能够有机会去读取交易里边的一些比较重要的字段信息,这些信息能帮我们做很多原来做不到的事。我们认为 <code>OP_PUSH_TX</code> 是很多技术得以实现的基础。</p> <p>再后来,在 <code>OP_PUSH_TX</code> 的基础上又往前走了一步,有了 <code>Stateful Contract</code>,有状态的合约,使得我们能够把一个合约里的状态往后传递。去年老刘在分享时也讲过,这使我们得以实现一种状态机,使得比特币脚本能够不仅仅被用来控制单个交易了,可以通过多个交易来回跳跃了,在跳跃时还能携带自己的状态了,这个是我们后续可以做很多事情的基础。</p> <p>好,这是比特币脚本发展历史上,几个比较重要的节点。一路走来,你可以看到我们能够运用的能力是不断提升的。然而另一方面,也存在一些始终未能被打破的限制。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/05.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/05_hu8ecd42fabdbcefdc10d2d89d2ce3cfa5_50050_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/05_hu8ecd42fabdbcefdc10d2d89d2ce3cfa5_50050_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="aBitcoin script limitations" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>在我们的白皮书感应合约的白皮书里,你可以看到一张图,整个白皮书应该就只有这一张图。这张图主要是用来说明,在感应合约之前,比特币脚本能做什么,不能做什么。</p> <p>你可以看到,图上就是打问号的那两个地方,正好对应着我们一开始提到的溯源和协作:一个是脚本往前追溯的能力,也就是 <strong>“我从哪里来”</strong>;另一个是脚本与它相邻的脚本,也就是跟同一笔交易内的其他输入之间的协作的能力,也就是 <strong>“我是谁/有谁同我一起?”</strong>。</p> <p>这两个能力,我们认为是比较关键的能力。</p> <p>好,我们首先来看前一个问题——<strong>溯源</strong>。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/06.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/06_huc706093f283f6b761703e5f24a540bef_29213_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/06_huc706093f283f6b761703e5f24a540bef_29213_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a06" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>很自然地,你可能会问一个问题,我们为什么需要溯源?</p> <p>因为需要解决 <strong>合法性</strong> 的问题。不仅仅是 token,很多比特币系统内的业务逻辑,都依赖于合法性问题的解决。</p> <p>那么,究竟什么是合法性问题?</p> <p>不管是 Token 也好,程序代码创造出的其他各类资源也好,在发行和转移的任意环节,都有可能被伪造。攻击者伪造之后,你的系统如果识别不出来真伪,或者说不能以最低的成本识别出来,就会造成灾难性的后果,或者说,至少也被迫需要处理许多你本来预期不到的情况,你的系统会异常的复杂。如果能够低成本实现这种防伪的工作,并将其收敛到非常小的区域,甚至是把它以用户无感的方式,以应用无感的方式把它给封装起来,那么不同的应用就可以更简单地使用自己创造的资源。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/07.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/07_hucc88032ee06864fc0a00b3113a128d89_67356_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/07_hucc88032ee06864fc0a00b3113a128d89_67356_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a07" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>那么往前再走一步,脚本资源的合法性,为什么是一个需要被解决的问题?或者说,比特币的这个 satoshi 怎么就不需要溯源,不需要解决合法性问题呢?</p> <p>这是因为,比特币本身,跟我们创造出来的资源是不一样的,比特币价值单位是 sat。大家知道, sat 是一个非常特殊的单位,是系统内部独立存在的,跟脚本没有直接关系。你的每一聪,都是一个独占的系统资源,独占性由节点软件来保证,这是系统的义务。</p> <p>也就是说,比特币节点软件,有义务保证每一聪都会被正常的计算。</p> <p>这种排他性和独占性,还表现在其他的地方:只要你能解锁,就能控制 utxo;你花了,人家就花不了,这是一种由系统赋予的独占性。不管你写的代码有多厉害,或者是写得很糟糕,无论如何,你都没办法去通过编程来增加它,或是减少它。它的数量恒定,脚本无关,也都是我们熟知的,非常重要的比特币特性。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/08.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/08_hu594053491e1576a720fb4589ea3dde18_69131_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/08_hu594053491e1576a720fb4589ea3dde18_69131_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a08" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>那么 token 或者说其他的通过脚本创造的资源,跟比特币本身的价值单位有什么不一样呢?</p> <p>我们通过比特币脚本创造出来的资源,其实是不具有这种 <strong>数量的恒定性</strong> 和 <strong>代码的无关性</strong> 的。假设你在脚本里算错了,不小心在某个很关键的业务节点上,多算了一步,或者说多加了一次,多减了一次,那么你的 token 可能就出问题了。或者说,你有某一个特殊的情况没考虑到,那么一旦发生,你的 token 可能就不能用了,或者这个 utxo 你没法解锁了。除了这种主观上的犯错,当其他人发现了系统缺陷之后,成功地伪造了一笔能够通过你的检测的 token,这也是有可能随时会发生的情况。</p> <p>不管主观原因也好,客观原因也好,都是因为 token 不是比特币系统的 “一类公民”,所以我们才需要通过某种机制去确保它的合法性。</p> <p>为什么一开始我把这个问题说得这么细,因为这就是我们整个解决方案的出发点。我们一定要知道比特币脚本它本身的限制,也就是最开始的边界在哪里,才可以知道这个东西的适用性,能帮我们提升哪方面的能力。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/09.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/09_hua5eb5d2f7675e97556ff5074d986a0e2_64646_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/09_hua5eb5d2f7675e97556ff5074d986a0e2_64646_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a09" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,我简单介绍一下,在 Sensible 之前的 BTP,也就是一种典型的依赖 oracle 的方案,是怎么来解决这个问题的。在感应合约前,也出现了很多系统,有很多 token的方案,国内和国外都有。</p> <p>BTP 的方案是基于一个外部 oracle 的,它通过 oracle 来实现了一个区块链外部的机制来维护 token 的合法性,可以看作是区块链的插件。</p> <p>从实现的角度,它维护了一个 utxo 集,来保证能以最低的成本确认 utxo 是否合法。这个解决方案非常精巧。</p> <p>然而,从工程角度,如果因为创建和维持了一个 off-chain 状态,这个状态是存在于区块链之外的,那么就得持续维护,确保它始终是可用的。这就涉及到状态要及时更新,当出现问题的时候,它需要能被随时恢复。在真实环境中可能会出很多预期之外的问题,不仅仅是软硬件的问题,还有其他的各种问题,比如节点限制,节点状态,内存池状态等等,会导致服务不可用。除此之外,这个链外状态集还需要关心状态的膨胀,因为比特币的 utxo 是不断增大的。然后也需要考虑部署多个 oracle 来避免单点。</p> <p>总得来说,一旦引入了一个状态的话,你就要对它负责,做更多的事情。</p> <p>好,这是在 Sensible Contract 之前,一个典型的 oracle 的方案,是怎么解决合法性问题的。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/10.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/10_hu27968280110b7a91d29dfdf64517e350_57142_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/10_hu27968280110b7a91d29dfdf64517e350_57142_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a10" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>开始之前,我再简单介绍一下 <code>OP_PUSH_TX</code>。理解了 <code>OP_PUSH_TX</code>,有助于你了解 PreImage 的作用。</p> <p>我们可以看到,右边的这 10 个字段,对应 PreImage 结构,这个结构在解锁的时候你把它传进去,就可以通过一个生成的私钥,使用 <code>OP_CHECKSIG</code> 来证明它是真实有效的字段。这就是 <code>OP_PUSH_TX</code>,通过它,我们得以把交易的关键信息推入堆栈,使得你的比特币脚本逻辑能够验证并利用这些信息。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/11.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/11_hufe4610bd65106930bb207595c1e239c1_47898_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/11_hufe4610bd65106930bb207595c1e239c1_47898_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a11" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>紧接着,我再介绍一下 Stateful Contract。</p> <p>大家看一下图,PreImage 第 8 个字段字段是 hash output,能够把当前你正在操作的交易的所有输出哈希一下,这就能被用来验证并保证后继交易的输出脚本一定是你想要的样子。通过这个机制,它可以实现从前一笔交易到后一笔交易的逻辑上的延续性。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/12.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/12_hu07096c9689d313f9860f39343eff03de_44529_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/12_hu07096c9689d313f9860f39343eff03de_44529_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a12" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,到这里我们小结一下。</p> <p>在准备知识里,我们看到了比特币的演化,看到了合约的限制,看到了为什么脚本从本质上需要溯源来保证合法性,然后一个典型的 oracle 方案是怎么来解决溯源的,我们也简单地了解了 <code>OP_PUSH_TX</code> 和有状态的合约的实现。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/13.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/13_hu063e0c0dabe297af2d9a2e68fbd52290_28387_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/13_hu063e0c0dabe297af2d9a2e68fbd52290_28387_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a13" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来,我们开始进入 Sensible Contract 相关的内容。</p> <h3 id="分步讲解">分步讲解</h3> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/14.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/14_hue1b2a60e076564abbf3fd1e653db239f_61594_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/14_hue1b2a60e076564abbf3fd1e653db239f_61594_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a14" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>在开始之前,我们先有几个约定,不然的话,很难通过讲述把话说明白,很容易绕进去出不来。</p> <p>我们定义了三个概念,这个有点绕,我慢慢说,大家看一下。第一个概念是,你当前正在解锁的 utxo 所在的交易,也就是大家可以看到右下角的 <strong>“当前交易”</strong>。 我为什么要解锁一个 utxo,因为我要进行某个操作。</p> <p>现在注意,我需要解锁的 utxo 一定是来源于一笔有效的交易,不然我没法拿着它。这笔有效的交易,我们叫它 <code>PreTX</code>,也就是 <strong>“前序交易”</strong>。为什么叫 <code>PreTX</code>?因为它是当前交易的前一笔交易,所以叫 <code>PreTX</code>。</p> <p>我们给它另外定个名叫 <b>TX<sub>n</sub></b>,意思就是第 n 笔交易。</p> <p>那 <code>PreTX</code> 的前一笔是什么呢?我们叫它 <code>PrePreTX</code>,很自然的,可以被称为 <b>TX<sub>n-1</sub></b>。</p> <p>这样就比较清楚了,你知道现在我们有了 <b>TX<sub>n</sub></b>,也有了 <b>TX<sub>n-1</sub></b>。</p> <p>最后一个概念是,所有这些交易追溯到最初的第一笔源头的交易。就像初始区块一样,这个交易我们叫它 <strong>创世交易</strong>,也就是 <code>GenesisTX</code>,我们也给它一个名字叫 <b>TX<sub>0</sub></b>。</p> <p>图上你还可以看到,在这一串交易中间,有一条竖线,这条竖线表示当前你所在的位置,也就是 <strong>“想要解锁还没解还没成功解锁”</strong> 这个状态。</p> <p>接下来的讲述,就会围绕 PreTX,PrePreTX 和 GenesisTX <strong>这几个交易之间的关系</strong>展开。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/15.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/15_hud376af59b59e9e5ff68a0b4da075bd21_50212_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/15_hud376af59b59e9e5ff68a0b4da075bd21_50212_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a15" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>首先我们先来看一看,一个简单的回溯方案是怎么做的。很简单,一页PPT就可以讲完。</p> <p>他是怎么做的呢?我们知道有状态合约能往后延续,但是不能往前回溯。怎么往前回溯?很简单,你把前一笔交易 PreTX 跟前前一笔交易 PrePreTX 都传进去,然后确认他们俩直接相关,这种相关性是真实有效的,那么不就能证明 <b>TX<sub>n</sub></b> 和 <b>TX<sub>n-1</sub></b> 是直接相关的了吗?</p> <p>这是一个我们能想到的最直接的方案。</p> <p>这个方案听起来理论上无懈可击,只有一个问题:<strong>会导致膨胀</strong>。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/16.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/16_hucf0603a9562da97da5be406d36fe0d81_66406_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/16_hucf0603a9562da97da5be406d36fe0d81_66406_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a16" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>你可能会问,为什么会膨胀?</p> <p>因为我们把前面的交易推到后面交易里,然后把后面的交易再推到更后面的交易里,前面的交易不断堆积到后面的交易里,就会导致单个交易整体上不断膨胀。</p> <p>那膨胀得有多快?</p> <p>答案是,非常快。准确说,这是一个斐波那契膨胀,因为 <b>TX<sub>n+1</sub></b> 的尺寸,是 <b>TX<sub>n</sub></b> 和<b>TX<sub>n-1</sub></b> 的尺寸之和,除此之外,还包含了一些它自己的信息,所以实际上是略略超过正常的斐波那契数列的。它的增长速度接近 2<sup>n</sup>,看看下面这个图,就知道膨胀的速度是非常快的。</p> <p>这里我们之所以关注膨胀问题,是因为它就是 Sensible Contract 在一开始的出发点。从这里出发,我们可以一步一步勾勒出全貌。我尽量按照故事本身的来龙去脉来讲述,这样逻辑整体上自然,容易理解一点。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/17.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/17_hu58e17e60651a8ffb14dd72363c3833bb_39101_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/17_hu58e17e60651a8ffb14dd72363c3833bb_39101_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a17" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,现在我们要想解决膨胀的问题,那么怎么有效地消除这种膨胀呢?</p> <p>我们分析一下膨胀的来源。膨胀的内容是在解锁脚本里的,对吧?正是因为你需要解锁,才需要把 PreTX 和 PrePreTX 给推进去,那么所有导致膨胀的东西,都在解锁脚本里。那么很自然的,我们就会想到,解锁脚本是那个具有“膨胀性”的东西,你不能一股脑把所有东西都塞到解锁脚本里,应该只放那些 <strong>有限且固定</strong> 的东西。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/18.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/18_hu4b4325c2f60b3cb1d794f8dc7586bc02_40845_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/18_hu4b4325c2f60b3cb1d794f8dc7586bc02_40845_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a18" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>到这里就已经比较明确了,什么是 <strong>有限且固定</strong> 的东西?</p> <p>对,就是锁定脚本。</p> <p>使用锁定脚本有两个原因。</p> <ol> <li>锁定脚本可以拿来识别脚本的行为,因为脚本就是行为,如果锁定脚本一样,那么行为就一样。</li> <li>锁定脚本虽然可能是很长的一堆代码,但它是固定长度的,不会没有限制地增长。</li> </ol> <p>所以很自然的,我们想到,是不是可以把锁定脚本直接推进去,这样不就能保证行为一致性又不膨胀了,对吧。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/19.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/19_hu3c39858852c9f6704e0fe0957549f4bb_26673_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/19_hu3c39858852c9f6704e0fe0957549f4bb_26673_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a19" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>然而,问题又来了,如果说锁定脚本可以作为指纹,被用来做匹配,这样就足以维护一个有效的 token 系统了吗?</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/20.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/20_hubaecb76b9d4ff3fe8caa5b9c92f4dc93_36398_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/20_hubaecb76b9d4ff3fe8caa5b9c92f4dc93_36398_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a20" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>其实是不够的。</p> <p>因为,从攻击者的角度,完全可以给你弄一个一模一样的锁定脚本,而且这个锁定脚本跟你系统毫无关系,是一个来自外部的交易,然后也能通过检测。</p> <p>这时候怎么办?</p> <p>此时很自然的,我们就会去想,应该通过某种机制,不仅保证它的内容是一样的,而且能够确保交易前后的相关性。</p> <p>那么如何确保相关性呢?</p> <p>可以通过签名的方式来实现。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/21.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/21_huad3237d527d872a5e2c773c9af138fda_38217_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/21_huad3237d527d872a5e2c773c9af138fda_38217_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a21" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这里我们用的是 Rabin 签名,昨天蒋杰也讲过。Rabin 签名的特点是什么呢?验签非常简单,很方便在脚本里通过简化的计算就可以验证这个签名。</p> <p>具体地说,这个 Rabin 签名,用的时候分成两步:</p> <ul> <li>首先,使用合约之前,我们在合约外对我们需要的那几个字段做签名;</li> <li>然后,在合约内部去验证这个签名,来确保我们签的那几个字段是有效的。</li> </ul> <p>通过这两个步骤,我们就可以确认他们的相关性了。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/22.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/22_hu6a0f080e00986449366a9159fe05c5e4_71728_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/22_hu6a0f080e00986449366a9159fe05c5e4_71728_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a22" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这个时候,你关心的是,究竟需要哪些信息,才能帮助你确认前后交易,也就是 <code>PreTX</code> 和 <code>PrePreTX</code> 的相关性。所谓相关性,就是说,如果我们有两笔交易,a 和 b,可以明确地确认 a 是 b 的直接后继,这就是相关性。</p> <p>那么哪些字段能够被用来做这个事呢?</p> <p>你可以看到感应合约的白皮书里面提到这样一个叫 u4 的东西,也就是图上这个结构。</p> <p>对一个(解锁中的)合约而言,u4 内包含了我们认为是最有价值的几个上下文的信息。当然,如果想扩展它做更多的事情,也可以放入其他的信息。但是就目前而言,最必要的就是这几个信息,一个是它的 outpoint(也就是 txid 和 index),然后是锁定脚本,它的比特币数量(也就是 satoshi amount),最后,也是最关键的东西,就是这一笔交易是被谁花费的(也就是 spent_by_txid 字段).这个字段是最关键的,因为正是由他来确定,前面给出来的 outpoint,是被后面的这一笔 txid 花费了。</p> <p>系统通过 Rabin 签名的方式把这两笔交易绑定到一起,这样你在合约里就可以去验证(对应着 PPT 上位于屏幕下方的验签逻辑)——前面的输出确实是被后面的 txid 给花掉了,于是就可以确认 <code>PreTX</code> 和 <code>PrePreTX</code> 它们俩的相关性了。</p> <p>这个签名出现在这可能有点突兀,大家看白皮书的时候可能会有点懵,怎么突然就冒出来一个签名呢?实际上,这个签名的目的很单纯,就是为了解决相关性的问题。就是为了向你证明,不仅仅锁定脚本是一致的,而且它也确实来自于 <code>PreTX</code> 的前一笔交易,也就是 <code>PrePreTX</code>。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/23.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/23_huaa5d3c19fcd8b24695bfa39f944c711a_56431_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/23_huaa5d3c19fcd8b24695bfa39f944c711a_56431_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a23" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,现在回顾一下刚才我说的流程,首先你在解锁时把 PreImage 压进去,这样你就得到当前正要解锁的 utxo 的锁定脚本,我们叫它 <b>L<sub>n</sub></b> (L就是 Locking script 的意思);然后,把你需要的锁定脚本压进去,就是前序交易的锁定脚本,我们叫它 <b>L<sub>n-1</sub></b>,那么你实际上你已经知道了 <b>TX<sub>n-1</sub></b> 与 <b>L<sub>n</sub></b> 一致,但你并不知道它的合法性是怎么保证的,也就是接下来的第三步。</p> <p>在第三步里,我们需要去验证它,通过验证这个 Rabin 签名,我们得以确认 <b>L<sub>n-1</sub> = L<sub>n</sub></b> 的合法性是可被验证的,所以套上个红框,表示它们俩确实是有这个相关性。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/24.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/24_hu02bcce15d874537c8b014c2f5095dddb_26042_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/24_hu02bcce15d874537c8b014c2f5095dddb_26042_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a24" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,这里请大家注意一下,我们一开始的初始方案,保证锁定脚本完全一致,其实是一个最简单的方案。实际上还有很多方案,只要你能够保证他们之间的授权关系,并不一定是非得要压入一个完整的锁定脚本,并且保证他们完全一致,其实有多种方案,锁定脚本一致只是我们最开始的方案。</p> <p>今天接下来的课程里,陈诚与蒋杰会与大家分享,我们在不同的情况下,怎么样用一些扩展的方案来解决问题。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/25.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/25_hucfd1d995b87fcc9f560bb26bde77a6fd_33531_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/25_hucfd1d995b87fcc9f560bb26bde77a6fd_33531_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a25" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,确定了单个交易的相关性之后,我们通过图的方式,可以把“从单个交易到整个交易链条”的推导过程,说得更清晰更明确一些。系统的有效性得以从单一扩展到整体。这个过程有点抽象,用图来表示会比较容易理解。</p> <p>正如你看到的那样,在当前的这笔交易和过去的那笔交易之间建立关联性,需要两个条件,第一个条件是锁定脚本一致,第二个条件是签名被验证。这样我们可以从当前交易回溯到上一笔交易。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/26.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/26_hufa5a9c46a2b43f3e0e1ad84a5ca56d46_30301_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/26_hufa5a9c46a2b43f3e0e1ad84a5ca56d46_30301_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a26" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这个时候,我们理论上是可以这样一路回溯上去的。当然其实你是不用做这件事,为什么?因为区块链已经固化到区块里了,你实际上不用手动做这件事。但是从逻辑上讲,它是这样一步一步推过去的,从当前交易你可以建立跟上一笔的相关性,而上一笔对应着上上笔&hellip;这样一路推过去,最终你会推到 <b>TX<sub>1</sub></b>,也就是第 1 笔交易。</p> <p>请注意,这个时候问题就来了:第 1 笔交易跟第 0 笔交易是怎么保证的?</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/27.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/27_hu24ddf643605796c7882ac8816091cd78_26354_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/27_hu24ddf643605796c7882ac8816091cd78_26354_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a27" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>第 1 笔交易跟第 0 笔交易,是通过创世交易的标记来保证的。</p> <p>我们在创造 token 的时候,你不是要发一笔创世交易,表示我现在要创造 token系统吗?</p> <p>你发的这笔交易 GenesisTX ,实际上是整个 token 系统的原点,那么它一定有一个独一无二的标识,我们把标识提取出来放到 token 系统里边,甚至从浏览器的角度,你只要看到这个标识,你就知道这个 token 一定是从那里来了。这个标识里可以写一些你需要的信息,来标识它的身份,让人一眼就能看出来它。这就好像百家姓一样,给定任何一个人,只要你知道他姓什么,立刻就知道了他是源自哪一个谱系。</p> <p>我们通过匹配标识,来保证从第 1 笔交易到第 0 笔交易的相关性。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/28.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/28_hubba2fb4ccd44350ac6b9125750b1f53a_34158_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/28_hubba2fb4ccd44350ac6b9125750b1f53a_34158_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a28" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>那么更进一步,怎么把这整个系统给连接起来呢?</p> <p>在任何一笔合约里,你就会写这样的逻辑:对于任何一笔给定的合约交易 <b>TX<sub>n</sub></b> 也就是你当前要处理的这笔交易,它要么来自于 <b>TX<sub>0</sub></b>,那就是我刚说的第二种情况,它的上一笔是创世交易,它自己是 <b>TX<sub>1</sub></b>。要么往前追溯,来自于 <b>TX<sub>n-1</sub></b>,也就是说它的上一笔是一笔合法有效的交易。</p> <p>只要你满足了(这两个条件中的任何一个),要么你自己是 <b>TX<sub>0</sub></b> 后面的 <b>TX<sub>1</sub></b>,要么你是<b>TX<sub>n-1</sub></b> 后面的 <b>TX<sub>n</sub></b>,只要你满足了这两个条件,就可以把中间的链条全部串起来。</p> <p>这就是大家高中数学学过数学归纳法,对吧?他们的合法性是这样一步一步推出来的,数学归纳法就是这样工作的。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/29.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/29_hu4f7b32692f72c40d9a853abe6f01cac6_53515_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/29_hu4f7b32692f72c40d9a853abe6f01cac6_53515_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a29" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>通过前面的推导,我们建立了合法性的推导过程:</p> <p>任何一笔有效的交易 <b>TX<sub>n</sub></b>,它的合法性,要么是来自于前一笔有效的合约交易 <code>PreTX</code>,也就是 <b>TX<sub>n-1</sub></b>,要么是来自于创世交易 <code>GenesisTX</code>,也就是 <b>TX<sub>0</sub></b>。同理,当你想创造新的交易时,合法性也会向后顺延,传导给它。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/30.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/30_hu6b1972e17dbc872314918d60ff01aa93_63549_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/30_hu6b1972e17dbc872314918d60ff01aa93_63549_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a30" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>解释完合法性的传递,整个溯源逻辑就完整了。接下来说一下具体的实践。我们在实践中遇到了一个合约尺寸的问题,昨天蒋杰这个问题也讲了,他讲的相对高阶一点,我们这里是比较初阶的内容。</p> <p>在解锁的时候,可以看到它推了两个锁定脚本进去,也就是说,你的锁定脚本如果编出来是5k的话,你解锁的时候需要10k。</p> <p>因为你的解锁定脚本里面都是代码,一定是比较大的,如果你没有做拆分的话,平白无故的变出来两份是很奇怪的,一个是 <b>L<sub>n</sub></b>,一个是 <b>L<sub>n-1</sub></b>,白白占据了双倍的空间。</p> <p>那么应该怎么优化呢?</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/31.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/31_hubec315d5bd0e525b7576ee5af6fa9f48_58955_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/31_hubec315d5bd0e525b7576ee5af6fa9f48_58955_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a31" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>我们注意到,前序交易的锁定脚本,不需要传它的完整内容,可以改成传它的哈希。这样 PreImage 传进来锁定脚本以后,把它以同样方式哈希一下,比较二者是否一致就可以了。比较两个哈希,比直接比较它们的内容要省事很多。</p> <p>这里有个细节,使用了 hash160,就是生成比特币地址的方式,这样的话合约协作时会更方便一些,这是一个小细节。</p> <p>通过这种方式,我们保证了解锁时,解锁脚本内,只需要传递一份锁定脚本的副本。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/32.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/32_hu5e65b7f921b05583e728592bb22f1650_29401_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/32_hu5e65b7f921b05583e728592bb22f1650_29401_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a32" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,到这里为止,刚才分成几个步骤,把感应合约的溯源部分完整过了一遍,实际上,不同合约之间的协作也是类似的原理。</p> <p>在白皮书的编写的时候,我有一个点处理得有点问题,总是把溯源和协作同时讲解,这样是不对的。后来我意识到这个问题,其实是应该拆成两份文档,一份专门讲溯源,一份专门讲协作,这样大家就不需要同时跟两条线。之后我会改进这个问题,毕竟白皮书才 v0.2 版对吧。晚点的时候我们希望能有一个更好的版本给大家。</p> <h3 id="答疑">答疑</h3> <p>好,接下来我会对一些问题做一些集中的回答。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/33.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/33_hu073baae27337897c683ce9dabaea0385_64078_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/33_hu073baae27337897c683ce9dabaea0385_64078_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a33" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>第一个最常被问到的问题,为什么我们不能把这个签名器放到合约里?</p> <p>很多人用各种方式问了这个问题,为什么需要额外加一个签名器,而不是把计算过程通过合约本身来实现呢?这样不就消除了对外部机制的依赖了。</p> <p>简单说一下,如果你要对一个数据做签名,那么你一定是需要这份数据的,对吧?你需要这个数据,就需要把这份数据放到你能访问的地方,对吧?如果你想要在交易(也就是合约)里处理,那你就得把这个数据放到交易里,对吧?你放交易里你就会膨胀,就就绕回到原点了,只要你通过任何方式你把它放进去,那你就会膨胀。</p> <p>我们签名的目的,就是为了把导致膨胀的因素,从区块链上移下来,弄到链外去。而这种迁移是有技巧的,有多种不同的迁移方式。通过签名器来移,最大的好处就是,能保证签名器本身的行为足够单一固定,它处理的数据也是单一固定的。相对而言,你不用太担心签名器代码写的不好,出bug了或者什么特殊情况,因为它很简单非常简单,就那么几十行。越简单的东西,出 bug 的可能性就越低。</p> <p>并且,由于签名器没有状态,也不需要你去维护它,你甚至都不需要给他一个很好的服务器,因为他只是执行了非常朴素的一个运算,在实践当中我们是部署在 aws 的 lambda 上面,用腾讯云的话就是云函数,你用个云函数,连存储都不需要,云函数还是免费送的,就可以一直不停的跑签名了。</p> <p>简单说,你不应该把这些数据弄到链上,因为只要你弄到链上,就会造成它的臃肿膨胀,那么就需要一个额外的链下签名器。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/34.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/34_hub622c4893bcb434d7936d4fff4193e30_55373_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/34_hub622c4893bcb434d7936d4fff4193e30_55373_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a34" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>第二个问题,为什么升级 PreImage 就不需要签名了?大家看白皮书里边有一段专门说了这个问题。感应合约有两个实现,一个实现是通过一个外部的签名器,而另一个实现是在 PreImage 的 10 个字段之后,再增加两个字段,一个字段用于溯源,一个字段用于协作。</p> <p>增加的字段是什么?去对之前交易的数据进行希,然后确保我们可以在解锁脚本里把对应的信息传进去,来验证他们俩是不是有效的。为什么我们在这个节点软件里边做更好,因为节点软件里面有足够的上下文,因为 C++ 里啥都有,你直接在里边算好了,从整个系统的角度来讲,其实是最优解,系统结构也是更简单的。我们认为这是一个合理的天然的操作,但是因为这涉及到对 PreImage 的修改,这个问题争议非常大,从它诞生的第一天争议就非常大。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/35.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/35_hu39c9e824731a1985dc827a01ca8f6b7e_80646_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/35_hu39c9e824731a1985dc827a01ca8f6b7e_80646_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a35" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>我们来看看具体有什么争议。有不少关心这个问题的人,他们也许根本都不关心感应合约具体做了些什么,他们就关心这个,反复问,你这个东西是要掀桌子吗,是想要把基本协议给改掉吗?其实不是的,这恰恰是这个提案最重要的地方,而且是最精巧的地方。</p> <p>在感应合约之前,我们也看到了一些方案,这些方案确实是需要修改协议。而感应合约不同的是,它保证了修改只是针对 PreImage 本身,而 PreImage 我们认为它是 2016 年,也就是后期才被引入比特币的软件实现。它的出现也是有很多历史原因导致的。</p> <p>我们认为 PreImage 不是比特币原始的基本协议的一部分,只是一个实现软件实现,我们认为升级一下,应该是问题不大的。当然这个事情非常有争议,我们可能出于立场的不同,对这个问题的看法是不一样的。回到刚才这个问题,升级 PreImage,最大的好处就是,我们连额外的签名机制都不需要了,这是对所有人都方便了。</p> <p>但是在这里我还想补充一点,如果说我们开了这个口子,假设我们在某个几率很低的情况下,真的改了 PreImage,并不意味着就一劳永逸,无需再改了,随着发展总是会涌现出新的需求。</p> <p>也许就像 Xiaohui 说的,你可能明天又需要什么新功能,是不是又要改 PreImage?小明能改,那小红能不能改?小蓝呢?</p> <p>这个时候,可能就需要有一个人,或者一个标准委员会这样的组织,来鉴别哪些改动,从比特币发展的角度来讲,是长期有益的。</p> <p>从实践中,各位也能看到,在比特币发展的十年的历史里面,节点软件引入了各种各样的东西。在那之前好像并没有一个类似以太坊的机制,来使得我们以一种有效的方式来迭代比特币软件。它的成长可以说是一种自然的成长,并不是一种人为规定的发展方向。包括是追溯到最开始的 1MB 的限制,都是以一种自然的状态(被加上去的)。这里是不是应该加一个 1MB 的限制,防止被攻击?好,那就加呗。那就加上去了。</p> <p>这可能涉及到更深层次的问题了,就是一个软件究竟应该怎样被开发出来才是合理的。</p> <p>《大教堂与集市》。不知道大家是否看过这本书。它主要讲的是,究竟是通过集市的方式,闹哄哄的开源市场,大家你一砖我一瓦来建设一个集市的方式,来造一个软件,还是通过严谨的规划和系统性的设计,来造出一个浑然一体,金碧辉煌的大教堂,一直都是争论的焦点。</p> <p>有的人觉得,靠开源的方式,我就能造出大教堂,你看看 Linux 不是典型的例子吗,对吧。</p> <p>我说下我的看法,首先以我的见识我很难去判断这样一定是不行的,那样一定是行的,但是我想说,大教堂有大教堂的可取之处,而且大教堂造出来的东西,真的有可能是其他方式不一定能造出来的。</p> <p>集市有集市的优点,集市的方式也能造出来很好的东西,这一点我们都看到了。说回 Linux,据我了解,Linux的社区近年来有什么问题?近年来,年轻人不再喜欢这种方式了。大家原来以为,开源是一个不可逆转的趋势,所有的人早晚都会来开源社区做贡献,会形成蒸蒸日上,日益增长的开发者社区。但是你可以看到,Linux 的核心开发者越来越少,第一越来越少,第二没有年轻人愿意来,全部是老人,全部是像我这样或者比我更大,比我大五六岁甚至十来岁的四五十岁的程序员。这反映了一种趋势,我不知道这个趋势是5年到10年的趋势,还是更长时间的趋势。似乎大家对这样的方式去构造软件,能否最终达到一个理想的结果,是开始有了疑虑的。</p> <p>这里时间关系我就不展开了。我个人的立场是,如果有可能的话,我会倾向于尽量去开源贡献我的东西,然而对于一个确定性的目标,我们要做长期的规划,用商业的方式去做可能会更适宜。总得来说,这两种方式我都不排斥,在不同的情况下以不同的方式去做好了。这个可能偏题了,跑的有点远,借这个机会跟大家分享一下我的一些想法。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/36.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/36_hudbcf0ad5ada51a44646a0e7da9d28b9d_63926_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/36_hudbcf0ad5ada51a44646a0e7da9d28b9d_63926_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a36" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,第4个问题,你说你这个系统能保证没法伪造对吧,真的没法伪造吗?</p> <p>我们来试一下就知道了。</p> <p>假设有一个输出,这个输出我们叫它 <b>Output<sub>fake</sub></b>,就是伪造的输出,里面包含了一笔看起来一模一样的锁定脚本。这个输出来自于交易 <b>TX<sub>n&rsquo;</sub></b>。那么这个伪造的 <b>TX<sub>n&rsquo;</sub></b> 它在溯源的时候会失败,为什么?因为它溯源需要提供一个合法的 <b>TX<sub>n&rsquo;-1</sub></b>。</p> <p>也就是说,光伪造你自己不行,你得伪造你的上一笔。这时候你就遇到问题,你可以说,我可以一直伪造上去,对,没问题,如果你一直伪造,最终一定会来到一笔系统外的交易,那个时候仍然绕不开上面这个问题,也就是前一笔和前前一笔之间的相关性的问题。</p> <p>伪造交易 PreTX,与一个系统之外的 PrePreTX,无法顺利建立相关性。</p> <p>你可以伪造自己,也可以伪造自己的后继交易,但是你没法伪造自己的前序交易。你没有办法伪造一个已经发生了的事实,一个伪造的交易,一定是来源于一个不合法的交易,对吧?它一定是来源于一个不合法的交易,那才谈得上是伪造,如果你来源于一个真实的交易,就谈不上伪造了。</p> <p>如果你提供了一个真实的交易,你就不是伪造,你是真的;如果你都提供不了的话,你这个一定是溯源会失败的,你自己就没法被解锁,你的后代当然也是没法被解锁,那就说明什么?说明你自己和你的后代不能够有效的通过溯源来关联到一起。</p> <p>你只能伪造未来,不能伪造过去,你之前的发生的事实,是已经既定的事实,想伪造就一定是来源于一个系统之外的tx,这样就没办法建立这种相关性,于是溯源的时候就会失败,这是整个系统的核心逻辑。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/37.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/37_hufcb82656ea22f523dd24f96b39d62569_41204_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/37_hufcb82656ea22f523dd24f96b39d62569_41204_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a37" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,还有一个经常被问到的问题,就是签名器作恶有什么办法,传什么参数都给你返回 True,或者说是被黑了,或者是服务器过期了,签名器不可用,等等,怎么办?</p> <p>因为今天早上第一节课是一个基础理论性质的课,我只谈最基本的东西。扩展的高级话题,陈诚和蒋杰会给大家分享。最基本的方案,就是在你的合约里边包含多个公钥,比如说包含20个,然后只要其中的10个认为有效,我们就认为是合理的。</p> <p>一下子攻击 10 个签名器的概率是比较低的,那么就可以保证这个系统,至少在签名器这一层是健壮的。更高级一点的办法就是蒋杰昨天说到的版本号机制,可以淘汰老的签名器,增加新的甚至还有更高级的用法。我们开发群里边比较活跃,旺仔在公钥授权这块也提了一些很好的想法。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/38.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/38_hu61035880e521d9ee25b3767de0643d3a_62344_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/38_hu61035880e521d9ee25b3767de0643d3a_62344_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a38" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>剩下的两页内容实际上是第一天工具概览时候的内容,为了完整性,我也把它包含进来了,可以作为一个回顾。</p> <p>感应合约有几个特性,跟其他的方案相比,它是没有外部状态的,oracle 不需要认证,然后它不需要额外的 op-return。它的数据段是包含在合约里的,你可以对它进行一些操作和验证,然后它是完全由矿工来验证的,而且它对比特币的其他的机制是友好的,我们可以说它 “bitcoinic”,比如说 metanet 可以跟他一起很好的工作。今天下午会跟大家分享一下这一块的案例。而且它是跟 bitcoin 一样是支持 SPV 的,既不比比特币更多,也不比比特币更少。</p> <p>你的模型,可以体现出跟比特币本身类似的特性,是非常有趣,也非常有用的。它带来的潜在好处,可能大于我们原先的认识。</p> <p>比如说像昨天群里有同学问,冻结(Freeze)怎么样,能实现吗?</p> <p>当时我跟蒋杰讨论,第一反应就是,这功能不难实现。蒋杰跟我说,就签名器里边弄一下就好了。实际上,如果什么(新加的功能)都在签名器里弄,长期看又会遇到 Xiaohui 说的那个问题,你今天加这个,明天加那个,到底什么能加,什么不能加,到时候需求虽然满足了,加来加去,你那个东西又会很大,又会越来越臃肿,越来越庞大,是一个庞大的链外机制,这样真的好吗。那么实际上,我们可能真不一定需要做那么多,我们只要让它保证它像比特币一样就可以了,比特币能封,比特币的 utxo 能封,我就能封;比特币如果不能封,我也不能封,就好了。我们目前看没必要把精力花在这个上面。</p> <p>当然了,你可以说,我就是要实现一个合约,它就是要能封,你怎么办?你可以通过特定的机制,一些高级的用法,等会儿陈诚和蒋杰会分享。这种分享是会持续进行下去的,我们之后会做很多相关的开发工作,大家如果真的对各种新的特性感兴趣,也可以加我们的开发群,在里面跟我们一起讨论,你可以看到很多人一些很有创意的想法,实现各种各样好玩的东西,包括一些通过抵押惩罚来保证额外的安全性,这些都是很有意思的话题。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/39.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/39_huf6e4a213fdb3c0cc2775b7f4f568dcfe_53619_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/39_huf6e4a213fdb3c0cc2775b7f4f568dcfe_53619_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a39" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,还是跟第一天一样,我简单提一下它的两个缺点,第一个是引入了一个额外的签名器,第二个是因为你的绝大部分核心业务逻辑都在合约里,导致你的合约的尺寸会比那些 “把这些关键逻辑放到外头,合约只是作为一个承载物” 的方案,合约尺寸要大一些,会导致这个问题。</p> <p>礼拜一的时候我举了个例子,后来有人来问我,我稍微具体的说一下,我们的结算有一笔结算是100个输出,每一个输出是8k就导致有800多k,我举这个例子不是说交易有多大,而是说我们做了优化,使得它的单个锁定脚本很小,只有8k。是这个意思。朋友说你这个是不是要说你弄能弄到多大,我说能弄到多大,你想弄多大就弄多大,这取决于上限(目前 TAAL 是 100KB)。</p> <p>我们确实是因为逻辑在合约里导致合约尺寸比人家大一些,但这些是可以通过一些方式去消解和简化的,等会就可以听到一些方案是可以缓解这个问题,甚至说能够做得比其他方案尺寸更小。我们也相信 scrypt 会在脚本尺寸这一块做出很多努力来优化,这里面的优化空间优化余地是非常大的。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/40.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/40_huef691691e1389b38991bdf717a07606a_45273_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/40_huef691691e1389b38991bdf717a07606a_45273_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a40" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,最后我们说一下 BCP。我们为什么要在比特币上去做一些模板这样的东西。其实是为了方便大家开发,提供一个默认的东西,这样每个人不用自己再去重新实现一套自己的东西。你可以不用,或者说你可以 copy 去随便改,改成自己的都没有没问题,只是给你一个很好的出发点。</p> <p>那么我们定义的这些 BCP 反映的是协议的自动化,正如周一时所说,把协议的自动化拆分成交易步骤,并且使得他们可以整体上被直接使用,能够节省大量的时间。</p> <p>从外界的观感上来讲,如果你封装的足够好,外界可以不用理解什么细节,我们讨论过一些有趣的东西,怎么把它给隐藏起来,让外界不用关心这个东西,用的时候不用太关心底下成熟的基础细节。</p> <p><img src="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/41.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/41_hubb43b980af539a1cba1628930d2d1787_27295_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-05-01-sensible-intro/slides/41_hubb43b980af539a1cba1628930d2d1787_27295_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a41" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,这就是我今天的分享,谢谢大家。有问题的话可以问。</p> <h2 id="现场问题解答">现场问题解答</h2> <h3 id="签名器的角色问题">签名器的角色问题</h3> <blockquote> <p>问:我有一个问题,就是说如果说交易量非常大的时候,这个就是提供签名期的这一方,他的动力是他怎么有动力去运行签名器,就说你跟因为每个token都会有一个发行方吗,然后你运行签名器的这一方和 token发行方会是之间会是什么样的角色,什么样的关系?</p> </blockquote> <p>这个问题可以用两点来回答。</p> <p>我们有一个直觉就是签名器好像是个第三方,但是他可以不是第三方。它可以是一个函数,所有的工具都可以集成这个函数,你什么地方都可以运行。当他足够成熟的时候,你可以预见的是绝大部分支持 Sensible 的基础设施都有可能内置,可以交叉验证,这是第一点。</p> <p>第二点,签名器是可以有一个独立的输出给他送钱的,你每签一个名,都可以给他钱。可以通过这种方式来激励不同的人运行不同的签名器,并且你可以通过来看这个签名器曾经签过多少名,来了解他的历史访问情况。他会为自己累积声誉,你就会在你写合约的时候,选择那几个声誉很好的,没出过问题的签名器,把他们的公钥给记下来。</p> <h3 id="为什么要执着于把-token-做到层一去">为什么要执着于把 token 做到层一去?</h3> <p>问:</p> <blockquote> <p>呃我还有一个问题就是说,因为在包括之前的 btc 上面也有基于 omni 的 usdt 对吧?这样的合约,包括在 bch 市场也曾经有过,然后 bsv 里面现在更多各种各样的 token 的方案,互相之间肯定是有差异。<br> 我记得你上次讲的时候,有一个表从纯一层到二层之间过渡的这样的一个,但是我觉得就是说 usdt 去发这个上面已经有很大的交易量,然后用起来也没有出现过什么问题,本身发行方他为自己负责,就是我去保证说我的所有这些 token 的总量,然后转移的这个合法性好像也没有什么问题。<br> 我们为什么就比较执着于说要求我把它做到一层去,用合约的方式来保证?<br> 还是说,是不是可以用另外一种方式,我做到二层,然后用这种比如说我发行方的信誉,或者说法律的手段去保证。<br> 这中间有什么考虑?</p> </blockquote> <p>答:</p> <p>应该这么说。通过二层本身是没有问题的,毕竟人家跑了那么多了对吧。然后,是这样一个问题,我们究竟是选择用技术的方式去处理,还是选择通过法律来保证这个主体是合法的有效的,是不作恶的。这是一个非常有争议的问题。</p> <p>按照正统的政治正确的方式来说,我们要靠法律,对吧?毕竟 code is code, law is law.</p> <p>但是从现实的角度来讲,你通过技术来保证,可能会成本更低。通过技术来保证成本更低了以后,也不排除用法律对吧?你用技术实现了成本更低的方案了以后,法律仍然是可以介入的,这两者没有直接的冲突。</p> <p>所以还是回答刚才那个问题,就是用一层的方案来实现,也许看起来好像是在试图说 code is law 的意思是吧?“在一层解决” 看起来是试图在用技术来解决一个(从二层的角度看)归法律管的问题。但是,我可以提供一个不同的解释,你通过技术来实现了低成本,然后法律仍然是可以保证的,你并没有牺牲掉什么对吧,但是你获得了额外的好处。</p> <!-- ![a40](slides/40.jpg) ![a41](slides/41.jpg) ![a42](slides/42.jpg) ![a43](slides/43.jpg) ![a44](slides/44.jpg) ![a45](slides/45.jpg) ![a46](slides/46.jpg) ![a47](slides/47.jpg) ![a48](slides/48.jpg) ![a49](slides/49.jpg) --> 2021.04 BSV 开发技术与工具概览 (v2) https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/ Thu, 29 Apr 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/ <p>这是我在 2021-04-12 BSV Bootcamp 上关于 BSV 开发技术与工具的介绍,可看作是去年10月同主题演讲的升级版。</p> <p>演讲幻灯片 PDF 可以在此下载: <a class="link" href="2021.04-bootcamp-bsv-tech-and-tool-v2.pdf" >2021.04-bootcamp-bsv-tech-and-tool-v2.pdf</a></p> <h1 id="202104-bsv-开发技术与工具概览-v2">2021.04 BSV 开发技术与工具概览 (v2)</h1> <h2 id="背景简介">背景简介</h2> <p>去年 10 月份,比特币协会在深圳主办了一次 BSV 的开发者活动。我本人在那次活动上做了一场名为 <a class="link" href="https://mp.weixin.qq.com/s/0TI492mwJL9jSkgaZsCfKg" target="_blank" rel="noopener" >“BSV 开发工具和技术概览”</a> 的分享,介绍了一些 BSV 开发相关的工具和技术。</p> <p>半年后,在今年 4 月,比特币协会在东澳岛主办了 BSV 的第一届训练营 (Bootcamp),我也接到了延续上一次的讲解 “BSV 开发工具和技术概览” 的任务,就在继承了框架的基础上,对分享的内容作了整理和更新。以下是分享的简略文字稿。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/01.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/01_hu2f2f73e0d50ee6586f4c6333f7c94ff4_26489_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/01_hu2f2f73e0d50ee6586f4c6333f7c94ff4_26489_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a01" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>大家好,我是顾露,是比元科技的创始人,也是小聪游戏的开发者,也是 Sensible Contract 的参与者,非常荣幸我们小聪游戏能协助比特币协会举办本次 Bootcamp 的活动。要是算上次在深圳的创新会的话,我们是第二次有机会作为协办方,跟比特币协会一起为开发者组织这样的线下活动,非常高兴。</p> <p>解释一下题目。半年前,我们做了一个这样的技术和工具的分享,但是因为更新很快,当时的分享包含了一些已经过时的内容,我就对内容作了适当的调整和优化,也增补了一些新出来的信息。(对这个调整版) 我给它升了个版本叫 2.0。如果有可能的话,我希望这个系列能够做下去,以便于及时淘汰掉一些不合适的内容,然后加进来一些更高质量的,我们认为对开发者更有帮助的内容。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/02.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/02_hua2ae1e1bb2bc74f3ded2c2f6a0700e86_28284_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/02_hua2ae1e1bb2bc74f3ded2c2f6a0700e86_28284_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a02" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>今天的内容框架大概是这样的:屏幕上左边的这一列是比较常规的思路,就是从协议,到工具,到服务 (数据服务,API服务等),到框架。就是构造一个 bitcoin 程序,需要的方方面面的开发资料和工具,以及如果想深入的话,理论知识需要从哪里获取。这是左边的常规提纲。然后在右边,我列了几个可能对开发者有所帮助的技术,来自几家独立的公司和团队,一个是 nChain,一个是 Xoken,一个是 Sensible。</p> <h2 id="常规的工具和库">常规的工具和库</h2> <h3 id="白皮书">白皮书</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/03.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/03_hube04bb8ad2f140c77912341df9751fae_37448_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/03_hube04bb8ad2f140c77912341df9751fae_37448_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a03" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,现在我们进入第一个主题,协议部分。</p> <p>在上次分享技术概览的时候,我没有列出最重要的协议——就是所谓 “协议的协议” ,也就是比特币的白皮书。</p> <p>白皮书的重要性,可以说怎么强调都不过分,之前没重视这点是不对的。可能不少人觉得,白皮书没多少内容,像 POW,矿工网络,这些内容,都是大家已经滚瓜烂熟,天天都在聊的内容,好像没什么特殊的东西吧。但是其实白皮书挺深的,如果过一段时间,你读一次白皮书,发现自己读出了新东西,那说明对比特币的认知是有进步的。对比特币的白皮书,如果缺乏深入的理解的话,是非常制约在比特币上做开发,尤其是长期的工程化的实践了。</p> <p>前段时间我的好朋友蒋杰,闲聊的时候他问我,白皮书里有啥内容,你还记得不?我回想了一下,然后说了一些我觉得比较重要的点,他听完了以后,神秘一笑。但说实在的,其实我挺慌的。因为我发现,我并没有自己以为的那么理解白皮书的内容,更别说抽丝剥茧,理解背后的脉络了。</p> <p>(这里我们现场互动了一下,听了下几位观众对白皮书的理解)</p> <p>非常感谢两位同学的发言。那么我们来看白皮书最先引入,也是最先被讨论的概念,某种意义上对比特币系统来说最根本的概念,就是 <strong>交易</strong>。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/04.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/04_hu02c012e39554d2eb537df98570255277_30348_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/04_hu02c012e39554d2eb537df98570255277_30348_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a04" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>对,你没看错,既不是区块,也不是链,更不是矿工,挖矿,激励&hellip;而是<strong>交易</strong>。</p> <p>你就会问了,为什么是交易? 白皮书为什么上来就讨论交易?</p> <p>原因很简单,因为 <strong>交易,以及对交易的处理,就是比特币的核心目的</strong>。</p> <p>在那之后描述的链状签名只是一种实现机制, Alice 签了以后给 Bob, Bob 签了以后再给 Carl,依次往后传递&hellip; 也就是说,链状的签名只是它的一种软件实现。</p> <p>白皮书从交易谈起,说明它是符合 <strong>“第一性原理”</strong> 的。关于第一性原理,我来举个例子吧。</p> <p>什么是第一性原理?</p> <p>比如说马斯克造火箭对吧,他是什么思路,他一上来不是要改良现有的系统,不是说看现在的火箭是怎么造的,我怎么能优化一下,把哪个地方做得更好。</p> <p>他思考的第一步是什么?是组成火箭的原料有哪些,这个材料是否合理,是否可回收,这些都是非常根本的问题,决定了马斯克的火箭跟传统火箭有质的不同。按照这个思路,把问题一项一项往前推,倒推到根上去,从头开始分析这个问题,所以才能做到自底向上,从最小的碎片拼凑起来,最终形成的,是一个我们认为是突破常规的这样一个方案。</p> <p>这就是所谓第一性原理。就是你思考问题,不受已有框架的束缚,回到这个系统想要解决的根本问题上来,从根本出发,然后再去一步步构造你的系统。所以大家才会看到白皮书,觉得惊为天人,竟然构造了一个前人完全没想过的系统。它深刻地反映了第一性原理,从最根本的事物,这个系统要解决的问题,也就是交易本身出发。这是值得大家留意的。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/05.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/05_hu59772c7bdcdf46cb565283729fe7c58f_24941_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/05_hu59772c7bdcdf46cb565283729fe7c58f_24941_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a05" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,说完了作为核心的交易,我们来看第二层,也就是白皮书里提到的,或者说他引入的第二个被重点阐述的概念,是<strong>时间</strong>。</p> <p>那么 <strong>“时间”</strong> 对比特币系统为什么这么重要?</p> <p>这是因为 <strong>时间的自然流逝</strong>,才是比特币系统的内在的本质。利用这个时间的 <strong>不可逆</strong> 的特性,我们才有了区块链这样一个链状的数据结构。区块链每 10 分钟出一个块,最后形成一个链。它为什么很自然的形成一个链呢?因为它就是时间的表现。</p> <p>区块链是怎么来的?</p> <p>你沿着头部的创世区块往后看,就能发现,这个系统整体上是时间从物理世界到在数字世界的一种模拟,这个链条就是时间本身。</p> <p>其实就是<strong>无穷的,海量的,没有终结的交易序列,随着时间的流逝,逐渐凝结到一个接一个区块里</strong>,这样一个过程。而这个凝结的过程就是由所谓的 POW 来保证的。</p> <p>如果你明白这个时间对这个系统的意义的话,你忽然就能明白,为啥不仅仅是区块是按照时间顺序往后排的,而且忽然也能理解为什么我们要求区块里的交易也一定要是按照时间顺序依次排列。</p> <p>好,到这里,从白皮书第一个引入的概念 “交易”,我们往外推了一层,讲解了第二层 “时间”。</p> <p>大家注意,“交易”和“时间”这两个概念,已经可以把比特币这个系统给撑起来了。后面的若干层也是系统的重要组成部分,但是他们不是系统核心概念。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/06.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/06_hua0ca4db97e6d133a48b9f17b78ffbf5a_19342_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/06_hua0ca4db97e6d133a48b9f17b78ffbf5a_19342_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a06" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来我们看白皮书的第三层,这是一个 P2P 的节点网络。</p> <p>那么 P2P 节点网络实际上已经被讨论得非常多了,因为大家关心矿工的行为,怎么去挖矿,获得收益,被系统设计的激励机制,然后怎么样让自己的算力更多,然后跟其他人竞争,以及在竞争当中为了保持收益,或者说扩大收益,他们要诚实,累积自己的信誉,等等。</p> <p>那么,这一部分内容是在说什么呢? P2P 网络也好,激励系统也好,诚实也好,是想表达什么意思呢?</p> <p>白皮书是在告诉你,这个系统的<strong>可增长性</strong>。它是通过设计了这样一个 P2P 的网络,以及附着其上的激励机制,来确保网络本身有足够的可成长性。不仅仅是在过去的初期,到了现在的中期,未来的长期,以及 2140 年甚至更久远的以后,系统以 P2P 网络的形式保持增长。</p> <p>白皮书对矿工网络的基本行为和激励机制都做了一定的描述,但是我们知道,不管是后来的矿工专门硬件也好,所谓小世界也好,都是系统不断演化的结果,未必是一开始完全规定好的,以后也必然会继续演化。</p> <p>比如说,系统激励如果降低的比较快,跟咱们预期的不一样,手续费没跟上,怎么办?</p> <p>如果以后人类能星际航行,行星之间的延时,怎么办?</p> <p>如果需要考虑,到时候戴森球上太阳挖矿,那怎么办呢?</p> <p>诸如此类的发展带来的问题,就需要在发展中解决,而系统作为一个整体,也得以在一次次解决问题的过程中一步步发展。</p> <p>这就是比特币这个系统里白皮书描述的第三层——P2P 节点网络以及所谓的激励机制。</p> <p>他们虽然重要,是系统的重要组成部分,但不是系统的核心概念,是系统关于增长性的一种设计和实现。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/07.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/07_hu137aa77f33acf9b92ae923ecc0cdd5be_21366_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/07_hu137aa77f33acf9b92ae923ecc0cdd5be_21366_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a07" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好了,说完了白皮书的前三层,我们进入第四层。大家可以留意,我讲述的顺序是按照白皮书的原始讲述顺序。</p> <p>白皮书讲了由内及外的三层了之后,它的第四层是什么?</p> <p>是关于系统的<strong>可持续性</strong>的,也就是通过 “效率” 和 “优化” 来保证系统的可持续发展。</p> <p>这个可持续性,主要有三个方面:</p> <ol> <li>可以对它做磁盘空间的优化,丢弃掉一些东西。</li> <li>可以通过 SPV 来保证,即使交易量非常大了,也可以用很简单的硬件来验证,来确保交易的有效性。</li> <li>怎么去管理 utxo,也就是拆分 utxo 和合并 utxo。</li> </ol> <p>前不久还有一个优化是说把大量小的 utxo 合并起来,可以不收矿工费,这都是优化的一个体现。关于 utxo 结构可以多说两句,因为这个结构太精巧了。平常别人说区块链有独创性,实际上 utxo 结构才是真正精妙的设计,我们可以说对 utxo 的管理,才是比特币技术的非常令人惊叹的部分。很多后来的公链抛弃了 utxo 这个结构,具体怎么样我们也不评价,我们只能说,比特币的原始设计是非常优雅的。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/08.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/08_hu79cf6b9b302ed037d98a310f4384e323_23449_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/08_hu79cf6b9b302ed037d98a310f4384e323_23449_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a08" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>前面从内到外讲了四个层次之后,白皮书的最后,也就是最外的这一环,说到了用户体验,具体涉及两个方面,一个是隐私,一个是信任。</p> <p>隐私这块,通过刚才 aaron 讲的每次换地址,公私钥转换,等等,通过一些机制来确保隐私是友好的。对普通人来说,有很好的私密性,不用太担心自己付款被别人知道。当然想知道的时候,你也可以向别人证明自己交过这笔钱。</p> <p>而信任这块,就回到了白皮书一开始的摘要开头就说明的,设计这个系统,就是为了能够去掉信任第三方,可以降低所有交易的成本,这对用户体验是非常好的。</p> <p>我们每个人,作为个体,在面对非常大的信任实体的时候,是非常无力的,我们没有那么多的时间和资源去跟他们对抗。中本聪设计出来这样一个系统,使得我们作为一个普通的个体,有机会跟所有的庞然大物,平等地,无许可地,无法篡改地交易,这是一个非常重要的事情。某种意义上,这使得比特币系统从一个孤立的系统,成为一个建设更美好的人类社会的重要工具。</p> <p>在白皮书里,隐私和信任放在最后一部分讲,它是系统最外部的第五环,是关于用户体验的。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/09.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/09_huae92fa41c1a4aeeedaaba3b52222cc5d_44777_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/09_huae92fa41c1a4aeeedaaba3b52222cc5d_44777_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a09" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,梳理完白皮书的结构,现在我们放到一块,整体上看一下。</p> <p>白皮书就是分成这五个层次,从内向外,展示了整个系统的全貌。它深入浅出地在很短的篇幅内塞入了大量的信息,而且还说得异常清楚。这里,我们整体上梳理一下。</p> <p>首先它谈到了比特币系统的目的。从交易根本出发,是使用链状的签名来实现的。</p> <p>然后,白皮书描述了怎么实现这个东西,就是利用了时间的不可逆性,把交易源源不断的凝结到了区块里,这个过程由 POW 来保证。</p> <p>第三环,怎么样保证这个系统能持续增长,对应的方案是这样一个 P2P 的网络,对网络里边的积极的,诚实的节点,进行了有效的激励,能够让他们获得更多的收益,并为了获取更高的收益愿意去改进系统,这是一个成功的长期有效的激励。</p> <p>后面更外的两层,白皮书讲到了,怎么去优化系统,保证它即使在超大的规模下,甚至是亿万人使用的情况下,比特币系统仍然能够安全有效的工作;以及对用户的影响,也就是用户体验。</p> <p>好,这是我对白皮书的梳理,希望对各位开发者能有所启发。</p> <h3 id="其他协议和工具">其他协议和工具</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/10.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/10_huf3418541bc27352417e35894aafb4691_29230_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/10_huf3418541bc27352417e35894aafb4691_29230_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a10" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,现在我们开始白皮书之后的内容。</p> <p>协议协议这块,我列举了两类协议,第一类,是我曾在去年的 BSV 的线上研讨会里讲过的一篇应用层协议,那里对所有的应用层协议做了一个梳理。大家如果想知道更多的细节,可以去翻出去年的视频看,如果觉得视频一个小时太耽误时间,可以看我总结的一张脑图,大概 10 秒钟就可以看完,里边把所有应用层协议都列了。应用层协议非常重要,但因为时间有限,在这里我就不展开了。大家可以看到这里列了 B、C、D、BCAT 还有一些其他的协议。都是关于数据的存储和索引的。</p> <p>另外一类协议就是 metanet 相关的协议,这也非常重要。如果希望在比特币上做开发,metanet 是必须必须深入研究和考察的内容。其实坦率讲,我自己一开始也没有太重视这个东西,但是后来我发现它非常有用,它是一种粘合剂。你在 bitcoin 上设计的系统,写的合约也好,做的支付也好,当你发现它不能充分的表达你的意图的时候,就需要 metanet 来帮助你,帮助你修补这个系统的一些缺失结构,把它们填满,成为一个更好的产品。</p> <p>举个例子吧,我们设计的 token,狙击大作战的 OVTS,在现在还没有浏览器的情况下,我们怎么知道某一笔转账里这个余额是谁的呢?这一笔是谁的,是谁转给谁的,其实是看不出来的,因为典型的比特币交易有一个进来地址,出去的地址,你一看就知道这个是自己的,对吧,收到多少钱,一看就知道是自己的。但如果这些信息全部在合约里,需要你打开比特币脚本浏览器,自己把合约粘进去,然后找到数据段,然后往前数若干个偏移,然后再去把相关的信息提取出来,这个是非常痛苦的,一般人也是不可能去做这个。</p> <p>但用 metanet 就很容易解决这个问题,你每次一笔交易发生的时候,你就输出一个 metanet 的节点,就完事了。</p> <p>metanet 的节点里边你还可以存其他各种信息。就像你去买咖啡时人家给你的小条一样,你可以存上你任何想要的信息,他们都是一一对应的,而且 metanet 还会自动帮你维护版本。多么完美。这只是一个很小的例子。包括刚才 aaron 讲的视频流,不断付费,张三给李四打工,你可以想象一下,本身这个系统是无法自说明的,大家在这里那里签名提交,其实是个黑盒,其实从外面根本看不出来是发生了什么。但是如果你有个 metanet 节点,就可以更新它的状态,把这些状态同步更新到 metanet 的子节点里,对所有的人都是立刻链上可见的信息,这是非常友好的。</p> <p>所以,可能 metanet 的应用空间,远远比我原先预想的要广阔的多,之后我可能会再花更多的篇幅去深入讲这个问题,为了不耽误时间,我继续往后讲。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/11.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/11_hu9e4c9e4bc5eed693b9b088e9b7ab8a9e_20053_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/11_hu9e4c9e4bc5eed693b9b088e9b7ab8a9e_20053_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a11" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这是两个入门的库,去年我也有讲,我推荐刚刚开始学习比特币的同学,从这两个库入手,可以做一些基本的操作,比如说创建私钥,然后给某个地址发交易。为什么用这两个库?因为他们有代码,如果不懂的话,你可以往里跟,然后一笔交易发出去有没有成功,到底是谁失败了?它里边的加密算法究竟是怎么做的,你都可以一步一步看出来,这样的话你一边做,一边就把比特币的系统从内到外的都了解了。</p> <p>而且他们有一个特点是非常有弹性,如果你不想了解的时候,你用他给你提供了很恰当的默认值,你不需要做什么事,就可以用最小的代价完成你要的功能。</p> <p>所以我推荐大部分没有接触过比特币的同学,可以从这两个库入手。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/12.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/12_hu05ec45d9d924a8481017fddd4db75179_16737_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/12_hu05ec45d9d924a8481017fddd4db75179_16737_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a12" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>当你更深入的时候,想希望了解比特币上合约的开发,推荐你了解一下 scrypt,看看他能做些什么。因为这次训练营我们有一天是 scrypt 的专场,这里我就不展开了,留给 scrypt 的同学,介绍他们的产品应该怎样使用。</p> <h3 id="服务">服务</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/13.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/13_hu6ca95fb52a7961db8894fd46563cdeab_21447_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/13_hu6ca95fb52a7961db8894fd46563cdeab_21447_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a13" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>服务这块我列了三个,一个是旺仔的 MetaSV。刚才 aaron 也有讲,MetaSV 提供的一般的功能我就不细说了,就是查地址,查区块,查交易,通过 API 查各种你想查的区块链上的信息。我这里说一下 MetaSV 新发的一个功能叫 xpub 的监控。如果你有一个 HD 的钱包,可以把 HD 的 xpub 拿出来,利用 MetaSV 查看跟它关联的路径下的所有地址的余额。为什么 xpub 监控以及它对应的云钱包这么重要,之前据我所知好像没有人把它真正做好过,这个功能因为它非常难,他的难度比我们想的更难,需要你监控很多地址,而且这些地址可能是杂乱无章的,可能是跳跃性的,这个系统做到90分是没问题的,但是要想真正做好这个系统,要做到 95 分甚至 98 分,是要消耗巨量的资源来检查并做大量的优化。</p> <p>具体的我不再展开,如果再展开今天讲不完了。大家读一下旺仔在知乎上的文章,就对 xpub 监控更有了解,功能推荐大家试一下。还有 whatsonchain 还没讲,whatsonchain 为什么我们用它?因为有时候旺仔在更新系统用不了,就用 whatsonchain 来替代一下。其实我还用了另外一个服务叫 blockchair。 blockchair 跟 whatsonchain 的不同在于,whatsonchain 当前的这笔交易的输出,你是不知道他有没有被花的,他被谁花了你也不知道,在他网页上看不出来啥东西,而 blockchair 上你可以前后追索,还是非常方便的,所以我推荐大家什么都去了解一下,你会发现有些地方你被卡住了,然后在另一个工具里其实很简单,可能就点点鼠标就完成了。</p> <p>最后一个API,是我们蒋杰同学实现的 Sensible API。它是专门针对于 Sensible 上面的这个 FT 也好, NFT 也好,还有其他日后我们做的所有的扩展,都可以用这个 API 来快速的进行查询。 如果应用支持Sensible 的话,可以直接调这个API 为你的服务提供支持,不用再重新自己去区块里交易里去分析,能省很多事。</p> <h3 id="框架">框架</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/14.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/14_huf8b7c5eefd161f30f73f7c98dbdd7ffa_21883_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/14_huf8b7c5eefd161f30f73f7c98dbdd7ffa_21883_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a14" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是更大一点的框架,我列了三个框架,是我在开发当中觉得比较好的三样东西。</p> <p>runonbitcoin 是一个在跑在比特币上的二层的 JavaScript 虚拟机,实际上方便了很多不太想去接触一层脚本开发的同学,因为总有这样的需求。我们不是说所有事情都需要在矿工共识矿工这一层共识来完成,一定有那么一些有意思的东西是通过典型的 JavaScript 脚本,甚至是,我之前说的 Lua 的 OperateBSV 那样在二层来实现的,甚至我自己还想搞一个 Python 的,因为这是实际开发者会面对的需求,你想如果你有一个 Python 的虚拟机跑在比特币上,你现有的 Python 代码不需要做太大的改动,就能运行在比特币上,这是一件很酷的事情,对吧?</p> <p>然后第二个是 MetaID。MetaID 在周三的时候会有一整天的时间讲,这里我也不展开了。然后 Sensible Contract 我也不展开了,因为周四的时候大家会深入了解 Sensible Contract 也就是感应合约的相关信息。</p> <p>他们这三个的共同点,就是他们不是为某一个特殊的应用场景设计的,他们是为一个更通用的目标,也就是帮助大家做各种不同类型的 APP 来服务的,是框架性的,相对通用的,三个我认为值得大家了解的东西。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/15.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/15_hu1ad4c77c524632aac2ce6e12593c0941_22083_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/15_hu1ad4c77c524632aac2ce6e12593c0941_22083_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a15" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是理论这块,如果你从开发上升一层,希望了解更深入的比特币内在机制,也就是这么做背后的原因。咱们刚刚说白皮书里不是有5层,他是这样设计没错,但是他为什么要这么设计呢?那么如果你想弄清楚的话,你可以看一下 <a class="link" href="https://craigwright.net/" target="_blank" rel="noopener" >Craig 的 blog</a> 和 Youtube 上搜 Theory of bitcoin,就可以看到比特币背后的理论性的知识。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/16.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/16_hu61ce0dc506ee52cad592e754a7ef6b19_22350_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/16_hu61ce0dc506ee52cad592e754a7ef6b19_22350_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a16" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,左边的5列就是常规的介绍,已经说完了。在右边我挑了三个技术,同大家分享一下,对一个单独的技术点,我提取了哪些信息,希望这些信息能够帮助到你们。先来讲 nChain 的技术。</p> <h2 id="专门技术">专门技术</h2> <h3 id="nchain-tech">nChain Tech</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/17.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/17_hud1984d0f2c09e14a98614132d0c6fbf6_28756_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/17_hud1984d0f2c09e14a98614132d0c6fbf6_28756_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a17" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>其实 nChain 的技术刚刚 aaron 已经讲了很多了,把我想基本上讲的差不多了。那就以我的方式复述一下。第一个 MAPI 我认为它最重要的两点,一个就是可以去矿工那里查费用,你可以去矿工那里查实时的真正的现在网络上能够被接受的费用,你甚至可以一直查,有时矿工会高一些,有时候矿工会低一些,甚至有时候矿工明明说自己只接受 0.5 的,但它实际上接受 0.2 甚至更低。有时他说自己能接受 0.2 的,但实际上只能接受0.5的或者更高,这都是有可能发生的。这是它的第一个功能,去查网络上的真实费率。</p> <p>第二个功能是交易提交,交易提交功能分为两种,一个是单个交易提交,一个是批量交易提交。我个人觉得,从矿工这里,如果你批量交易提交的话会更管用一些,因为大家都可以在上面做优化。但是我们其实没没有太用好这个功能,我们自己也需要研究,怎么样去把它给用的更好,因为我们经常有时候还发现一些奇怪的情况,这就不细说了,这里面有些坑,感兴趣的同学可以私下找我聊。</p> <p>接下来是 SPV。SPV 是对一笔交易的快速验证,你不需要下载所有区块链上的信息,就能快速对它验证,也是对你想要花的那笔钱的快速验证。</p> <p>它分为两个方面,一个是人家给你付了人家付的钱,你付钱的那笔交易你能验证,第二个是人家想付的那笔钱,确切的说,那一笔 utxo 是否来自于一个有效的交易,这笔交易可以快速在区块链上验证。这个场景对应的是刚才说到 bip270 里,人家来咖啡馆买一杯咖啡,你拿到了一笔人家签名的交易,凭什么就是相信对方给的是有效的 utxo,就会去拿着它的 utxo 找到它的前序交易,用 SPV 验证一下,看看这个交易是不是有效的,如果它的前序交易是有效的,我们就认为它交易本身是有效的,所以 SPV 还可以用在这个地方,是一体两面的,一个是交易前的,一个是交易后的。</p> <p>SPV 很好,大家都知道很好,它节省大量资源,但是它也有不好的地方,这里我提一下。SPV 不好的地方在于它需要区块的支持,需要区块支持的意思就是只有入块了才有默克尔根,你才能去做 SPV 验证。比如说刚才那个场景,你需要检查一个 utxo 是否有效,那么找到它来源的交易还没入块,就只能继续往前追索,假设这个人就是来要搞破坏,他搞无穷多个子孙交易,然后对你来说其实是非常大的困扰。这是 SPV 目前的一个限制,它只能对已经入块产生了默克尔根的东西来验证它的有效性。</p> <p>好,这是 SPV 我提到的它的两个特点和一个不足,当然不足可能也是一个特性,需要区块支持。</p> <p>那么第三个是 nakasendo 的门限签名,相关的技术可以了解一下,如果不了解,你可能不会知道他可能会用在哪些地方。你了解他之后,就会发现好像自己需要这么个东西,就是这样一个神奇的东西。它跟metanet一样,你不了解你会觉得它就是个偏门技术,了解了就会发现它的通用性比你想的更大,可能用在很多地方,这里就不展开说了。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/18.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/18_hu030c52ad5ac6a335f82f5631b21e0c05_46409_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/18_hu030c52ad5ac6a335f82f5631b21e0c05_46409_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a18" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这是去年10月份的时候,我分享时的一页 PPT 上我写了 SPV Channels,也就是所谓 SPV 通道,是依托于矿工网络的高效 P2P 通信,这个信息是不准确的。我讲完以后,那个礼拜,老刘找到我说,不对,我看了SPV Channels 不是基于矿工网络的,它是一个中心化的服务器。</p> <p>这里非常抱歉,所以在这里我给大家专门注明一下,我当时提供了一个错误的信息,非常抱歉。我给大家跟老刘鞠一个躬,非常感谢老刘指出问题。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/19.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/19_hue894cf04e8c9206c8dcccbb3fff00381_39159_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/19_hue894cf04e8c9206c8dcccbb3fff00381_39159_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a19" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>真实的 SPV Channels 是这样的,你可以看到,下面的几个特性仍然保留,但是最重要的,我当时认为是非常重要的,“通过矿工网络来传播” 这个特性,其实是没有的,它是一个非常典型的,中心化的消息业务服务。可以这么说,它就是一个像QQ聊天服务器一样的东西。本质上,我们说它就是一个中心化的东西。这是一个修正。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/20.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/20_hu7d5fc6ba4d96e02546ae1122d2f92452_22129_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/20_hu7d5fc6ba4d96e02546ae1122d2f92452_22129_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a20" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>但是,此处有反转。</p> <p>昨天晚上跟哲明大哥交流了一下,哲明大哥告诉我,TouchStone,这个是打点钱包的一个开源项目。</p> <p>这个项目里提供了我想要的东西,真正依托于矿工网络的P2P的点对点的加密通信,这个事情是可以做成非常大的一件事情,可以被用于很多方面。</p> <p>因为我也是刚了解到这一点,只是听说,还没动手试试,所以我也不敢说太多,以免下次又要鞠躬。但是我推荐大家了解一下。</p> <h3 id="xoken-tech">Xoken Tech</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/21.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/21_huf95769abee27d11e3f94139c1f1b83e1_22269_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/21_huf95769abee27d11e3f94139c1f1b83e1_22269_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a21" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好。说完 nChain 的这一部分,我来讲一下 xoken。xoken 是我偶然间了解到的,为什么我会对他产生兴趣,因为我们都知道 nChain 有一个非常厉害的项目叫 TeraNode,但因为释放出来的信息很少,我们其实并不知道 TeraNode 的实现细节,但是我们可以通过跟 TeraNode 竞争的另外一个,也就是非官方的超大区块处理项目,来侧面的了解他们做了些什么。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/22.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/22_hu8aa1cb7300bd3116783fc2aa28e52762_26131_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/22_hu8aa1cb7300bd3116783fc2aa28e52762_26131_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a22" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>据我所知,这两个东西,提供的最核心的价值,就是大规模的分布式的并行的交易处理,也就是说把对交易的处理给彻底的打散了,又不像我们比特币的原始的处理方式是在一个系统内部处理,而是把它分散成为一个更现代化的系统,不在一个实例里,不好意思,我终于想到那个词了,不在一个 instance 里面处理,而是分散到多个服务,多个物理服务,多个逻辑服务上去做真正的现代意义上的比特币服务了,这是我认为他们带来的最大意义。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/23.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/23_hu05517c55607ad3e66032b4dfbbc0ee31_61716_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/23_hu05517c55607ad3e66032b4dfbbc0ee31_61716_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a23" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>Xoken 的核心技术是 TMT。Transpose Merkle Tree。它是啥意思呢?它是把默克尔树的中间的缓存的节点,默克尔树中间不是有很多层,从底下交易往上一层一层验证,最后到默克尔根,他把中间的这些缓存节点通过转置,使得你从叶节点到根节点的遍历直接就包含了相关的默克尔证明。所有的原始交易,和最后的默克尔根,仍然保持在他们原来的位置上不变,但是中间已经预处理了一遍了。</p> <p>那么通过对默克尔树的中间节点做预处理,并且存到一个 Graph DB 里边去,它有什么好处呢?它能降低你处理 T 级区块的内存占用,按照他们的广告词,据说树莓派也能处理 T 级区块了,当然它可能处理得很慢,但是它能处理,因为它内存的需求降下来了,可以变成一个流式的处理。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/24.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/24_hub5ce4bec619d9792c13602d870211e00_37953_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/24_hub5ce4bec619d9792c13602d870211e00_37953_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a24" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这是它的一个组件,是刚刚讲的 NEXA。可以看到我列了几个它的特性,比如说它的 Grape DB 是天生适合用来存 TMT 的。然后他为什么用了 Haskell,因为这个东西的惰性求值,使得它对流式处理非常友好。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/25.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/25_huad93c12cf8ff4468252e83d024c864c1_74136_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/25_huad93c12cf8ff4468252e83d024c864c1_74136_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a25" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是 VEGA,这是它的一个比较核心的内部技术,刚才说的 NEXA 是它的外部技术,而 VEGA 是它的内部技术,它做了完全的分布式的交易处理。下面的英文就不细说了,但是从上面我提取的关键词,可以知道它是针对比特币在大规模的交易处理下的一些优化。其中他提到几个点,当发生重组的时候,就是 re-org 的时候,它能够瞬间切换。然后他有一个分片技术又有广告嫌疑了,sharding done right,号称是实现了正确的分片。在比特币系统上,针对比特币的结构来做的分片,还有刚说的 TMT,都是值得一看的技术。大家感兴趣的话,可以自己去他的官网上看,它提供的资料有限,但是它是开源的,但是代码是 Haskell 的。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/26.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/26_hu7c5255f19df5c629cac2ffcf8f41d6f5_29180_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/26_hu7c5255f19df5c629cac2ffcf8f41d6f5_29180_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a26" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,那么回到 Xoken,为什么我关心这个事情?</p> <p>因为它是我所见到的第一个,就是真正的针对大规模交易处理的解决方案。它就是典型的从 “第一性原理” 出发,我就去想怎么处理交易本身,怎样才能有效处理超大规模的交易,别的不管。我管你是用什么实现的,你是用什么默克尔,或者是什么其他数据结构,我不管,怎么样能大量地快速处理比特币交易,才是我唯一关心的。这是从第一性出发来解决问题的,这是一个很好的案例。</p> <p>你看,他就呼应了我们之前说的白皮书。交易,才是比特币的核心目的,不要被其他的干扰了。它的物理存储也好,它是拿什么存的,拓扑结构也好,它是怎么实现的,用了什么数据结构,用了哪个语言,这都不重要,这些全都不重要。</p> <h3 id="sensible-tech">Sensible Tech</h3> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/28.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/28_hu3722dab2b15b064a14e60123a6a80a8a_22542_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/28_hu3722dab2b15b064a14e60123a6a80a8a_22542_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a28" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,最后我来讲一下感应合约。因为周四我们会讲感应合约内部的东西,这里我只是简单讲一下 Sensible 的特点,即使你对它的内部实现不感兴趣,你可能对它表现出来的一些外部特性会感兴趣,方便你了解一下。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/29.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/29_hud38a134eb6f30635b9604ab712b5a0ef_59064_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/29_hud38a134eb6f30635b9604ab712b5a0ef_59064_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a29" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>总得说来,感应合约有4个小特点,第一个特点是,完整的交易合约的逻辑封装在里面。它外部依赖的签名器是没有状态的,它的状态都是靠 contract 来传递的。没有,也不需要第三方的认证,实际上是有别于目前已有的其他这些方案的。</p> <p>第二个特点,full featured contract data payload。就是说他通过 contract 可以来携带需要的信息,不需要去你再去其他地方拼凑信息了,这些信息可以验证,不需要用 op-return 来做这件事。当然你可以用 op-return 来辅助它来做一些事情,但是这不是必须的。其实技术上讲不是这样,我这里说的 op-return 是指一个独立的 op-return 输出。</p> <p>第三点,它是完全去中心化的,是由矿工来验证,不需要一个鉴权者,也不需要一个验证者。</p> <p>第四点,它是使用比特币的特性来实现的,我们叫它 &ldquo;bitcoinic&rdquo;,就像 python 的方式我们叫 pythonic 那样,它是跟 metanet 能够非常良好的匹配的。而且它是跟比特币一样支持 SPV 的。应该这么说,它没有比比特币做得更多,也没有比特币做得更少,它只是跟比特币保持一样。</p> <p>好,这是它的4个小特点,更多的特点,如果你感兴趣的话,周四可以听我们讲,或者是看官网上的介绍都可以。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/30.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/30_hub299774afb1b5b5da4ce06a15a7ef8e3_50080_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/30_hub299774afb1b5b5da4ce06a15a7ef8e3_50080_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a30" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>获得上面这四个好处,要付出两点代价。第一个是,需要一个外部的极小化的签名器,来对一些重要的字段做签名,这是一个外部服务。</p> <p>虽然这个服务非常小,可以做到任何地方,包括钱包,或者是其他任何地方,但是确实需要这样一个东西。</p> <p>以前我们曾说,是不是能够通过改 PreImage 来去掉这个东西,因为如果 PreImage 把这个字段加上了,就不需要签名器再去生成了。</p> <p>但是修改 PreImage,这个是牵涉面非常广,我们也并不是说打算靠嘴推动,天天把精力都花在怎么游说别人这件事上,我们更希望它这个系统本身,不管你升不升级,它都能很好的工作。之前说要修改 PreImage,造成了很大的误解。因为我被问到的最多的问题,就是,听说你们这个玩意要改协议,大动静,要把别人房子拆了,回答了几次了之后,我也不知道该怎么回应才是有效。那么今天我可以明确地说一下,实际上我们不会去提改 PreImage 这个方案,简单说我们就暂时不考虑这个选项。</p> <p>这是它第一个代价,需要一个外部的小的签名器。</p> <p>第二个代价就是,由于它的逻辑是在合约中实现的,导致它比那些需要在外部去维护一个 utxo 集,或者说维护其他的状态和做一些其他事情,比如说鉴权,校验等等外部功能,它的脚本尺寸要更大一些,为什么?这个很好理解,因为我们的关键业务逻辑是在脚本里做的,而别人是在链外做的,那么代码尺寸自然会有一些区别。</p> <p>那么具体大到什么程度呢?其实也不是很大,因为我们一开始没意识到尺寸问题,就写得有点奔放,但是后来经过优化,我们的一个 FT 的输出,现在是 8k。然后我们狙击大作战前两天上了新的合约版,在中午12点的时候会给大家结算,这些结算单笔交易会产生 100 个输出,每个输出 8k 然后加起来就是 800k,听起来其实也还好,毕竟一次性处理一百个人 0.8 兆。</p> <p>我们还会继续优化,我们也知道 scrypt 实际上是有很多空间是可以优化的。我们一起努力,把尺寸变得更小。小到什么程度,小到你把逻辑放在合约里,你也不会觉得有什么心理负担,要小到这个程度才行。</p> <p>好,这是感应合约的两个不足。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/31.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/31_hu6270bcfc241ec2b56a2380ca80a95cbe_41739_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/31_hu6270bcfc241ec2b56a2380ca80a95cbe_41739_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a31" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这些 BCP 是我们基于感应合约开发的一些,我们认为可以直接对标以太坊上的一些协议。</p> <p>这样做的话,大家都能省力一些。每个相关的参与方,都可以直接用这个来实现自己要的功能,同样的接口,同样的操作,能极大地降低成本。这个事情我们还在推进之中,之后还会加入更多的 BCP,但是我们加这个东西要谨慎,要维护一个良好的,正交的,解耦合的状态,不能说是到最后成了一团乱麻,然后 A 引用 B,B 引用 C,到最后自己也分不清。最好是每一个都是彼此独立的,正交的,每一个都能独自发挥有效的作用。</p> <p>从官方的宣传材料里得到的信息,也就是右边列出来的部分,这种对智能合约的叙事是我认为说的比较准确的,&ldquo;automation of agreements with easily definable transaction step&rdquo;。是什么意思呢?就是说,合约的本质,是一种关于契约或者说约定的自动化,是怎么自动化的呢?通过比较容易被定义的多个交易步骤,连起来说就是 “通过 (容易被定义的) 多个交易步骤来实现的契约自动化”。BCP 是符合这个定义的。我们希望把一些双方达成的协议,形成一个固定格式的交易模板。你看到的左边的 BCP 123 等就是模板。之后还会有更多的模板,就好像去豆丁网上搜 “某某某合同模板” 一样的。我们会形成越来越多这样的模板,这样的话以后就不用自己,从头开始写一个合同,直接拿模板,把合同金额和银行改一下就完事了。当然了可能要审查一下。</p> <p>好,这是我们认为的 BCP 的目标是什么,也就是给大家提供尽可能方便的可以直接用的合同模板。</p> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/32.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/32_hucd1fe5ff7a16f701cdbd610371734e60_28192_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/32_hucd1fe5ff7a16f701cdbd610371734e60_28192_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="a32" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,这差不多是我今天的全部内容。还有一些没有列出来的技术,其实也是非常有价值的,我们希望这个是一个系列,以后会有一个迭代。</p> <h2 id="take-away">Take Away</h2> <p><img src="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/2021-04-bsv-dev-overview-2.png" width="1746" height="804" srcset="https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/2021-04-bsv-dev-overview-2_hu48e0f3ef6993f3ced1b4ed697a5a1eb9_146061_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2021-04-25-bsv-tech-and-tool-v2/images/2021-04-bsv-dev-overview-2_hu48e0f3ef6993f3ced1b4ed697a5a1eb9_146061_1024x0_resize_box_3.png 1024w" loading="lazy" alt="a2021-04-bsv-dev-overview-2" class="gallery-image" data-flex-grow="217" data-flex-basis="521px" ></p> <p>最后还有一页 Take Away 这一页不用细看,是我刚刚讲的所有的内容的一个回顾,一个快速查阅的脑图,可以当作弊条看一眼。</p> <p>好,今天的分享就到这里,谢谢大家。</p> <h2 id="问答环节">问答环节</h2> <p>(主持人) 好,不知道大家有多少人看过顾露的 1.0 版本的演讲,在我们 B 站上,稍后我们同事会发到群里面去,然后看到顾露真的是说是程序员 2.0 就是 2.0,1.0 里的很多内容都没有讲,2.0 这个里面基本上都是全新的内容。我们离午饭还有一点点的时间,如果大家有问题的话请举手。</p> <h3 id="sensible-与以太坊的合约对比">Sensible 与以太坊的合约对比</h3> <blockquote> <p>刚才您提到 Sensible 的合约有一些代价。对,Sensible 合约跟以太坊上的合约的差异还有优势在哪里?</p> </blockquote> <p>这是一个大问题。简单说它 (Sensible Contract) 试图使得比特币的脚本拥有更多的能力,不是瞄着以太坊去的。之前跟别人解释过,从本质上讲,以太坊的计算模型跟比特币是有差异的。以太坊是一个全球中心的计算机,所有的机器都是计算机的一部分,而比特币的原始设计,还是第一性原则,它是处理交易的,它根本不是用来提供什么计算的,计算只是它提供的一个附加的组件。所以比特币的设计是分散的,utxo 是分散的,每一个 utxo 理论上跟其他 utxo 不应该有太多的关联,这跟以太网是完全两个思路。(打比方的话) 一个是单核单进程,一个是微服务多进程。</p> <blockquote> <p>对,我其实以前也做过那种并发处理,其实对于我们这些对理论不是那么了解的人,可能他更多想知道的就是在哪一些应用可以把它比特币化,比如说以太坊上一些应用可以把它比特币化,或者说哪些又不能比特币化,这个是我们很想知道的一个。</p> </blockquote> <p>对,这个问题非常好。这个问题我谈一下我自己的理解,可能是非常初级的。</p> <p>哪些应用能被比特币化?</p> <p>为什么我们会有这个问题?是因为我们比任何人都希望以太坊上的应用在比特币上,不仅能做,而且能做得更好。</p> <p>但是我想说的是,在以太坊上大量的已有的应用,他们经历过大量的迭代。这些迭代不仅是有针对用户体验的,针对功能的,更多的有一部分迭代,目标是使它更匹配以太坊计算模型。</p> <p>你现在只有单核,你就尽量想好怎么样用单核的特性,用好单核,这就会导致你的程序更难被多线程化,这很容易理解,对吧。</p> <p>这会导致一个趋势——在以太坊上越成熟越完备,越适应以太坊的东西,反而越难以被移植到基于 utxo 的系统。不是说它不可能,当然可能了,也可以做到,但是会发现,对于一个非常适配以太坊模型的应用,把它迁移到 utxo 上,会花费比你针对 utxo 去设计一个 utxo 友好的这样一个系统,要花费更多的心力。当以后 scrypt 足够成熟 Sensible 也足够成熟的时候,也许我们会提供一个东西让你相对无痛的转过来,但是这种无痛的转换,背后是有代价的。</p> <p>比如说那天 Angus 就问了一个这个问题,swap 从以太坊上转过来会有问题。人家以太网上你只管给他交易发,然后它自动就线性处理了。到了比特币上了以后,有一个 utxo 专门来维护所有的状态,岂不是大家都要指着这个 utxo,你用完了给我,我用完给他,这个东西谁能保证有那么高的实时性,如果成千上万人参加的话,大家一顿猛提,那整个系统不就各种卡死,这就是一个很重要的,这是一个非常好的问题。</p> <p>然后我当时给他回复,还是之前老刘的观点,就是说,比特币它的目的,不是在于比你专门设计一个系统要表现的更好,它不是这样,他的目标是给你提供更多的选择。</p> <p>比如说你可以由开发这个功能的主体,由他去管理 utxo 我就是做 swap 的人,你们的 swap 需求都提给我,我来负责,哪怕再多的人,10万人,我也会把它 stream 到一个 tx 序列里面去,我来把这个事情处理的很好,这是一种方案了。</p> <p>但是这种方案你会说,你这个中心化,都是你来搞,你不搞怎么办?人家以太坊上都是去中心化的,你会有这样的疑问。</p> <p>那么这种情况怎么办呢?我们可以退一步,不要由我作为一个主体,我来操心所有的细节。我挂了所有人都挂了,而是交给像旺仔这样的通用数据,大家都去他那儿获取有效的 utxo 获取到了你就 swap 成功,获取不到你就 swap 不成功,获取到过时了的,那是你自己获取的问题,自己想办法解决就好。</p> <p>这样的话,你就可以通过多个数据访问商来实现这个功能,实现了一点点的去中心化,但是牺牲了一点点实时性,因为所有的交易都来你这儿的话,你可以对它做很好的优化,因为大家都从你这儿过,你有这个队列你可以对它做很多优化,但是使用一个通用的服务,你就只能只依赖通用的服务了。当然了,后来我又补充了一句,我说你依赖通用服务也没关系,因为你依赖这样的通用服务其实是还好的,因为旺仔会做大量的优化,确保它的DB和它的区块链状态是高度同步的。你不用太担心 utxo不够及时,因为他 (旺仔) 的优化搞不好比你自己优化的好,因为人家就是就是干这个的。他能保证他的 DB 跟区块链能够尽可能小的延迟。</p> <p>所以你其实只牺牲了一点点延迟,换来了一点点去中心化,你也可以牺牲更多的延时,然后换来更多的去中心化,在比特币上你永远有选择。这是老刘给我的最大的启发,就是在比特币上你永远有这样的选择。</p> <p>在去中心化和高效处理之间,按照你自己的需求,定制出来一个平衡的方案。</p> <p>这就是我想说的,就是你可以选择从以太坊上迁移,但是这取决于你愿意怎么去做 (trade-off)。</p> 2021.02 《实用性阅读指南》 https://gulu-dev.com/post/2021-02-19-practical-reading/ Fri, 19 Feb 2021 00:00:00 +0000 https://gulu-dev.com/post/2021-02-19-practical-reading/ <p>读 《实用性阅读指南》 的简单笔记。</p> <p><img src="https://gulu-dev.com/2021-02-19-practical-reading/cover.jpg" loading="lazy" alt="cover" ></p> <p>本书是实用性阅读的入门材料。</p> <p>由日语翻译而来的书,相对亲切和质朴,适合小朋友阅读。</p> <p>阅读本书,是为小聪私塾准备材料,稍晚些时候为小朋友简单梳理和讲解,如何做实用性的阅读。</p> <p><img src="https://gulu-dev.com/2021-02-19-practical-reading/mindmap.png" loading="lazy" alt="mindmap" ></p> 2020.11 抓住机会 https://gulu-dev.com/post/2020-11-14-grasp-the-opportunities/ Sat, 14 Nov 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-11-14-grasp-the-opportunities/ <p>注:本文以彩云小译的输出为主。费解之处请<a class="link" href="https://fabiensanglard.net/silicone/index.html" target="_blank" rel="noopener" >参考原文</a>。</p> <p>本文与 2014.03 发在 blog 上的 <a class="link" href="https://gulu-dev.com/post/2014-03-22-dont-lie" target="_blank" rel="noopener" >不要说谎 (Don&rsquo;t Lie.)</a> 正好组成了一组文章,相互呼应。</p> <p>“不要说谎” 一文主要的着眼点在于,<strong>项目开发中的历次迭代(在任何时候)都不应打破产品的质量标准。</strong> 而本文则指出,那些无法维持标准的产品会 <strong>因为退化</strong> 而给新的产品带来机会。</p> <h2 id="这些就是所谓的机会-these-are-called-opportunities">这些就是所谓的机会 (These are called opportunities)</h2> <p>昨天苹果公司更新了他们的三款机器。在发布会上,据说 MacBook Air、 MacBook Pro 和 Mac Mini 将采用自研的 M1 芯片。这些“One more thing&hellip;”事件通常伴随着夸张的数字,这一次并没有偏离传统。</p> <ul> <li>高达3.9倍的视频处理速度。</li> <li>高达7.1倍的图像处理速度。</li> <li>高达3.5倍更快的 CPU 性能。</li> <li>每瓦CPU效能功耗比提升至3倍。</li> <li>高达2倍的图形处理器性能。</li> <li>更快的机器学习性能高达15倍。</li> <li>高达2倍速度的固态硬盘。</li> <li>速度超过 98% 的 PC 笔记本电脑。</li> </ul> <p>如果觉得最后一个说法难以置信 <sup id="a1"><a class="link" href="#f1" >1</a></sup>,Geekbench 展示了一款 ARM MacBook Air 2020,这让英特尔 MacBook Pro 2019在单核性能上相形见绌 <sup id="a2"><a class="link" href="#f2" >2</a></sup>。可以肯定的是,这些新的笔记本电脑提供了很大的性能提升。</p> <h2 id="前进一步后退两步">前进一步,后退两步</h2> <p>结构更迭和性能提升确实让人印象深刻,不过这里也许有个情况。</p> <p>在接下来的几个月里,那些购买 M1 机器的人将享受到强大的响应能力和迅速的启动时间。一些曾经臃肿的应用程序将再次像大多数工具那样运行。但是很快这些指标就会开始下降。响应能力和启动时间将逐渐恢复到以前的状态,老的“非 m1”机器将变得比以前更慢。</p> <p>对于硬件工程师节省的每个周期,软件工程师将增加两条指令 <sup id="a3"><a class="link" href="#f3" >3</a></sup>。</p> <p>我对这个问题最清晰的记忆是在2008年,当时我用 ssd 替换了我的 hdd。它改变了我的生活,所以我写了这篇文章 <sup id="a4"><a class="link" href="#f4" >4</a></sup>,这样读我博客的五个人也可以改变他们的生活。 Photoshop 在一秒钟内就开始了。可以立即启动 XCode。那真是太美妙了。</p> <p>10年后,一台有着 M.2 NVMe 和 Ryzen 5 2600 的机器需要13秒来启动 Photoshop。我也不再使用 XCode 了。</p> <h2 id="有人在乎吗">有人在乎吗?</h2> <p>做更多的事情需要更多的资源,这很正常。现代的射线跟踪器需要更大的处理能力,来产生更好的图像。同样,2020年的编译器拥有令人敬畏的静态代码分析器,这是他们在2009年没有的。</p> <p>但是做同样的事情永远不应该变慢。启动一个应用程序不应该比以前花费更长的时间。如果一个特性要花费启动时间,我宁愿不要它。</p> <p>是因为我们不在乎启动时间,还是因为我们别无选择?</p> <h2 id="我昨天就想要">我昨天就想要</h2> <p>早在2008年,当我亲眼目睹第二次浏览器大战时,火狐从网景公司的灰烬中崛起,对 Internet Explorer 进行报复,谷歌发布了一款新的浏览器。</p> <p>火狐有很多很棒的功能。它有选项卡、调试工具和常规的 bug 修复等大量有用的功能。 Chrome 也有这些功能。但是当我看到它几乎立即启动,而不是像火狐那样花上5秒钟(才启动)的时候,我立刻换了它,再也没有回头。</p> <p>正如后来谷歌工程师解释的那样,速度是核心优先考虑的事情。</p> <blockquote> <p>我们使用自动化测试来仔细监视启动性能,该测试几乎对代码的每个更改都运行。这个测试是在项目很早的时候创建的,当时谷歌 Chrome 几乎什么功能都还没开始做。我们一直遵循一个非常简单的规则:</p> <p>这个测试的指标不能退化。</p> <p>因为在出现性能问题时立刻处理它们,比拖到以后再修复它们要容易得多,所以我们可以很快地修复或回滚任何的(有问题的)改动。因此,我们这个(已经变得)非常大的应用程序,现在启动的速度和我们开始时使用的非常轻量级的应用程序一样快。</p> <p>&ndash; Brett Wilson,Chrome 软件工程师 <sup id="a5"><a class="link" href="#f5" >5</a></sup></p> </blockquote> <p>一旦行动迟缓,就很难走出泥潭,让人们重新审视自己。众所周知,Firefox 开发者在2010年试图通过 bug # 627591 修复(长达5秒的)启动时间问题 <sup id="a6"><a class="link" href="#f6" >6</a></sup>。他们可能解决了许多问题,但这也不再能唤起我第二次尝试的勇气了。</p> <h2 id="机会">机会</h2> <p>有了像苹果这样更好的硬件,创造者们大多能得到更好的工具。一些(不那么好的)公司的工程质量会降低,或者不再进行这类 “确保系统不会退化的测试” ,但即使这样也是一件好事。因为对有些人来说,这些缺陷有另一个名字。这些就是 <strong>所谓的机会</strong> (These are called opportunities)。</p> <h2 id="参考文献">参考文献</h2> <ul> <li><b id="f1">1. </b> <a class="link" href="https://www.pcworld.com/article/3596814/no-the-new-macbook-air-is-not-faster-than-98-of-pc-laptops.html" target="_blank" rel="noopener" >No, the new MacBook Air is not faster than 98% of PC laptops</a> <a class="link" href="#a1" >↩</a></li> <li><b id="f2">2. </b> <a class="link" href="https://browser.geekbench.com/v5/cpu/compare/4651583?baseline=4651916" target="_blank" rel="noopener" >MacBook Pro (16-inch Late 2019) vs MacBookAir10,1</a> <a class="link" href="#a2" >↩</a></li> <li><b id="f3">3. </b> <a class="link" href="https://en.wikipedia.org/wiki/Andy_and_Bill%27s_law" target="_blank" rel="noopener" >Andy and Bill&rsquo;s law</a> <a class="link" href="#a3" >↩</a></li> <li><b id="f4">4. </b> <a class="link" href="https://fabiensanglard.net/ssd/" target="_blank" rel="noopener" >SSD Reboot your thing</a> <a class="link" href="#a4" >↩</a></li> <li><b id="f5">5. </b> <a class="link" href="https://blog.chromium.org/2008/10/io-in-google-chrome.html" target="_blank" rel="noopener" >I/O in Google Chrome</a> <a class="link" href="#a5" >↩</a></li> <li><b id="f6">6. </b> <a class="link" href="https://bugzilla.mozilla.org/show_bug.cgi?id=627591" target="_blank" rel="noopener" >Bug: preload dlls on windows</a> <a class="link" href="#a6" >↩</a></li> </ul> 2020.11 《乔丹传奇》 (段旭) 阅读笔记 https://gulu-dev.com/post/2020-11-06-michael-jordan/ Fri, 06 Nov 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-11-06-michael-jordan/ <img src="proxy.php?url=https://gulu-dev.com/post/2020-11-06-michael-jordan/jordan-min.jpg" alt="Featured image of post 2020.11 《乔丹传奇》 (段旭) 阅读笔记" /><p>前两天读了段旭在 2013 年写的《乔丹传奇》。我很少读传记,偶然翻到这本书,一气读完,非常精彩,摘录了一些有趣的句子,也做了全文的梳理。</p> <p><img src="https://gulu-dev.com/2020-11-06-michael-jordan/jordan-min.jpg" loading="lazy" alt="《乔丹传奇》 " ></p> <hr> <h2 id="一些逸事">一些逸事</h2> <h3 id="乔丹时刻--对球队的强烈感染力和驱动力">“乔丹时刻” —— 对球队的强烈感染力和驱动力</h3> <p>资深电台主播约翰尼·科尔(Johnny Kerr)回忆说,“他们有机会在主场解决战斗,却让机会溜走了。他们不相信自己会是一支在主场三战两败的球队,这一点都不像他们。”</p> <p>在这弥漫着死亡气息的空间里,忽然一道阳光照射进来——乔丹登机了。</p> <p>乔丹戴着一副墨镜,头上的帽子很花哨,身上的运动衬衫更花哨,嘴里还叼着一根巨大的雪茄。这个样子,不像是去鏖战,倒像是参加庆功派对。他无比招摇地跟大伙儿打了个招呼:“哈喽,世界冠军们,让我们去菲尼克斯收拾他们吧!”</p> <p>他嘴里的雪茄没有点燃,有人问:“这雪茄是干嘛的?”乔丹回答:“这是我的胜利雪茄。”言下之意,庆祝夺冠用的。</p> <p>飞机上的气氛瞬间变了,不再死气沉沉。乔丹的穿着、言语、举动,像给战友们注射了一管快速见效的兴奋剂。每个人都得到暗示:这一趟我们是去拿冠军的,我们到底在郁闷些什么?</p> <p>只有球队内部人员见到这一幕。直到多年以后,它仍是公牛老板莱恩斯多夫最喜爱的“乔丹时刻”,是他珍藏在内心深处的美好记忆。乔丹那与生俱来的领袖气质和人格魅力,旁人想学是学不来的。</p> <h3 id="乔丹的幽默">乔丹的幽默</h3> <p>上半场打完,美国队领先23分,奈特仍然决定拿乔丹开刀,以免下半场发生任何意外。“该死,迈克尔,”奈特吼道,“你什么时候才开始做掩护呢?你就只抢篮板和得分!”</p> <p>乔丹乐了:“教练,我不是在哪儿看到你说,我是你带过的最快的球员吗?”</p> <p>“是啊,”奈特说,“但跟这有什么关系?”</p> <p>乔丹回答:“教练,我做掩护了,只是速度快到你没看见。”</p> <h3 id="乔丹与皮蓬的关系">乔丹与皮蓬的关系</h3> <p>乔丹与皮蓬的关系完全不同。乔丹看得到皮蓬的天赋,也知道皮蓬欠缺很多他自己当初在北卡篮球队享受到的优质调教,于是,乔丹要磨练皮蓬的基本功,还要把NBA所需的坚韧精神品质灌输到皮蓬的头脑中、血液里。皮蓬越是认真学,乔丹就越是乐意教。这个过程需要时间,因为在很长一段时间里,乔丹对于皮蓬骨子里的韧性很是怀疑。他俩是队友,后来是最好的搭档,却并不是真正的朋友。两人在家庭出身、成长经历、接受教育等方面存在着巨大的差异,乔丹对自己人生的各方面都有极大的自信,而皮蓬在某些方面却非常自卑,阿肯色州贫困的生活环境深深影响着他的人格。</p> <h3 id="早期跟记者相处得不错">早期跟记者相处得不错</h3> <p>乔丹早期同跟队记者的关系其实相当不错。对记者们来说,他容易接近,态度友好。乔丹理解,和媒体打交道是他工作的一部分,而且他意识到,跟记者们私下闲聊,可以加强自己对联盟其他球队的了解,包括听到一些八卦传闻,比如谁谁谁跟队友关系不好,谁谁谁不服教练管教,诸如此类。乔丹从记者那儿便利地获知信息,再给记者提供一些自己的生活细节——他明白这个道理:要从别人那里得到信息,最好的办法,就是拿一些自己的信息出来交换。</p> <h3 id="难以被觉察的力量">难以被觉察的力量</h3> <p>乔丹看上去很瘦,所以很少有人意识到他有多强壮。鲍勃·奈特已经发现了这点,他告诉每一个人,乔丹的许多技能,其实就源自那难以被人察觉的力量。“你们没看到他的力量,是因为他外表看上去没那么强壮,不是那种野兽般的身体,”奈特说,“但是,他有力量。当他背身打你,你是防守人,他把手看似无力地放到你膝盖上,你就会像被铁钳夹住一样动弹不得。”到这个时候,乔丹还从未做过任何力量练习。</p> <h3 id="关于自己的得分能力">关于自己的得分能力</h3> <p>82场比赛,从第一次跳球到最后一声哨响,我一直在攻击。这就是我的心态。就身体天赋而言,那支球队是我在公牛待过的所有赛季中最差的一支。赛季开始时,我们的先发阵容是史蒂夫·科尔特(Steve Colter)打组织后卫,厄尔·丘尔顿(Earl Cureton)和查尔斯·奥克利(Charles Oakley)打前锋,格兰维尔·维特斯(Granville Waiters)打中锋。我知道,如果我们要成功,我就需要多得分。我敢肯定道格·科林斯也是那么认为的。</p> <p>我曾连续9场得分上40。你不知道一晚上拿40分需要耗费多少能量。一整个赛季,场均拿32分,跟场均拿37分多一点,差别是很大的。这么想吧:如果我某晚拿了32分,那我必须在下一晚得42分才能追平。</p> <h3 id="关于-the-shot-绝杀一投">关于 “The shot” 绝杀一投</h3> <p>那不是普通的一投。至少,那次投篮命中,为乔丹后来无数次执行绝杀,奠定了坚实的心理基础。有一回被问到为何投关键球总能保持镇定,乔丹就说:“我猜或许第一次你会紧张,可一旦你成功得越来越多,你就会越来越自信。</p> <h3 id="关于-北卡体系">关于 “北卡体系”</h3> <p>迪恩·史密斯那一套,会压抑球员个性和个人天赋的成长吗?作为北卡球员的楷模,沃西有着完全不同的表述方式,他说:北卡的体系,不是设计用来约束球员的天赋和运动能力,而是设计用来降低风险;球始终在移动,目的是给每个人创造合适的投篮机会,</p> <h3 id="关于-三角进攻">关于 “三角进攻”</h3> <p>杰克逊喜欢把三角进攻描述成“五人太极”,其基本原理是:通过球员在场上不断地移动,引诱对方防守失去平衡,从而创造出大量的机会。之所以得名“三角进攻”,是因为它最基本的形态就是边线三角。</p> <h3 id="他们-太阳队-知道怎样去拼但他们不知道怎样去赢">“他们 (太阳队) 知道怎样去拼,但他们不知道怎样去赢。”</h3> <p>乔丹在自传中说:“1993年总决赛跟菲尼克斯和查尔斯·巴克利交手,就像在跟你的小弟弟交手,你知道自己装备精良。七次里头,弟弟可能会打败你一两次,但你知道他最终肯定会输。太阳队不知道如何去赢——他们知道怎样去拼,但他们不知道怎样去赢。这是有差别的。”</p> <h3 id="年轻的科比在场上向乔丹讨教打球">年轻的科比在场上向乔丹讨教打球</h3> <p>乔丹觉得有趣的是,湖人队自始至终不愿意包夹他,所以他终究得到了充足的单打机会,拿到了充足的分数,公牛队也赢下了比赛。更让乔丹惊讶的是,第四节,年轻的科比竟然在防守时向他讨教起来。“他问起我的背身单打动作,是这么说的,‘你两腿之间是迈开一点,还是收紧一点’。这有点让我震惊。”乔丹说,“当他这么问我的时候,我感觉像个老头儿。我告诉他,在进攻端,你始终要感知、要清楚防守球员在哪儿。背身单打转身跳投的时候,我总是用我的腿来感知防守在哪儿,我好根据防守做出反应。”</p> <h3 id="在乔丹身边皮蓬就能成为联盟第二好的球员">在乔丹身边,皮蓬就能成为联盟第二好的球员</h3> <p>伯德这样写道:“我相信在迈克尔·乔丹身边有全联盟第二好的球员,斯科蒂·皮蓬。你把迈克尔从那支球队拿掉,斯科蒂将落到第五位,但只要迈克尔跟他一同出战,他俩就是全联盟最好的两名球员。”</p> <hr> <h2 id="结构图">结构图</h2> <p><img src="https://gulu-dev.com/post/2020-11-06-michael-jordan/jordan-mmap.png" width="1875" height="618" srcset="https://gulu-dev.com/post/2020-11-06-michael-jordan/jordan-mmap_hub46c1743490dd696591f53c468d86048_145058_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2020-11-06-michael-jordan/jordan-mmap_hub46c1743490dd696591f53c468d86048_145058_1024x0_resize_box_3.png 1024w" loading="lazy" alt="《乔丹传奇》 全书梳理" class="gallery-image" data-flex-grow="303" data-flex-basis="728px" ></p> 2020.10 金山区块链交流 https://gulu-dev.com/post/2020-10-19-kingsoft-visit/ Mon, 19 Oct 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-10-19-kingsoft-visit/ <img src="proxy.php?url=https://gulu-dev.com/post/2020-10-19-kingsoft-visit/title.png" alt="Featured image of post 2020.10 金山区块链交流" /><ul> <li><strong>2022-05-16 补记</strong> <ul> <li>这是 2020 时在金山的一次临时安排的访问和关于区块链的技术交流。</li> </ul> </li> </ul> <h3 id="材料分享">材料分享</h3> <ul> <li><a class="link" href="2020-kingsoft-visit.pdf" >分享全文 PDF 链接</a></li> </ul> <h3 id="内容">内容</h3> <ul> <li>比特币和区块链的核心概念</li> <li>区块链作为一个有机的系统整体是如何工作的</li> <li>从工程师角度看,bitcoin 和 git 本质上的相似之处</li> <li>bitcoin 的分叉历史和开发资料</li> <li>SatoPlay 在 2020 时的产品线简介,业务模型,解决方案等等</li> </ul> 2020.10 Blog Migration to Hugo https://gulu-dev.com/post/2020-10-04-blog-migration-to-hugo/ Sun, 04 Oct 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-10-04-blog-migration-to-hugo/ <p>Finally I started migrating my personal blog <code>gulu-dev.com</code> to <code>hugo</code>, away from <code>Bitcron</code>, which I&rsquo;m afraid is probably no longer a great choice for blog hosting, after writing 107 posts there for 6 years (2014-2020).</p> <h3 id="more-freedom-less-stucking">More freedom, less stucking</h3> <p>I have already experimented with <code>hugo</code> for a while and have created a new blog this week, and will migrate all stuff from Bitcron in the near future.</p> <p>There are several reasons behind the decision:</p> <ul> <li><code>gitee.com</code> is pretty much faster than <code>Bitcron/FarBox</code> within China</li> <li><code>hugo</code> is much more customizable than an automatic engine</li> <li>git-based-repository holds revision history intrinsically, and probably preserves more reliability and integrity than Dropbox file syncing</li> </ul> <h3 id="no-more-504-gateway-time-out">No More &lsquo;504 Gateway Time-out&rsquo;</h3> <p><img src="https://gulu-dev.com/post/2020-10-04-blog-migration-to-hugo/504.png" width="865" height="272" srcset="https://gulu-dev.com/post/2020-10-04-blog-migration-to-hugo/504_hucb2f0a72a6f13c9ed452d904422cef30_11038_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2020-10-04-blog-migration-to-hugo/504_hucb2f0a72a6f13c9ed452d904422cef30_11038_1024x0_resize_box_3.png 1024w" loading="lazy" alt="the intermittent 504 from Bitcron" class="gallery-image" data-flex-grow="318" data-flex-basis="763px" ></p> <p>The last thing kicked me away from Bitcron/Farbox is, <strong>intermittent 504</strong>, which is said &ldquo;to be fixed when there is a fix&rdquo;.</p> <p>This is terrible.</p> <p>I&rsquo;m afraid Haipo no longer has much enthusiasm in this project. Though I&rsquo;m willing to pay a bit more than I did for a stable service, it probably doesn&rsquo;t yield enough revenue to keep it self worth maintaining any more.</p> <h3 id="the-migration-period">The migration period</h3> <ul> <li>There might be <strong>a few months ahead</strong> to complete the migration process.</li> <li>I will post <strong>on both sides</strong> before everything is settled here.</li> </ul> <h3 id="202101-情况更新">(2021.01) 情况更新</h3> <p>去年 10 月,<a class="link" href="https://gulu-dev.com/post/2020-10-04-migration-to-gitee/" target="_blank" rel="noopener" >我开始将我的 blog 从 Bitcron 迁移到一个更合适的地方</a>。现在技术上的迁移已经完成,老的文章日后有机会再逐渐同步过来。</p> <h4 id="访问方式">访问方式</h4> <ul> <li>新的 Blog 仍然通过这里访问:<a class="link" href="https://gulu-dev.com" target="_blank" rel="noopener" >https://gulu-dev.com</a></li> <li>底层是 Hugo 生成的静态页面,方便定制,速度和稳定性也较以前大为提高。</li> </ul> <h3 id="202109-再次更新">(2021.09) 再次更新</h3> <ul> <li>我开始使用 Netlify 托管整个网站的部署和访问(自动化程度大大提高)</li> </ul> 2020.10 BSV 线上研讨会:BSV 应用层协议 https://gulu-dev.com/post/2020-10-04-app-layer-protocol/ Sun, 04 Oct 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-10-04-app-layer-protocol/ <p>两个月前,比特币协会 (Bitcoin Association) 与 nChain 联合举办了一次线上研讨会 (此 Webinar 全系列共 8 节讲座,链接见文末),针对 BSV 的一些技术做了系统而深入的讲解。</p> <h3 id="研讨会概述---整体主题梳理">研讨会概述 - 整体主题梳理</h3> <p>这次研讨会的内容全面而丰富,我简单按照主题列一下:</p> <ol> <li><strong>节点技术</strong> - Bitcoin SV 全节点相关的技术,包括安装,配置,基础架构,性能测试 (第 1/6 节)</li> <li><strong>协议架构</strong> - Bitcoin SV 分层网络,应用层协议,及商户接口 MAPI (第 2/3/4 节)</li> <li><strong>可证公平</strong> - Bitcoin SV 可证公平游戏 (第 5 节)</li> <li><strong>门限签名</strong> - Bitcoin SV 上的门限签名技术,及相应的 Nakasendo SDK (第 7/8 节)</li> </ol> <p>很荣幸,我有机会成为讲师中的一员,参与了这 8 节课程中 &ldquo;Bitcoin SV 应用层协议&rdquo;(链接见文末) 这一节课程的讲解。</p> <h3 id="应用层协议---内容梳理">应用层协议 - 内容梳理</h3> <p>下面是讲稿的全部内容,共 26000 余字,略读全文需要 20-30 分钟。</p> <p>由于全文较长,我将其整理为下面三个小节,并为每部分内容划分了小节,并添加了小节标题。</p> <ol> <li><strong>协议栈的架构模型</strong> (内容包括 Bitcoin SV 应用层协议栈与 TCP/IP 协议栈之间的模型与架构对比,及其演化及竞争的关系)</li> <li><strong>应用层协议与实践</strong> (汇总了 BSV 生态内活跃的绝大部分应用层协议,及对应的开发工具,以及对开发者而言,有价值的原则,技巧与实践)</li> <li><strong>课后问答和释疑</strong> (如何对不同协议做选择和取舍,协议的演化,等等,及我向 Jack Davies 请教时,Jack 对部分问题的进一步阐释)</li> </ol> <h3 id="正文-一-整体介绍及大纲">正文 (一) 整体介绍及大纲</h3> <p>(演讲正文由此处开始)</p> <p>大家好,欢迎大家参与由比特币协会和 nChain 联合主办的 Bitcoin SV 系列线上研讨会,本活动于每周二及周四晚上 8:00~9:00 进行,为期一个月。我是顾露,是比元科技的创始人,也是 Bitcoin SV 上的应用 <a class="link" href="https://satoplay.com" target="_blank" rel="noopener" >小聪游戏</a> 的开发者。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/1.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/1_hu921212d6468968d9f05999b036762cfc_83357_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/1_hu921212d6468968d9f05999b036762cfc_83357_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="1.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>本期讲座的主题是 <code>**Bitcoin SV 应用层协议**</code>,内容包括了 <strong>协议栈的架构模型</strong> 和 <strong>应用层协议与工具概览</strong> ,也就是说包含了理论和实践这两个部分,主要面向对 “Bitcoin SV 应用层协议特性”,以及 “在 Bitcoin SV 上开发应用程序” 感兴趣的听众,也欢迎其他的开发者。</p> <p>大家在听讲座的过程中,如果有任何问题可以在评论区提出,助教老师会进行问题收集,我也会在讲座结束后统一进行解答。最后希望各位保持课堂的秩序,在接下来的一个小时里有所收获。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/2.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/2_hu73840253f1b7d07dc9edd23e4b4925a6_67046_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/2_hu73840253f1b7d07dc9edd23e4b4925a6_67046_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="2.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,这里我们展开说一下今天的内容框架。</p> <p>在这里可以看到我们刚刚说的两个部分,一个是协议栈的架构模型,另一个是协议和工具概览。</p> <p>那么在架构模型这一块,我们会对比应用层协议栈跟传统互联网的 TCP/IP 协议栈,做一个他们之间的模型和架构的对比,以及他们演化和竞争的关系。在协议和工具这块,我们会重点介绍各种协议 (protocols) 主要分为数据协议、支付协议和token协议,我们会逐一地讲到它们各自的特点。最后会介绍一下当前有哪些工具可供使用。</p> <p>那么对开发者来说,我们这一讲既有协议架构的理论的知识,也有很多开发者他们针对自己的需求,定制各自的协议,及相应的探索实践,这样整体上来讲,可以作为开发应用时的一个比较全面的参考。</p> <h3 id="正文-二-协议栈的架构模型">正文 (二) 协议栈的架构模型</h3> <p>好,我们现在先一起来看一下传统的互联网抽象模型。目前已有的抽象模型有两个,一个是 OSI 模型,一个是 TCP/IP 模型。</p> <h4 id="1-传统互联网抽象模型-tcpip">1. 传统互联网抽象模型 (TCP/IP)</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/3.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/3_hu0cffbbdf52a0b86bd3331ca1ab6a73a4_79707_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/3_hu0cffbbdf52a0b86bd3331ca1ab6a73a4_79707_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="3.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这两个模型我简单介绍一下,OSI 是上世纪70年代互联网形成早期的时候,定义的一个模型,这个模型相对比较繁琐,从最上面的应用层到最下面的硬件设施一共有7层,那么反过来 TCP/IP 协议就相对实用一些,他把 OSI 的7层协议简化成了4层,在右边就可以看到,我们看右边的图它是比较有特点的,从最上面的就是应用程序所在的层开始,那一层里面应用程序的数据,实际上只是他自己的数据,然后为了被传输,加了一个 UDP header,再往下一层为了 IP的寻址和路由,又加了 IP header,到最后硬件层,为了让它在设备之间可靠的传输,又加了数据帧的帧头,叫 frame header。</p> <p>那么我们的疑问是:bitcoin 的模型是什么样的?在传统的互联网抽象模型里边,bitcoin 应该在什么位置?</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/4.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/4_hu99e1dbe0b4fae4b7537f49da4856ada1_69981_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/4_hu99e1dbe0b4fae4b7537f49da4856ada1_69981_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="4.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,在回答刚才那个问题之前,我们先简单的把 TCP/IP 模型里面,每一层的上下文介绍一下,那么后续理解 bitcoin 网络模型的时候,就更方便一些。</p> <p>我们从下往上一层一层地看,首先是物理层,这一层是网络的基础设施,数据链路层,主要是开放网络上的硬件设备以及这些设备之间的物理连接。然后到了网络层,这一层提供的功能是寻址和路由,也就是帮助不同的参与者找到自己的目标地址,他们所在的位置。网络层是IP协议实现的,我们知道现在互联网上 IPv4 的地址非常紧张,正在普及 IPv6,这个是大的背景。</p> <p>那么在这个之上是传输层。这一层为整个互联网提供了健壮可靠的消息传输,是基于TCP协议实现的。TCP协议通过损失了一部分效率,来保证消息传输的健壮和可靠。如果你想避免或者说减轻这一部分效率损失,可以用 UDP 协议。对大多数的互联网应用来说,TCP协议是他们构建其他协议的基础,比如说我们访问网站,浏览网页需要用到 HTTP/HTTPS,这些协议就工作在 TCP 上。</p> <p>在最上面的应用层,这一层是直接面向用户的,通过无数的这种针对特定的具体的情况下面的协议,就可以帮助用户去实现各种各样的数据交换,来实现千差万别的数据服务。我们常见的云存储、视频直播、网络游戏、移动支付,这些都是应用层的服务,所以你看这个层,其实它比其他的层占的面积都大,这是应用层它丰富和复杂性的体现。</p> <p>那么我们简单介绍了一下这4层网络模型,给之后我们理解 bitcoin 的模型,提供一个对比和参照。</p> <h4 id="2-tcpip-与-osi-的对比">2. TCP/IP 与 OSI 的对比</h4> <p>好,在这之前我们先简单对比一下 TCP/IP 和 OSI。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/5.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/5_hu69ca313c2d149bf4d947c1e18d778b3e_94422_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/5_hu69ca313c2d149bf4d947c1e18d778b3e_94422_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="5.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>可以看到左边的应用层,在 OSI 里面,实际上是应用层,表示层和会话层,共三层。</p> <p>这里我展开介绍一下,(右边第一项) 应用层里面,是各种应用层的协议,什么 HTTP、FTP 这些协议在 OSI 里也是属于应用层。 (右边第二项) 那表示层是什么?它实际上是各种不同的数据格式,比如说图片有 JPG、PNG 这种的,然后不同的算法,不同的加密方式,其实也是表示层的一部分,因为所有的这些数据,实际最终看起来是什么样子,实际上就是一种 presentation,是一种表示和呈现。 (右边第三项) Session 会话层。 会话层是对应到主机的进程里面,是本地主机跟远程的主机他们之间进行的会话,以及与会话相关的维护性工作。</p> <p>那么除了应用层两者不一样之外,还有底下的物理层 (也不一样)。 在 TCP/IP 协议里面,底下的物理层在 OSI 模型里,实际上被拆成了两层,对应起来说,物理层实际上是跟物理设备直接相关的,是建立维护断开物理连接用的。数据链路是建立逻辑连接,去处理数据帧用的。(这两者) 一个是物理性的,一个是逻辑性的,总的来说就是它们的差别其实并不大。</p> <h4 id="3-metanet-模型-整体演化">3. MetaNet 模型 (整体演化)</h4> <p>好,那么讲了传统的模型了之后,我们来看一下 metanet 的抽象模型。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/6.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/6_hu75cca36d2d96833740360d85e2449476_63986_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/6_hu75cca36d2d96833740360d85e2449476_63986_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="6.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>它需要支持各种不同类型的数据,就像传统互联网一样,有非常丰富的数据(处理和呈现能力)。而且我们会去讲解为什么会设计成这样,他是怎么工作的,从内到外去理解这个系统。</p> <p>大家可以看到左边的图,为什么选左边这张图?它实际上是 bitcoin 的层次化网络,也就是所谓的 BLN (Bitcoin Layered Network) 这个层次化网络,因为时间关系,在这一讲里是不会展开,但是随着 Bitcoin SV 的发展,我们可以去观察和验证,看看真实世界里网络的发展会不会像图上这样去演化。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/7.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/7_hu1908b160df2700f66bb8835f75a67fbd_63676_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/7_hu1908b160df2700f66bb8835f75a67fbd_63676_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="7.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,在开始讲之前我先问一下,作为开发者来讲,你觉得 bitcoin 应该是在哪一层?</p> <p>有几个选项:物理层、网络层、传输层、应用层和“都不是”。</p> <p>我在这里停留一点时间,大家可以思考一下。</p> <p>这是一个很有趣的问题,因为这个问题实际上是开放的,目前来说没有一个严格意义上确定性的标准答案。</p> <p>接下来我们会解释一下,到目前为止 Bitcoin SV 的实现情况,以及未来在这个基础上,我们期望去实现的理想的情况。这样应该就能说明白, bitcoin 是怎么从根本机制上,去结合并驱动整个系统,在不同层面上去创新的。</p> <p>好,我们先回到传统的 TCP/IP 模型,从这里开始。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/8.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/8_hu4739d32aca862d3ed177391ce9112d0a_47399_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/8_hu4739d32aca862d3ed177391ce9112d0a_47399_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="8.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>按照我们目前的情况来看,bitcoin 的具体位置是在应用层和传输层之间。</p> <p>这是因为,往下看,bitcoin 需要通过下面的传输层 TCP 来实现数据的传递。TCP 协议是非常稳定的协议。往上看,越来越多的应用程序,开始利用 bitcoin 来提供服务。 bitcoin 所在的位置,目前来说实际上是一个 <strong>承上启下的结合部的位置</strong>。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/9.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/9_hu55258fde6a4781f0a39e0fb213257d67_62972_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/9_hu55258fde6a4781f0a39e0fb213257d67_62972_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="9.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>从目前的角度来看,bitcoin 也的确是互联网上的一个应用,这是目前真实情况的反映。确切的说,在应用层里面,bitcoin 是属于偏底层的一个基础性的服务性质的应用。</p> <p>但是如果仅仅把 bitcoin 和 metanet 看成是应用层的子集,从协议栈的角度来讲,它意义就是非常有限的了。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/10.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/10_hu98b4c2b02a014706951c3ad8bfc6a2f0_57283_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/10_hu98b4c2b02a014706951c3ad8bfc6a2f0_57283_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="10.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这里我们拿微信来打个比方,微信本来只是 App Store 里一个非常单纯的社交应用,但是因为它涉及面越来越广,后来就有了微信小程序,微信小游戏,慢慢的你会发现 App Store 可以被它蚕食,甚至是替代了。微信,从一个应用,慢慢地变成了一个操作系统,甚至以后有可能会变成一个终端。那么 bitcoin 也有可能会经历这样的过程。</p> <p>接下来5~10分钟里,我们会探讨一下 bitcoin 是如何影响,并且延伸到不同的层次,更进一步,在将来,整个 metanet, 也就是元网,这个模型是怎么样演化的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/11.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/11_hu0e16a2cb5112ac2612c011c33117675f_62709_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/11_hu0e16a2cb5112ac2612c011c33117675f_62709_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="11.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>如果我们把 MetaNet 和 TCP 放到一块,对比来看的话,我们认为,bitcoin 会开始逐步渗透,透过传输层和网络层,直接到达下面的物理层。总的来说,这是由 bitcoin 作为一个经济系统,强大的经济激励模型来决定的。在TCP之外,bitcoin 不仅可以发展出独特的替代性的方案,而且对物理层也会有相当程度的影响。</p> <p>举一个目前已经在发生的例子,10年前的中本聪,也只是推断网络里面会出现服务器集群,作为大型的专有设施来挖矿,但是后来很快就出现了专属的矿机,矿池,矿场,这些是跟算力直接相关的 POW 硬件,是很典型的基础设施。</p> <p>那么物理层这个基础设施,作为现实网络的承载层,很大程度上可以被传统互联网和元网共享。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/12.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/12_huee451a91d1fa4c6284757195ab52226e_60939_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/12_huee451a91d1fa4c6284757195ab52226e_60939_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="12.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>其实互联网一开始就是这样的,在90年代互联网发展的初期,那个时候互联网大量的复用了已有的电话网络。不知道大家还记不记得,我们那时候都是通过 modem 来拨号上网的。我记得那个时候上网,要是有电话拨进来,网络还会断线,网络被占用的时候,有时候还拨不进来。互联网其实是借助了当时已经普及的电话网络,走进了千家万户。我想如果 bitcoin 发展起来的话,元网实际上同样也会共享和复用已有的基础设施,这样来最大程度地借助已有的物理层来实现普及。</p> <p>这里我们顺便说一下,这些物理设备,在实际使用上可能会非常不一样,这是元网的性质决定的。</p> <h4 id="4-metanet-模型-对协议栈每一层的影响">4. MetaNet 模型 (对协议栈每一层的影响)</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/13.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/13_hu5f41871c437c4109620b611c0715afe9_47707_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/13_hu5f41871c437c4109620b611c0715afe9_47707_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="13.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,说完了整体上的大形状,接下来我们从最下面的物理层来讲起,向上挨个去分析每一层的具体情况。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/14.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/14_hu86c0b382483c3b82b12d719dc3e3b80c_61285_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/14_hu86c0b382483c3b82b12d719dc3e3b80c_61285_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="14.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>首先是物理层。现实网络中,物理层是对原始数据帧的接收和发送。这是一种很宽泛的封装,实际处理的是节点跟节点之间数据交换的行为。</p> <p>为什么 bitcoin 会改变 TCP/IP 的物理基础这一层呢?</p> <p>我们已经知道,矿工之间是通过专门的强化的通道连接,经济激励会让他们不断改进连接,不断改进相互沟通的效率,这样才能尽可能更快地去处理区块,获得更多的奖励,更有竞争力。</p> <p>这种激励反过来会改变物理层的模型。举个例子,在挖矿的演变过程里面,挖矿产生的能源消耗本来非常大,但是由于有激励,有挖出的币来驱动,在真实世界里,会逐步转变为对绿色能源的利用,因为这样是最优解 (这些能源是所谓的 least-useful energy 效用最低的能源)。</p> <p>具体的说,本地的网络就会用专门的硬件设施去做更大更快更大吞吐量的本地路由,然后通过共同的广播机制去高效地广播到其他子区域,那么这也是 bitcoin 在未来改变整个TCP协议站的方式。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/15.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/15_hu310fb380e01e0a25dd64bc1ea22fe816_48108_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/15_hu310fb380e01e0a25dd64bc1ea22fe816_48108_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="15.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>然后是网络层。当前网络层主要是基于 IP 协议的互联网协议。这一层是互联网的基础协议,经过多年的完善,实际上是非常牢固、非常可靠的。如果我们把 IP 协议看成是网络上不同部分的一种定位,寻址和路由的方式,那么比特币网络实际上提供了一种不同的方式。新的方式是建立在分层网络上,不同的层次会有不同的专门化的方案,通过 Miner ID 来互相识别和沟通,并建立相应的声誉 (reputation) 。 Minor ID 是矿工的一个标识,当你发起了跟特定事务相关的请求,很有可能你只关心核心的矿工网络上,那些提供特定服务的成员,比如说如果你拉取视频的话,是专门提供视频流的数据服务商。那么有的矿工(以及与他们关联的服务商)在你关心的这个事情上有特别的积累的声誉,它通常也能更好的处理你的请求,提供更有竞争力的服务。</p> <p>这种声誉,它独特之处在于,它有持续的 POW 也就是工作量证明来作为背书,与传统的可以刷,可以作弊的好评系统相比,这种声誉的累积是更可靠的,很有可能改变我们聚合,访问,反馈及评价的方式。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/16.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/16_huf5226943b95efda433c08801ab9f7226_57785_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/16_huf5226943b95efda433c08801ab9f7226_57785_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="16.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是传输层。当前的传输层是 TCP/IP 来实现的,是主机之间可靠的端对端的传输。在这一层,我们可以把 tx 也就是交易,当成一种非常特别的消息格式,这样在传输层我们可以把我们关心的数据封装到特定的消息格式里面去,通过网络节点去传播,就像一笔典型的 tx 一样。这样的话,交易消息就是对应用数据的封装了,我们知道 bitcoin 的交易格式已经有10多年的历史了,已经被证明是非常可靠,而且非常紧凑。尤其是底层的专门硬件不断进化了之后,这种消息格式也会跟着一起进化,这样 tx 的处理也会变得更加高效。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/17.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/17_hub78629be2686f20c6bf25ecc9e1c1499_39719_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/17_hub78629be2686f20c6bf25ecc9e1c1499_39719_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="17.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,在传输层之上,我们可以看到 bitcoin 自己有专门的一层。之前我们一直在说 bitcoin 对其他层会产生影响,然而 bitcoin 作为独立的一层,它实际上对上面的应用层以及整个架构其实也是有着深远的影响的。</p> <p>左边这些实际上是 bitcoin 能提供的功能,这些功能都是传统的 TCP/IP 协议里没有的。给数据包盖时间戳,是经由 POW 背书的时间戳,在时间戳的基础上,会产生一个天然的排序。这个特点非常有价值,也是目前互联网数据包所没有的。因为 bitcoin 作为一个时序无关的系统存在,他其实并不关心这个事件是什么时候发生,以什么顺序发生,只是非常忠实地,按照事件发生的顺序把它们坍塌为一个状态。</p> <p>当然了,你可以手动地选择这些交易什么时候发生,通过你触发的时机。不过从整个系统的角度来讲,非常有价值的是,bitcoin 网络总是会按照单一的原始顺序,把所有的事件给逐个坍塌。这个特性影响会非常深远,作为对比,在传统的互联网协议站上,所有这些数据,他们发生的顺序是不确定的,它们在互联网上流动,被路由的顺序,从回溯的角度讲,也是不一定可靠的。</p> <p>存在性证明,是往账本上去添加事件的时候,自动就会生成一个跟它相关的 Merkle 证明,这个证明非常有价值,这也是为什么 bitcoin 被概念化成独立的一层的最本质的原因,就是因为有存在性证明。</p> <p>不知道大家是不是还记得第一页上的图,当应用层的数据被传输的时候,下面每一层都给这些数据追加一些额外的信息,而 bitcoin 这一层,追加的就是这些 Merkle Proof,也就是这些默克尔证明。那么这些证明用来确认,这些数据,在那个时候,是存在并且有效的。上面说的这几样,时间戳,顺序,存在性证明,他们之间是相互关联的,都能归结到这些默克尔证明上来。</p> <p>这个不变性,最后一条,记录的不变性 (immutability) ,是 bitcoin 可以提供的最核心的特性。</p> <p>互联网上的数据不变性非常重要,这种不变性,有能力确保,当一个事实发生了以后,数据历史是不可修改的。为很多事情能够提供线索,提供证据,是非常有价值的。</p> <p>上面三个特性能导出不变性,这就使得所有的数据,在应用层下面,都能经过 bitcoin 层,以用户无感的方式,也就是用户不需要知道它存在,自动就 “可追溯,不可变” 了。</p> <p>那么还有 (这一页左边最下面的) 数据层。bitcoin 本身其实是一个数据层,是一个非常通用的,可以处理各种不同类型数据的数据层,同样这也是在 TCP/IP 里面是没有的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/18.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/18_hu1f3c44785d166cfa2ab4fe7bf03c7be8_41867_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/18_hu1f3c44785d166cfa2ab4fe7bf03c7be8_41867_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="18.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,那么最上面的一层是应用层,这一层也是我们今天课程后半段的核心的内容。</p> <p>应用层,它的特点是跟传统互联网是非常融合的,可以延用以前的方式去处理数据,展示数据。在应用层这一层的开发者,还是用他们之前熟悉的工具去处理和展示数据,用户也用他们熟悉的方式去接收和交互,不需要理解什么新的东西。那么这一层其实可以是 bitcoin 无关的,也就不一定要跟比特币有关,可以作为一个可选项,既可以由传统的互联网支撑,也可以由 bitcoin 层支撑。也就是说,bitcoin 提供的默克尔证明这些,都是附加的,不是必须的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/19.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/19_hu5f41871c437c4109620b611c0715afe9_47707_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/19_hu5f41871c437c4109620b611c0715afe9_47707_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="19.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>说完了每一层的情况,我想回顾一下整体的情况。我们可以看到,随着发展,bitcoin 穿透了中间的传输层和网络层。这是不是说,有了新机制,老的就可以不要了?不是这样的,他们是各自发挥各自的作用。</p> <p>有人可能会问,不通过 TCP/IP 协议,那还怎么通信?</p> <p>实际上这不是什么新鲜事,比如说我们熟悉的 GPS 就不是 TCP/IP 协议的对吧?我们的 GPS 设备上面有 GPS 芯片。再比如,我们拨一个电话号码,打个电话给别人,这个也不是通过 TCP/IP 协议去找到对方的。</p> <p>我们认为,在未来 bitcoin 会发挥更大的作用,bitcoin 网络上也会有更大的价值转移。在这之后,会有专门化的网络逐步的出现。这个过程非常自然,就像矿机取代 CPU 挖矿那样,现在你已经看不到 CPU 挖矿了对吧。到那个时候,metanet 就会成为真正意义上的元网,互联网也会真成为真正意义上成为元网的子集。</p> <h4 id="5-应用层概览-alps">5. 应用层概览 (ALPs)</h4> <p>好,这个是目前应用层的400个项目的列表。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/20.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/20_hu2ab1adbb4cd7940e937c4293233d9012_100549_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/20_hu2ab1adbb4cd7940e937c4293233d9012_100549_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="20.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这个图其实是有点老的,最底下这个黑的可能看不太清楚,这个是 Bitcoin SV 协议;再往上一层,有一点点灰的是 MetaNet 的协议;然后再往上它分成三个部分,一个是协议层,一个是工具层,一个是应用程序层。那么每一层都对上面的一层提供支撑,应用是由工具来支撑的,工具是由协议来支撑的。</p> <p>今天我们不去讲具体的某一个应用,也不讲某一个工具,主要是在讲协议部分,以及这些协议对开发者写应用有哪些特定的帮助。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/21.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/21_hu7cc10be5deafc8da9eab03117f3ce655_58812_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/21_hu7cc10be5deafc8da9eab03117f3ce655_58812_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="21.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>应用层协议。那么,什么是应用层协议呢?应用层协议需要满足下面的三个特征,第一个是有效的利用 bitcoin 交易里内嵌的数据,第二个是将 bitcoin 交易作为消息格式来使用。第三个是能够 (在必要的时候) 通知应用层。只要满足这三个特性的东西,我们才称之为应用层协议。这三个特性可以帮我们去标准化,怎么来使用 bitcoin 来组织应用层的数据,并且在合适的时候,去通知更高层的业务逻辑。</p> <p>应用层协议对 (页面左边) 下面三个角色有什么影响呢?矿工和节点,他们通常是不关心这个协议的,他们更关注底层规则的执行情况。比如说,我们有大量的数据和行为,在 op_return 里,矿工对这些数据大部分时候是直接忽略的。服务提供商会考虑现成的协议,或者在没有合适的协议可用时,制定他们自己的协议,而用户则从成熟的协议里面获益,他们会用脚投票,去决定哪个协议能活得更长一点。</p> <p>举个例子,微软刚出 Win95 的时候,OpenGL 作为图形学的标准已经发展很多年了,是非常成熟的协议。微软自己弄了一个 DirectX,花了大量的成本,做了好多年,还是被各种吐槽。这种吐槽到了这样的地步,就是用户装了一个游戏以后,总是第一时间切到 OpenGL,有的游戏只支持 OpenGL。但微软比较厉害的就是,它不计成本不断投入,一直反复改进 DX,一直到 DX7 的时候,量变引起质变,然后 DX8 的时候,基本上就统治了图形渲染这一块,从那时候起,协议的战争就结束了。</p> <p>那么,从这个故事你也可以看到,协议的演化是有多方参与的,不同人关心的东西不一样,到最后会有一个比较厉害的胜出。</p> <h4 id="6-简易支付验证spv--可追踪证据链">6. 简易支付验证(SPV) &amp; 可追踪证据链</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/22.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/22_hucadb4b1b6c81ea43c4ad7f28b2695986_83614_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/22_hucadb4b1b6c81ea43c4ad7f28b2695986_83614_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="22.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,现在我们来看一看用户数据提供商和矿工他们之间是什么关系?那么服务提供商相当于是像 Twetch 这样的微博应用,他们一方面利用应用层协议去把数据从链上给过滤出来处理,并且把区块里那些非常紧凑的数据转换成服务可以直接用的形式,这个过程实际上是一个数据展开的过程。另一方面,用户发起请求的时候,就把准备好的数据用更方便更可读的方式返回给用户。</p> <p>那么对于用户来说,他们随时拥有去链上直接验证数据有效性的能力。你看就是这样,利用 SPV,就是简易支付验证,来链上直接查看自己关心的数据的默克尔证明。</p> <p>SPV 服务实际上对数据提供商和用户都是有用的。</p> <p>我们拿微信支付来做个比方,微信相当于是矿工,也就是基础服务层。那么你扫了披萨店的二维码来付款的时候,披萨店处理之后,告诉你付款收到,并且在卡包里给你增加了一些积分优惠券,这一类东西。这个时候,如果你不放心的话,你可以自行去微信钱包里查看余额,去卡包里查看你的优惠券。 然而有一点不一样的是,传统互联网应用里面,上面说的这种消费记录,积分的变化都是微信在打理,比如说他决定只允许你查6个月之内的,你就只能查6个月之内的。也就是说,核心的业务数据都在微信里,用户和商户其实是很被动的。但是在 bitcoin 的协议栈里面,所有的数据是天然可以有明确的所有权定义和访问的授权的。这个是从本质上优于传统的互联网的。</p> <p>我们知道传统互联网上数据来来去去,转瞬即逝,如果商业公司不保存,不提供,或者是因为时间久远不可访问,或者甚至它直接淘汰掉服务的话,这些数据实际上是无迹可循。那 bitcoin 作用就是为所有的这些关键的事件,提供一条可以追踪的证据链。 我们叫它 a trail of evidence,可追踪的证据链。</p> <h3 id="正文-三-应用层协议与实践">正文 (三) 应用层协议与实践</h3> <p>好,现在我们进入今天的下半段就是应用层协议,它实际上分为数据协议、支付协议和 Token 协议。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/23.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/23_hu63388f4a34985f151b1890bf9c2fc7eb_51334_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/23_hu63388f4a34985f151b1890bf9c2fc7eb_51334_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="23.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>我们一个一个来看,首先是数据协议。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/24.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/24_hu78f99239985eec9819adf496d2be29d8_52322_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/24_hu78f99239985eec9819adf496d2be29d8_52322_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="24.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这个数据协议,是关于怎么去创建和组织区块链上的数据的。</p> <h4 id="1-b协议--b-cat---文件及流媒体">1. B协议 / B-CAT - 文件及流媒体</h4> <p>好,最简单的是 b-protocol,就是 B 协议。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/25.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/25_hud3e499056d160be6974adf47fb08176e_72597_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/25_hud3e499056d160be6974adf47fb08176e_72597_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="25.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>B 协议是什么意思?它支持把一个文件上传到区块链上。</p> <p>可以看到,下面是一个例子。我们把有紫色小花的这幅图,上传到区块链的1个 op return 里面。你可以看到它分成4个部分,data,media type,encoding,file name,分别是数据、类型、编码和文件名。这实际上是跟传统互联网上的 B 协议非常像,这个是 unwriter 在早期实现的,这也是最快的把文件放到区块链上的方式。</p> <p>实际的应用层,就是实际向用户提供服务的网页去取数据的时候,其实是可以做到 bitcoin 无感的,也就是说,你的数据,想在任何一个时间点上把它全部迁移到链上,都可以,是否在链上其实也不应该影响到实际的访问。读取出来后,其实对用户来说是个熟悉的界面,他也不是那么关心,你这个数据是不是真的在链上。随着网络承载能力的提升,传上去的文件可以越来越大,一开始是KB级别,很快,就可以到 MB 级别。也就是说,我们实际生活当中用到的大多数文件,理论上都是可以放到链上的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/26.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/26_hu29eb7d9e79486fa0ba056110f2859249_78087_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/26_hu29eb7d9e79486fa0ba056110f2859249_78087_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="26.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好。在 B 协议的基础上更进一步,我们有 B-CAT 协议。B-CAT 协议实际上是连续的多个块。同一个文件,如果太大,你不想把它放在一个 tx 里,那么你可以把它用连续的多个 tx 连接起来,本质上是同一个文件的多段存放。不仅仅是因为“大”而拆分,也有可能是因为,这个文件是连续的,有可能在你播放的时候,这个文件还没出来,比如说像一些视频直播,每分每秒都有可能会产生新的tx,所以我们叫它cat,是一个流媒体的形式。下面的类比你可以看到 b-cat 可以用来做数据流,可以用来做直播这一类的流媒体。</p> <p>(动画示意) 这是一个例子。 多个 output 实际上是一个文件的多个不同部分。那么在最终有一个 concatenation tx,在这个里面,我们把文件的多个部分,就是可以看到图中第一部分,第二部分一直到第n部分,使用他们所有的 txid 拼成了同一个文件。</p> <h4 id="2-d协议---动态可变内容">2. D协议 - 动态可变内容</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/27.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/27_huc272366c89d921a9c04d3c00a914620a_57132_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/27_huc272366c89d921a9c04d3c00a914620a_57132_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="27.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,跟刚才的 B 协议不一样的是,D 协议实际上针对的是可变的内容。那么对同一份内容,比如说右边的狗狗,我们可以支持修改图片,给它加一个皇冠。那么会产生一个引用,总是指向最新的内容,这里的 d 实际上是 dynamic 的意思,就像 DHTML 一样。我们可以看到,原始的小狗图片是一笔交易,然后我们修改了这个图片以后,是另一个交易。那么通过一个协议,对于原始数据和变化后的数据,我们可以保持一个单一的引用,利用它的先后顺序,总是找到最新的引用。</p> <p>在这个基础上,我们还可以给 D 协议里边的内容,给他们添加kv值,产生KV值的关联。你可以看到,在最下面的这笔交易里面,通过一个key和value来指定把哪些数据附加到这个图上去,比如说他可以设定一个新的 key 为 name,然后指定 value 为小明,只有图片的主人才能来做关联,那么这个是数据所有权的天然体现。</p> <h4 id="3-bob-协议---多协议组合">3. BOB 协议 - 多协议组合</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/28.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/28_hud4bed37cc426e41f7968b666fd4caf45_82529_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/28_hud4bed37cc426e41f7968b666fd4caf45_82529_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="28.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好的,BOB,这是一个更复杂的协议。BOB Schema,它实际上是一个多个协议的组合。</p> <p>有的时候我们的应用程序需要更复杂的数据组织形式,并且希望在复杂的协议之间交互,就用的上这个了。</p> <p>你可以看到这里有一个 tape,它这个 tape 就是磁带的概念。它是多个协议的线性排列,每个协议的数据块都是里面独立的单元。你可以看到这里有cell 0, cell 1, cell 2,它是分别是不同的协议,每个 cell 有一个独立的操作,比如说第一个 cell 里面存了 op_return,然后第二个 cell (也就是 cell 1),里面存了 B 协议,cell 2 存了 D 协议,cell 3 存了 B 协议,等等。那么这是其中的一个输出,另一个输出里面也是一样,也可以存很多东西。</p> <h4 id="4-关于-实际数据是否应完整存于链上-的讨论">4. 关于 “实际数据是否应完整存于链上” 的讨论</h4> <p>这里面有一个非常关键的点,也是一直以来争论非常大的情况,就是“实际的数据是不是要完整存在于链上”这个问题。那么每个应用需要根据自己的情况去考虑,这里的重点在于,这些数据全部都流经了 bitcoin 这一层。</p> <p>考虑到实际情况的复杂性,真实世界里面数据(按照重要性)可能会有很多层。</p> <p>我们拿一局足球比赛来打个比方,一场比赛90分钟打完以后,最关键的核心数据是,谁赢了,谁输了,比分是多少,谁进了球。那么这些信息,我们完全上链是毫无问题的。更多的数据,比如说有多少观众,多少次射门,每个球员跑了多少米什么的,这一类信息是是完整的上链还是哈希上链呢?就可以考虑一下了。</p> <p>更进一步,这场球赛的每一分钟里,每一个球员的位置,他们的运动,这一类实时的细节,如果你不是专业的针对运动员的数据分析服务,绝大部分情况下是不需要上链的。</p> <p>但是,不管这些数据最终是否上链,他们全部都能够在他们发生的时候,流经协议栈的 bitcoin 层,是可以获得默克尔证明和时间戳、存在性证明等这些好处的。这是 bitcoin 这一层非常有价值的服务。</p> <h4 id="5-元网-metanet-协议">5. 元网 (MetaNet) 协议</h4> <p>好,接下来我们看到的是元网协议。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/29.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/29_hu55a2bc69aceaadf80169c65c6ac58b6c_72037_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/29_hu55a2bc69aceaadf80169c65c6ac58b6c_72037_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="29.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>那么元网协议作为一个数据协议,其实它是一个相对比较复杂的东西。这里限于篇幅,我们只是简单的介绍一下,如果深入的话,这一个协议可能就把剩下的时间全部都给占去了。</p> <p>元网协议最重要的关键点是,它是用来 <strong>建立数据之间的联系</strong> 的。而且通过建立联系,它形成了一个层次化的结构,他用 “边” (Edge) 这个概念来把逻辑相关的 tx 给关联起来。要是没有这种关联呢,tx之间只有先后的关系,也就是基于时间戳的排序。有了这个树状结构,最大的好处就是任何一个数据都可以去层次里面去追溯和定位。这种定位对复杂的数据组织来说是非常必要的。</p> <p>举个例子,比较大型的 3D 游戏里面,有数以万计的物体,它们都组织在一个巨大的场景树上面,这个场景数可以提供各种需求和服务,比如说你哪个物体看不到,需要被裁减了,哪个物体碰撞了,需要被反弹了,这些都依赖树状结构去提供快速的筛选,检索和定位的服务。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/30.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/30_hu01abdeb40017282c962a897a2116453e_77547_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/30_hu01abdeb40017282c962a897a2116453e_77547_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="30.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,我们看到这个元网里面,最重要的一个概念就是“边”。</p> <p>“边” (Edge) 的定义是什么?它是节点跟节点之间,逻辑上的关联性。在子节点里面有父节点的签名,这一点是非常重要的。你可以看到下面的放大的框里面,子节点的输入里面父节点的签名。这样的话就能够避免假的子节点,也就是别人伪造一个子节点,因为如果没有父节点的私钥的话,它是没法签名的。在子节点的输出里面,是有父节点的 txid 的,这样的话就构成了一条边,你通过子节点你可以随时往上追溯。</p> <h4 id="6-metanet-应用---权限管理--版本历史">6. MetaNet 应用 - 权限管理 &amp; 版本历史</h4> <p>好,我们来看看 MetaNet 的数据协议能做些什么事。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/31.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/31_hufae00004d4a52493ab04b39593d804d6_86800_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/31_hufae00004d4a52493ab04b39593d804d6_86800_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="31.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>首先是它可以帮我们管理权限。你可以看到只有根节点就是p0的节点,它能够去创建p1和p2,而在第二层到第三层里面,只有p1才能创建 p1.1和p1.2。那么同理也是 p2 才能创建 p2.1 和 p2.2。</p> <p>那么这实际上是比 TCP 要更好的。</p> <p>这是因为,第一点是 bitcoin 天然会帮你去验证这些所有的这些交易,也就是这些消息它的签名来确保这些数据的有效性,可以节省大量的验证的工作量。 那么比特币的基础设施,使得你不用自己去验证哪些数据是有效的,哪些是伪造的。那么 (第二点), 在这些数据依赖的关系之外,除了数据本身,还有这些交易,就是 utxo 所定义的支付关系,也就是说谁给谁付了多少钱,这些支付关系也是非常重要的数据,在你需要的时候就可以直接用。</p> <p>打个比方,比如说我们一个游戏内的道具,在游戏里被别人捡了,实际上你是不需要在数据中去管理这种所有权的转移。因为通过 utxo 管理, 通过支付关系,它已经转移给另外一个人了,你不需要在数据里去再去额外定义一遍,它已经转移了。</p> <p>除了权限之外,MetaNet 还可以有很多其他的应用,比如版本管理。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/32.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/32_hu45d53b2448106515527ff3e921bb6bca_71566_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/32_hu45d53b2448106515527ff3e921bb6bca_71566_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="32.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>比如说在这里同一个 key,创建了多个交易,多个交易源于同一个 key,但是有不同的 data 在这个时候就很自然的能够形成,关于这个数据的版本历史,那么你再通过时间戳很容易就找到最新的版本了。你看就像这样 (动画演示),很容易你就知道 (对于同一个 key 来说) 后面的一定是新的版本。那么即使在同一个区块里面也是一样可以保证的。</p> <h4 id="7-mom---元网对象模型">7. MOM - 元网对象模型</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/34.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/34_hu5678404f36d4c53090b802338ddc8a1e_76565_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/34_hu5678404f36d4c53090b802338ddc8a1e_76565_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="34.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,在元网的协议的基础上,我们有一个对象的模型,这个对象模型其实是一种基于元网协议的组合。</p> <p>你可以看到右边,三种不同的颜色,比如说绿色,绿色是 B-CAT,实际上是一个父节点,然后下面追加了 n 个子节点,那么通过这种方式,我们可以把元网跟其他的协议相组合,可以形成非常灵活的配置,这也是一种非常有效的方式,去对不同的协议去组合分层 (来应对真实世界里繁杂的业务需求)。</p> <p>这些组合既可以是线性的,也可以是递归的,也可以是层次化的。在上面我是通过数据流的方式,然后到了下面每一个我又可以做成可变的,这是非常灵活的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/35.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/35_hu28fe43dcbc08a923dbbfc43c11b63380_33476_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/35_hu28fe43dcbc08a923dbbfc43c11b63380_33476_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="35.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这是 MOM 的一个实际的例子。对于同一组相关联的数据,在不同的情况下,你可以用不同的协议去解决不同的问题,这样你实现功能的时候可以非常灵活。同样,利用前面说到的版本(管理功能),你可以很容易的去迭代你的协议。</p> <h4 id="8-map--aip-等">8. MAP / AIP 等</h4> <p>好,还有一些比较小的协议,比如说 map 就是 map,它实际上是 magic attribute protocol。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/36.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/36_hu75815a849149fb7a819ba1091499cefd_71177_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/36_hu75815a849149fb7a819ba1091499cefd_71177_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="36.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>看起来好像很复杂,但实际上它很简单,它就是给现在已有的一个数据附加更多的 kv 值,那么也就是更多的属性。你上传了一个文件,你可以去写这个文件是作者是谁,是什么时候创建的,然后它的标签有哪些,你可以设置很多的kv值给他。实际上是给一个数据赋予更多的属性。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/37.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/37_hu78eeb41d7b36645aca1bf40bf70baae6_68385_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/37_hu78eeb41d7b36645aca1bf40bf70baae6_68385_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="37.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>那么另一个是叫 AIP 协议,AIP 协议是关于作者对有版权的作品来署名的。你可以通过这个协议,来签署一个专门的签名信息上去。</p> <p>也可以用它来做第三方的签署。比如说你有一笔交易,你需要另外一个公证人来签一下,他不会出现在 input 里边,但是你需要他的签名,这个时候你就可以用这个协议。或者说有很多人联名签署一笔交易,在不关心 utxo 的情况下,也可以做到很多人来签名同一个文件,比如说什么请愿书一类的,上面可以收集几万个签名。</p> <h4 id="9-协议的共性">9. 协议的共性</h4> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/38.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/38_hu512148bb9c60abfabe0068fe1c302c9e_61359_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/38_hu512148bb9c60abfabe0068fe1c302c9e_61359_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="38.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>好,前面说了一大堆协议,现在我们来归纳一下这些协议有什么共同的特点,也就是协议的共性。</p> <p>归纳起来,有这样两个特点,第一个特点是:有一个公共的前缀,每一个协议有各自的前缀,你可以用前缀去区分不同的协议。第二条是用 PUSH_DATA 来分割数据。这两点其实是非常浅显也非常直白的,你基本上你用过这些协议的一看就懂。</p> <h3 id="正文-四-支付协议--token-协议">正文 (四) 支付协议 &amp; Token 协议</h3> <h4 id="支付协议---bip270">支付协议 - BIP270</h4> <p>好,说完数据协议了之后,我们现在来讲,支付这一块的协议。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/39.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/39_hudcceafc2d5364112a006fbb8471627f2_49691_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/39_hudcceafc2d5364112a006fbb8471627f2_49691_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="39.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>首先我们先来看一看最近讨论的比较多的 BIP270 协议,这个协议的目的,是用来处理和简化商家和客户之间的支付的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/40.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/40_hu47b6be252c585948c99fcdfdaac6a26a_61614_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/40_hu47b6be252c585948c99fcdfdaac6a26a_61614_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="40.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>之前我们最经常见到的 (处理方式) 是什么样的呢?</p> <p>是客户签名了一笔交易,然后把它广播给网络,然后商户你自己去找,找到了你就认为这个人签了一个有效的交易。</p> <p>那么 BIP270 有什么不一样呢?</p> <p>是商户提供一个模板给客户签字,客户签了之后由商户去提交给网络。</p> <p>那么这样做,其实是跟我们比较熟悉的传统的支付系统是很像的,交易最终是不是完成,商户自己会对它负责。实际上是简化了整个流程,商户需要自己去广播,需要自己去结跟结算系统同步,从用户的角度来讲是省了很多事情的。</p> <p>BIP270 内融入了 SPV 的流程,对交易验证只要很少的运算资源就能完成。 不管是用户也好,商户也好,就可以通过 SPV 去验证,这样保证了就是大部分的支付场景下面,你想去验证交易,不需要花费什么运算资源,也不需要运行全节点。</p> <h4 id="支付协议---扫码">支付协议 - 扫码</h4> <p>好,那么第二种支付协议是扫码。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/41.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/41_hufbf79e838eac32216b075c2afb2bde65_64142_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/41_hufbf79e838eac32216b075c2afb2bde65_64142_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="41.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>扫码支付,我们其实用微信和支付宝已经很熟悉了。它是用户提供一个二维码,商家扫描这个二维码了之后,向用户钱包的服务器发出一个支付请求,然后钱包的服务器来让钱包的客户端去签名。签完名之后,跟前面的270是一样的,仍然是由商户去广播这笔交易。那么由商户去广播,对商户的安全性是有更好的保障。比如说你金额越大,商户可以等越长的时间,来保证有效的交易尽早的去到多个节点上。不一样的是,在扫码这种方式里面,用户钱包只显示一个二维码,系统里多了一个钱包服务器去参与,这个参与者需要去告诉钱包,什么时候对哪笔交易签了名。</p> <p>这里我们专门讲一下,我们前面说的 BIP270 和现在说的扫码支付,为什么说他们是可以扩展的支付模型 (scalable payment model)。</p> <p>有这么几条原因,一个是,原来的方式里,用户去广播,商户自己去对应的地址上面查,实际上是很低效的,有很多支付的时候,你会对同一个地址查多次,如果不同的客户付到不同的地址,你要到不同地址上去监听,是很低效的。 第二个原因是,(在 BIP270 下) 用户是可离线的,在特殊的情况下,比如说有时候电梯里没信号,这时候不需要联网就能产生交易。再比如说有很多设备他们可能不被允许联网,比如说摄像头,它也可以产生有效的交易。 第三条原因是,商户也可以依赖 SPV 不需要运行全节点。那么这对商户来说就省了很多事情。</p> <p>所以有这么几条,那么这种模型不管对商户也好,对用户也好,都是可扩展的。</p> <h4 id="支付协议---paymail">支付协议 - Paymail</h4> <p>好,第三种支付协议是 Paymail。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/42.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/42_hua2336646873f7c32f1091059eb96e63c_73162_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/42_hua2336646873f7c32f1091059eb96e63c_73162_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="42.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>Paymail 是一种非常有价值的服务,本质上是利用电子邮件来做一个别名。它比 1 开头的 bitcoin 经典地址好记很多。就好像你用一个域名一样,如果没有域名的话,大家都通过 IP 去访问互联网,是非常不友好的,而且不太可能记住。</p> <p>从实现的角度,我们简单说一下细节。用户是依赖 Paymail 的服务来做这个地址的决议,就好像我们通过域名的服务 (就是DNS服务) 去确定对象的IP。我们有理由推测,提供这个服务(从别名到具体地址,或者到公钥的这种查询,解析和决议这种服务),它本身也会变成非常有价值的服务。 有了一个明确的协议了以后,任何人都可以进来竞争,促进网络不断的去进化。</p> <h4 id="token-协议">Token 协议</h4> <p>在数据协议和支付协议之后,我们再讲一下简单的讲一下 token 的协议。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/43.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/43_huccc0afbdf9e1b7889ee0d347018a1378_53124_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/43_huccc0afbdf9e1b7889ee0d347018a1378_53124_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="43.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>Tokenized 我简单提一下,它也是传统的 request/response 模型,对开发者来说是很熟悉的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/44.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/44_hu62985734d834399fe454b5d4ac8e2e69_70683_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/44_hu62985734d834399fe454b5d4ac8e2e69_70683_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="44.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>它通过预言机来引入外部的数据,它里面也内置了一些他自己的合约,有很多合约,比如说什么电影票什么的,可以拿起来改一改就能用。 Tokenized 的特点是针对监管是非常友好的,他有很多方式来帮助监管来判断,这里时间关系我们就不展开说了。</p> <p>另外一个 token 协议是 Run (RunOnBitcoin)。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/45.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/45_hu352356f695267cb56da675760977be94_47827_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/45_hu352356f695267cb56da675760977be94_47827_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="45.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>跟 Tokenized 不太一样,Run 更加专注于现实世界的物品或者虚拟物品,它的数字化和通证化的,一个相对快捷的方案。他这个协议本身要比 Tokenized 更简洁一些,更好扩展一些,他很容易支持自定义的物品,也是支持智能合约和 Token 了。</p> <h3 id="正文-五-应用层工具包-toolkits">正文 (五) 应用层工具包 (Toolkits)</h3> <p>好,讲完了协议这一块,我们来简单的了解一下,应用层是怎么通过工具包来支持的。因为对于应用层开发来说,光有协议是不够的。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/46.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/46_hu369788078c0fd820d8dd1db71d28d55b_59312_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/46_hu369788078c0fd820d8dd1db71d28d55b_59312_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="46.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>我先问个问题,就是,应用层,具体指的是什么?</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/47.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/47_hu2f5e665a38a252dd3c6d6606a96b8828_83041_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/47_hu2f5e665a38a252dd3c6d6606a96b8828_83041_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="47.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>有 ALPs,也就是应用层协议,和开发者工具,商家服务,还有应用 (这些选项)。</p> <p>那么应用层具体指的是什么呢?</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/48.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/48_hu04aeb2085d7d49c7472915cff4ccd487_57268_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/48_hu04aeb2085d7d49c7472915cff4ccd487_57268_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="48.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这里的基础设施是指应用层的基础设施,那么协议跟基础设施有什么区别?协议其实只是协议而已,基础设施才能够让开发者可用。只有一个单纯的协议的话,开发者是没法上手去直接做自己的事情的,就像我们开发游戏是不可能拎着 OpenGL 和 DirectX 就直接上了,我们可能还需要什么 Unreal,Unity这样的引擎或开发包。</p> <p>那么工具包里面,既包含了协议,也包含了基础设施,说白了就是一些代码,我们可以直接拿来就能开发。那么我们也能看到,这里应用层它实际上包含了协议和工具包两部分。</p> <p>好,在最后,我们罗列并对比下,所有的应用层协议,和他们对应的这个工具和基础设施。</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/49.jpg" width="960" height="540" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/49_hu39969de2f34856373037a16320864589_112611_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/49_hu39969de2f34856373037a16320864589_112611_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="49.jpg" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>你可以看到有三种颜色,蓝色的部分是数据协议,深蓝色的是支付协议,黑色的是 Token 协议,那么右边是它们对应的工具,一般都是一些软件的 SDK。</p> <p>在我们做实际开发的时候,我们不一定会完全的符合已有的协议,你可以部分的符合,然后可以扩展,甚至你可以演化出你自己的协议。</p> <p>比如说我们小聪游戏,打算实现游戏里的道具,那一开始用 Tokenized 了,后来发现游戏里的道具资产,有各种奇奇怪怪的属性,而且运行的时候还会不断变化。这个时候对这些很特定的业务需求,Tokenized 就不是很灵活了,所以后来我们定制了自己的协议,一开始我们叫它 GAP,也就是 game asset protocol,游戏资产协议,后来我们改了一下名字叫 GAS。</p> <p>实际上如果你的应用产生了真实的价值,你大概率会定制出你自己业务相关的方案。甚至可以这么说,你的业务越深入,你就会越来越寻求专门化的方案,毕竟这是业务上的积累,是非常有价值的。</p> <h3 id="课后问答">课后问答</h3> <p>好了,以上就是今天这节讲座的全部内容,感谢大家的参与。接下来我会对讲座的过程当中收集到的一些问题来解答一下。</p> <p>第一个问题,有朋友问能不能开发一款战棋游戏,我要说,这是好主意,欢迎来跟我们一起参与,来设计。</p> <p>第二个问题,有朋友问,“不上链,但是流经 bitcoin,所以可以对这个数据来进行默克尔证明”,这个怎么理解?</p> <p>实际上是这个数据的哈希是可以存在 op_return 里的,然后 op_return 是随着交易一起上链的。所以这要看具体怎么用了,可以多层次的去用,可以去把所有的数据坍塌成一个哈希,也可以坍塌成一个数据流,这就取决于具体对数据的重要性的分析了。</p> <p>第三个问题,还有同学问,在 MetaNet 里面是怎么证明自己拿到的版本是最新的?这个问题是因为所有的所有的 tx 是有一个时间戳排序的,即使是在同一个区块里面,实际上是它交易顺序也是完全确定的,我们完全可以依赖 bitcoin 系统本身的顺序,来拿到最新的版本。</p> <h4 id="问题开发应用时怎么判断什么情况下用哪一个协议">问题:开发应用时怎么判断,什么情况下用哪一个协议?</h4> <p>好,然后第4个问题,就是,这些协议比较零散,在开发一个 APP 的时候怎么做判断,什么情况下用哪一个?</p> <p>这个问题,最核心的点,其实这是一个很大的问题,但最核心的点是,你要问自己一个问题:我的数据需不需要跟别的应用共享?如果不需要的话,你完全可以用你自己定制的方案,如果需要的话,你就尽量用已有的协议,这样能显著的降低你跟别人沟通的成本。</p> <p>在这个基础上,我还想提一下,“对协议怎么去选择”,其实是反映出来的是,你怎么看待你自己程序里的关键业务数据,你怎么判断,怎么分析这些数据。这个话是怎么说呢,因为我们做开发的,就是有一个词之前比较流行,叫数据驱动开发,也就是 data-driven development。我们发现,凡是这种由数据驱动的程序,就是你一开始就想好你的数据是怎么来到哪去,这种数据驱动的程序,它整体的复杂度,会比那些就是强调业务逻辑的那种程序,复杂度要低很多,你的程序的流程会清晰很多,可读性也会好很多,出 bug 几率也会低很多。</p> <p>那么再回来看这个问题,就是你选协议的时候,最好的做法是,先思考你的业务,先思考你的业务的核心数据,然后围绕你的核心数据去做设计,做架构,然后对你自己的核心数据的特点,你明确的理解它了,你知道了明确的数据从哪来往哪去了以后,你再去选对应的协议,那就很显然了。</p> <h4 id="问题这些协议是稳固不变的还是会随着时间的推移随-bitcoin-体系的演化而变化">问题:这些协议是稳固不变的,还是会随着时间的推移,随 bitcoin 体系的演化而变化?</h4> <p>然后是第5个问题——你认为这些协议是稳固的不变的,还是会随着时间的推移,整个 bitcoin 体系的演化而变化的?</p> <p>这个问题是这样的,bitcoin 底层的协议,我们知道它是 set-in-stone,就是我们在下次更新之后不会再变了。但是这是不是说,应用层的协议也都不变了?其实不是这样的,我觉得如果站在 5~10 年的尺度上去看的话,整个系统,从应用层的角度来讲,一定是一个不断发展不断演化的过程。</p> <p>要是比特币的分层网络 (BLN),它能够向我们期望的方向发展的话,我觉得,可以预见,这个行业应该是会从粗放式的,慢慢的转向精细化、专业化,对特定的业务会很重视,那个时候就不是看什么TPS,看峰值的这个时候的业务量。那时候会去关心什么呢?比如说,平均响应延时,终端的用户体验,这一类的问题。就是相当于在特定环境下,解决特定环境的痒点,痛点,这种能力指标了。</p> <p>到那个时候,我觉得那个时候可能会有更多的专有的定制的协议,甚至是有的私有协议,可能还会产生比较重大的影响。</p> <h4 id="问题目前有没有一个比较全面的支持所有协议的生成-tx">问题:目前有没有一个比较全面的支持所有协议的生成 tx?</h4> <p>还有一个问题是,目前有没有一个比较全面的,支持所有协议的生成tx? 这个问题比较深,现在目前还没有。但是可以剧透一下,我自己写了一个小工具,这个小工具可以给我自己作为一个参考,生成各种不同协议的 tx,我其实是打算把它梳理成一个比较大的条目,在你写 bitcoin 程序的时候,可以直接三五行代码拷过去就能用。这个可能不能作为一个严谨的工具,但是可以作为一个个人的小教程或者是示范代码,之后我可能会把它放到 GitHub 上去。</p> <h4 id="问题bitcoin-是一个基于经济激励的系统这个激励在开发者层面上如何体现出来呢">问题:bitcoin 是一个基于经济激励的系统,这个激励在开发者层面上如何体现出来呢?</h4> <p>这个问题很有意思,总的来说我认为这是一个机会。</p> <p>有的同学可能会觉得像 Linux 那样的开放社区,通过开源作为纽带来激励。不是很好吗?看起来也行之有效地运行快30年了。然而你可能不一定知道的是,社区里面,中老年开发者的比例每一年都在上升,在他的核心开发者圈子里更是如此。也就是说,年轻的新鲜血液,越来越不倾向于参与这种开源协作了。</p> <p>当年大多数开发者在为微软的Windows操作系统开发应用程序的时候,苹果公司用 “app store 三七分成” 的简单口号,开发者们用脚投票,光速构建起来了一个庞大的开发者社区。可见正确的激励模型是非常重要的。</p> <p>我相信在不远的将来,在数据和价值上,一定能找到一个被广泛应用的商业模式,而这种商业模式是可以被标准化和协议化,并沉淀到应用层协议里的。这样一来,那些参与进来的开发者和厂商才能从中获益,广大人民群众才能真正有得花,有得赚。</p> <p>游戏的行业有句老话叫做:一流的厂商定标准,二流的厂商做引擎,三流的厂商做游戏,我希望早日能在BSV生态里看到这一场景的出现。</p> <hr> <p>好,就这些问题的话,各位同学那本次线上研讨会到这里就正式结束了,Bitcoin SV 线上研讨会,系列讲座将会在每周二及周四晚上8:00~9:00进行,为期一个月。屏幕上方的二维码是我们系列研讨会的报名链接,欢迎大家将课程推荐给身边的开发者,也希望你们持续关注 CSDN 上的 Bitcoin SV 开发者专区,链接是 bsv.csdn.net,谢谢,我们下期再见。</p> <h3 id="引用及视频地址">引用及视频地址</h3> <ol> <li><a class="link" href="https://bsv.csdn.net/m/zone/bsv/online_lecture" target="_blank" rel="noopener" >Bitcoin SV 线上研讨会 (全8节) 视频地址汇总</a></li> <li><a class="link" href="https://live.csdn.net/room/zxff716/6bGl6TfW" target="_blank" rel="noopener" >&ldquo;Bitcoin SV 应用层协议&rdquo; 讲解视频</a></li> </ol> <h3 id="jack-对-bip270-的扩展性的释疑">Jack 对 BIP270 的扩展性的释疑</h3> <p>本文中关于 BIP270 的扩展性的释疑,来自于我向 Jack Davies 请教时,他详细回复的邮件。</p> <p>关于这一点,我的问题如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Questions: </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">1. (about BIP270) what does it mean by &#34;Payments as we know them&#34; in PPT? (in page 38) </span></span><span class="line"><span class="cl">2. (about BIP270) Why &amp; how the method &#34;allows the merchant to scale their own security&#34; (mentioned in the audio without explaining)? </span></span><span class="line"><span class="cl">3. (about both BIP270 and Show&amp;Pay) Why &amp; how the method &#34;allows scalable payment model&#34; (mentioned in PPT without further explaining) </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">I read through BIP270 proposal and I believe I understand the difference between the proposed </span></span><span class="line"><span class="cl">method and the original naive method. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">for the security, are you referring to the broadcasƟng is done by merchant, so that it eliminates the </span></span><span class="line"><span class="cl">possibilty of client double-spending? </span></span></code></pre></td></tr></table> </div> </div><p>Jack 的回复中,涉及这几个问题的原文,摘录如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span><span class="lnt">32 </span><span class="lnt">33 </span><span class="lnt">34 </span><span class="lnt">35 </span><span class="lnt">36 </span><span class="lnt">37 </span><span class="lnt">38 </span><span class="lnt">39 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Question 1: </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">When I say &#34;payments as we know them&#34; I am trying to explain that BIP270 deliberately </span></span><span class="line"><span class="cl">mimics/takes inspiration from the way we do payments in the real world, with or without </span></span><span class="line"><span class="cl">Bitcoin. In other words, it mirrors the asymmetry between customer and mechant – the </span></span><span class="line"><span class="cl">merchant has the responsibility of broadcasting a Bitcoin transaction, in the same sense that a </span></span><span class="line"><span class="cl">merchant using an existing physical payment terminal provides a connection to the </span></span><span class="line"><span class="cl">Visa/Mastercard etc. clearance and settlement system. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Question 2: </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">When I mention a merchant being able to &#34;scale their own security&#34;, this is referring to the </span></span><span class="line"><span class="cl">fact that a merchant can make their own decisions e.g. how long to wait after broadcasting a </span></span><span class="line"><span class="cl">transaction to be confident that the Bitcoin transaction will make it onto the blockchain. As an </span></span><span class="line"><span class="cl">example (numbers made up, for illustration) if it takes 0.1s for a transaction to propagate to </span></span><span class="line"><span class="cl">50% of network hash rate, and 1minute to propagate to 99%, then somebody selling a cup of </span></span><span class="line"><span class="cl">coffee might be happy to give the coffee as soon as they have broadcast the transaction, but </span></span><span class="line"><span class="cl">somebody selling a car may ask the customer to wait for one minute. Note that in the case of </span></span><span class="line"><span class="cl">the car, the 99% propagation minimises possibility of the payment failing, but much faster </span></span><span class="line"><span class="cl">than in the existing world. So BIP270/SPV mimics existing payments model, but far more </span></span><span class="line"><span class="cl">efficiently, and quickly. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Question 3: </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">When we refer to the &#34;scalable payment model&#34; we are talking about a couple of things. First, </span></span><span class="line"><span class="cl">the fact that we are handing a full transaction to the merchant – this means that the </span></span><span class="line"><span class="cl">merchant can query the Bitcoin network to ask if the transaction has been &#39;accepted&#39;, or </span></span><span class="line"><span class="cl">request a proof that is in a block. This query is much more efficient/quicker than if the tx was </span></span><span class="line"><span class="cl">broadcast by the customer, and the merchant has to &#39;scan an address&#39; for incoming payments </span></span><span class="line"><span class="cl">– this makes BIP270/SPV much more scalable in this sense. Secondly, this method allows for </span></span><span class="line"><span class="cl">the customer to be offline, which means payments can &#39;scale&#39; to many different methods of </span></span><span class="line"><span class="cl">use e.g. smart cards/physical devices. Thirdly, this method does not require the merchant to </span></span><span class="line"><span class="cl">run a full node – it only requires that they have a copy of block headers and have a </span></span><span class="line"><span class="cl">connection (for tx broadcasting and querying) to the Bitcoin network of miners. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Hopefully the above should also address your question about why the method is more secure </span></span><span class="line"><span class="cl">(or resilient to situational double-spending), and yes this is in large part because the </span></span><span class="line"><span class="cl">merchant broadcasts the transaction, and that we apply Bitcoin&#39;s probabilistic security model </span></span><span class="line"><span class="cl">regarding how much of the Bitcoin (mining) network the transaction has propagated to. </span></span></code></pre></td></tr></table> </div> </div><p>Jack 的回复全面,系统,深入,一气读下来豁然开朗,令人击节。</p> <h3 id="彩蛋">彩蛋</h3> <p>能翻到这里的同学,可能是真爱了。附送彩蛋两个吧。</p> <p>第一个是应用层协议全貌缩略图:</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/ALPs-1.png" width="1591" height="922" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/ALPs-1_hu04b0d3bc4cac19d33b9111c12b4d2be6_193289_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/ALPs-1_hu04b0d3bc4cac19d33b9111c12b4d2be6_193289_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ALPs-1.png" class="gallery-image" data-flex-grow="172" data-flex-basis="414px" ></p> <p>第二个是应用层协议栈的简易说明:</p> <p><img src="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/ALPs-2.png" width="1280" height="720" srcset="https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/ALPs-2_hu125dde5bd4fd5fb3469334d372608f6e_196502_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2020-10-04-app-layer-protocol/images/ALPs-2_hu125dde5bd4fd5fb3469334d372608f6e_196502_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ALPs-2.png" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这两张图对应了今天研讨会 “应用层协议” 的两大块内容,可以作为一个快速的 CheatSheet 以供参考。</p> About Gu Lu https://gulu-dev.com/about/ Sun, 04 Oct 2020 00:00:00 +0000 https://gulu-dev.com/about/ <h2 id="profession---blockchain-industry-2018-2022"><strong>Profession - Blockchain Industry (2018-2022)</strong></h2> <ul> <li>Team <ul> <li><a class="link" href="https://brimless.net/" target="_blank" rel="noopener" >Brimless Lab - Full-scale Innovation.</a></li> </ul> </li> <li>Products <ul> <li><a class="link" href="https://sensilet.com/" target="_blank" rel="noopener" >Sensilet</a> Plugin-style Wallet</li> <li><a class="link" href="https://enfty.io/" target="_blank" rel="noopener" >ENFTY.io</a> NFT Trading Platform</li> <li><a class="link" href="https://defrontier.net/" target="_blank" rel="noopener" >Defrontier</a> GameFI Platform</li> <li><a class="link" href="https://defrontier.net/games" target="_blank" rel="noopener" >Overshoot</a> Blockchain Game</li> <li><a class="link" href="https://blockcheck.info/" target="_blank" rel="noopener" >blockcheck.info</a> Block Explorer</li> </ul> </li> <li>Solutions <ul> <li><a class="link" href="https://sensiblecontract.org/" target="_blank" rel="noopener" >Sensible Contract</a>: Smart Contract Solution</li> </ul> </li> <li>Blockchain awareness: Bitcoin / Ethereum / Solana</li> <li>Has been the founder of a blockchain startup for 4 years til now</li> </ul> <h2 id="profession---game-industry-2005-2020"><strong>Profession - Game Industry (2005-2020)</strong></h2> <ul> <li>Professional game development (15+ years as a game developer)</li> <li>Game engine awareness: Unity 5-2021, Unreal 2 / 3, Gamebryo 2.3 / 2.6 / 3</li> <li>Mobile platform awareness: iOS and Android</li> <li>Last-gen game console awareness: XBox, XBox 360 and PS3 (partially)</li> <li>Server awareness: architecture for both typical MMO games and casual games</li> <li>PM awareness: traditional milestone-driven model and modern scrum-based agile model</li> <li>Has been Senior Programmer/Tech Lead/Tech Director with various technical titles</li> <li>Has been Team Lead/Tech Manager/Startup Co-Founder in various management titles</li> <li>Has been involved in 10+ game titles across PC/Console/Mobile platforms</li> </ul> <h2 id="writings"><strong>Writings</strong></h2> <ul> <li><a class="link" href="https://gulu-dev.com/library/" target="_blank" rel="noopener" >Gu Lu&rsquo;s Library</a></li> </ul> <h2 id="tags"><strong>Tags</strong></h2> <ul> <li>hobbyist of <a class="link" href="https://en.wikipedia.org/wiki/Taoism" target="_blank" rel="noopener" >Taoism</a></li> <li><a class="link" href="https://en.wikipedia.org/wiki/Trekkie" target="_blank" rel="noopener" >trekkie</a></li> <li>ex-<a class="link" href="https://en.wikipedia.org/wiki/Quake_III_Arena" target="_blank" rel="noopener" >Quake III</a> player</li> </ul> <h2 id="contact"><strong>Contact</strong></h2> <ul> <li>@mc-gulu <a class="link" href="https://github.com/mc-gulu" target="_blank" rel="noopener" >on GitHub</a>.</li> <li>@gulucraft <a class="link" href="https://twitter.com/gulucraft" target="_blank" rel="noopener" >on Twitter</a>.</li> </ul> <h2 id="biography-written-in-2015"><strong>Biography (written in 2015)</strong></h2> <p>I&rsquo;m Gu Lu, a game developer moved to Guangdong Zhuhai(广东珠海) in 2014 (previously living in Shanghai for 9 years).</p> <p>After graduated from Wuhan University of Technology(武汉理工大学), I came to Shanghai in 2005 and worked at <strong>Ubisoft (Shanghai)</strong> for two titles - Ghost Recon: Advanced Warfighter(幽灵行动:尖峰战士) and Splinter Cell: Double Agent(细胞分裂:双重间谍). During that period I got myself familiar with <strong>Unreal Tech</strong>, gained some practical experiences about <strong>XBox and XBox 360</strong>, and became friends with a number of talented people who are actively gearing up the game industry in Shanghai.</p> <p>In 2007, I went to <strong>Goldcool Games</strong>(金酷游戏) which was later acquired by Shanda(盛大网络). A 3D MMO title named <strong>Magic World 2</strong>(魔界2) was kicked off after my arriving. I evaluated several engines (with Gamebryo chosen), picked up developers for this project, and mainly focused on developing it for around 3 years. I became Gamebryo-capable and had quite a lot of components designed and implemented during that time. The product was presented at GDC 2010 Expo, which was a fine journey to me in SF.</p> <p>In December of 2010, I co-founded <strong>U Know Games</strong>(游诺网络) and led the development of a typical MMO title for nearly two years. Beside the management role, I also participated in designing and implementing some main constructs: the underlying <strong>data-driven scripting model</strong>, <strong>byte-stream-and-replication-based networking model</strong>, <strong>frontend tool-chain</strong>, <strong>integrated all-in-one editor</strong>, and some other smaller components. However, with an unprecedented hardwork with the whole team, unfortunately we still failed to achieve our goal on time. It became a sad story that our investor ZQGames(中青宝) asked us to transform the MMO into a web game. While most of our developers resisted to this transformation, the project <strong>ended up being terminated</strong> in the autumn of 2012.</p> <p>Before moving to Zhuhai in 2014, I spent one and a half year working at <strong>CCP Games (Shanghai)</strong>, with main focus on helping to improve the toolchain of Dust 514, which was launched in May 2013. In the spring of 2014 my family made a decision to leave Shanghai for a whole new adventure in southern China. Currently I&rsquo;m working at <strong>Seasun Games(西山居)</strong> as a technical manager.</p> <p>Although in half of my career I have been some kind of managers, project leads and that sort of roles, I still recognize myself as a programmer, who constantly <strong>believes in simplicity</strong> when designing and coding, and takes the programming as <strong>a combination of gardening art and architectural engineering</strong>.</p> <p>I&rsquo;m currently working on some Unity-based mobile titles mainly, and a few VR prototypes in spare time.</p> <p>(<em>please note that the biography hasn&rsquo;t been updated since 2015</em>)</p> <p><img src="https://gulu-dev.com/about/2020-07-11-photo.jpg" width="1024" height="576" srcset="https://gulu-dev.com/about/2020-07-11-photo_hu5e91db4951b2efcd4e6608aab966bf14_67081_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/about/2020-07-11-photo_hu5e91db4951b2efcd4e6608aab966bf14_67081_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="about-me-photo" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p><img src="https://gulu-dev.com/about/2021-10-04-about-me.png" width="1280" height="720" srcset="https://gulu-dev.com/about/2021-10-04-about-me_hu4df05104c6483a71b02e1a677b8c8a14_383678_480x0_resize_box_3.png 480w, https://gulu-dev.com/about/2021-10-04-about-me_hu4df05104c6483a71b02e1a677b8c8a14_383678_1024x0_resize_box_3.png 1024w" loading="lazy" alt="about-me" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <h2 id="revision-history"><strong>Revision History</strong></h2> <ul> <li><em>2022-05-16</em> blockchain industry experience added</li> <li><em>2020-07-11</em> <a class="link" href="https://gulu-dev.com/library/" target="_blank" rel="noopener" >Gu Lu&rsquo;s Library</a> added</li> <li><em>2016-12-04</em> biography updated</li> <li><em>2014-06-03</em> written initially</li> </ul> 2020.09 为什么要把 BSV 用在游戏(这个应用场景)里? https://gulu-dev.com/post/2020-09-21-use-bsv-in-games/ Mon, 21 Sep 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-09-21-use-bsv-in-games/ <p>为什么要把 BSV 用在游戏(这个应用场景)里?</p> <p>创新营上,有朋友问起区块链的应用场景,想听我说一说,在游戏里用区块链,当时是怎么考虑的,我即兴回答了下,后来茶歇时 aaron 专门来跟我讲了下,问我能把不能把那个小发言写篇文章,听他这么说我很高兴,从福州回来之后,我回忆了下自己当时的发言,在这里记下来。</p> <hr> <p>首先要说的是,并不是我有十几年游戏的开发经验,就天然地觉得应该用力把区块链往游戏里怼,那样不就成了“手里拿着锤子,看谁都像钉子”了。</p> <p>那么为什么要把 bsv 用在游戏这个场景里呢,或者换种方式问,区块链跟游戏结合的底层逻辑是什么?</p> <p>之前讲了很多区块链对于游戏会有哪些帮助,今天反过来,谈一谈 <strong>游戏这个载体对区块链有什么好处</strong>。</p> <hr> <p>有三个相对而言更底层一些的特点,可以逐个简单说一下:</p> <p>第一,游戏是很少有的,一个完全无中生有冒出来的,纯线上的,与现实世界相独立,相平行的虚拟世界。这个虚拟世界与现实世界没有那么多千丝万缕的联系,黏连性低。</p> <p>稳定运营中的大型游戏,往往是一个“漂浮”于真实世界之上的,既“开放”,又“闭合”的自洽系统。</p> <p>而可能你没有留意的是,<strong>比特币系统也是这样</strong>。他们的某些特质,表现出高度相似的特征。</p> <p>我们知道,不管是固体还是液体,跟气体都是很难混合的。而两种气体,却能无需任何外力,很均匀,很自然地混在一起。</p> <p>是的,你可能明白我要说什么了,简单说,现实世界如果是“固体”和“液体”,那么游戏和比特币都是“气体”。</p> <p>真实世界里有不少的情形,具有强烈的时效性,地域性,易变性,如果我们去强制性地与区块链结合,强行赋予所谓的“不可篡改”的“优点”,有时候不仅没好处,可能反而是有害的,或至少也会造成“不必要的固化”这样的坏气味。</p> <p>注意,这并不是在说,真实世界就不应该与区块链结合,只是说这种结合同线上相比会存在相当的滞后性,就像传统行业花了很多年,都仍对互联网这种我们看做理所当然的空气一样的存在的东西,欲拒还迎。</p> <hr> <p>第二,游戏的自由度很好,不是那么依赖 “人设” 等等相对固化的特征。 拿直播举个例子吧,跟直播这种线上应用场景相比,游戏产品比较灵活,每一款游戏都可以有不同的定位,风格,就像电影一样,没有固定的套路。而 “直播” 往往无法脱离主播的人设,特点或标签而凭空存在,一旦环境有变,适应新情况通常是比较困难的。(这里举了个吃播的例子)</p> <p>这种不同寻常的灵活性,使得不同的游戏可以挂载不同的经济体系,在区块链的快速成长期,以不同的姿势,快速实验各种不同的可能性。</p> <hr> <p>第三,更微妙的是,游戏作为一个虚拟世界,往往比现实有更大的弹性,往往容错率更高,是相对“温和”的,万一被玩坏也是可以恢复的,并不会损坏真实世界。这就导致创新的负担小很多,不用背太多包袱。</p> <p>“温和”的环境具有对创新的包容性。</p> <p>一行游戏代码,如果有 bug,我们修复了,发个新版本,错误就烟消云散了,新版本的代码干净得好像 bug 从未来过。一家上千病患的医院,一艘上万吨级的油轮,如果核心业务逻辑与未经千锤百炼的“智能合约”强绑定,一旦出了问题,可能会造成难以逆转的灾难性损失。</p> <p>因此,对待还在快速发展中的新鲜事物,放在与真实世界相独立的虚拟世界中,不断创新和迭代,不失为一个好的选择。</p> 2020.05 主观货币 (Subjective Money) https://gulu-dev.com/post/2020-05-27-cobra-subjective-money/ Sat, 30 May 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-05-27-cobra-subjective-money/ <p>Cøbra 关于主观货币的论述。</p> <p><img src="https://gulu-dev.com/post/2020-05-27-cobra-subjective-money/2020-05-27-cobra-subjective-money.jpg" width="317" height="379" srcset="https://gulu-dev.com/post/2020-05-27-cobra-subjective-money/2020-05-27-cobra-subjective-money_hu5eb05937851a744973dbb42898b2c1e3_74791_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-05-27-cobra-subjective-money/2020-05-27-cobra-subjective-money_hu5eb05937851a744973dbb42898b2c1e3_74791_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="2020-05-27-cobra-subjective-money.jpg" class="gallery-image" data-flex-grow="83" data-flex-basis="200px" ></p> <p>半个月前,<a class="link" href="https://twitter.com/CobraBitcoin" target="_blank" rel="noopener" >Cøbra</a> 在 bitcoin.org 的代码仓库里<a class="link" href="https://github.com/bitcoin-dot-org/bitcoin.org/issues/3332" target="_blank" rel="noopener" >提交了一条 Issue</a>: My involvement with Bitcoin.org,里面提到他将在今年逐渐淡出 Bitcoin.org 比特币官方网站的活动,并把域名留给值得信赖的人 (will be left in trusted hands)。</p> <p>这个帖子<a class="link" href="https://www.reddit.com/r/btc/comments/gj36c9/bitcoinorg_owner_cobra_on_blockstream_fucking/fqiaylq/" target="_blank" rel="noopener" >在 reddit 上</a>被 Roger Ver 留言</p> <blockquote> <p>I have cash in hand. (我手里的现金已备好)</p> <ul> <li>MemoryDealers, Roger Ver - Bitcoin Entrepreneur - Bitcoin.com</li> </ul> </blockquote> <p>随后,此帖<a class="link" href="https://bitcointalk.org/index.php?topic=5248291.0" target="_blank" rel="noopener" >被转至 bitcointalk.org</a> 上,引发了热烈的争论。 Cøbra 也在此主题下两次回复别人 ( <a class="link" href="https://bitcointalk.org/index.php?topic=5248291.msg54487313#msg54487313" target="_blank" rel="noopener" >回复一</a>, <a class="link" href="https://bitcointalk.org/index.php?topic=5248291.msg54499261#msg54499261" target="_blank" rel="noopener" >回复二</a> ),这两段回复(尤其是第二段)的内容值得一读。</p> <p>在彩云小译的帮助下,我把 Cøbra 的回复整理成了下面两段文字,并各自拟了小标题。</p> <hr> <h3 id="回复一闪电网络和-blockstream">回复一:闪电网络和 Blockstream</h3> <ul> <li><a class="link" href="https://bitcointalk.org/index.php?topic=5248291.msg54487313#msg54487313" target="_blank" rel="noopener" >回复一的原文链接</a></li> </ul> <p>Cøbra 先回复了推荐 theymos 来接替的人:</p> <blockquote> <p>theymos 控制了太多的资源,最好还是选别人,即使他显然很值得信任。</p> </blockquote> <p>然后 Cøbra 写到:</p> <p>聪明人也会把几十年的时间浪费在没有任何结果的事情上。</p> <p>学术界到处都是这样的科学家和数学家,他们花费了一生中大量的时间去创造新的系统和概念,却被证明是浪费时间,而且对世界并未产生什么积极影响,(考虑到机会成本)他们的产出甚至会是负的,而这些人原本可以更好地利用他们的时间,去追求更好的想法。我个人认识一个物理学家,他后悔花了几年时间研究弦理论。你会感到非常吃惊,如果你知道,有多少聪明的高手泥足深陷于糟糕的项目里,无法自拔。</p> <p><strong>闪电网络缺陷重重。</strong> (注:比“打了一个糟糕的补丁”更糟糕的是,为了处理这个补丁带来的副作用,而引入的下一级甚至更多级的,无穷无尽的糟糕补丁)</p> <p>你打算如何教育用户,花钱之前得先把钱放到通道里?好吧,也许钱包可以自动打开通道。 那你怎么向用户解释,比特币网络间歇性的交易费飙升会导致他(用来交易)的钱没了?想关闭通道前,还得存更多的币进去 (为了交主网手续费)? 好吧,我猜你可能想到会为此放一个免责声明或者警告。那你又该如何解释瞭望塔 (watchtowers,灯塔,也就是巡视节点,一种用来检测欺诈的高级货)? 还有<a class="link" href="https://medium.com/@StrikeSide/how-to-backup-your-lightning-network-channels-170c995c157b" target="_blank" rel="noopener" >静态通道备份</a> (SCB,不备份状态的话,突然的事故就会导致丢钱) 又是干嘛的? 用户可能会期望能收到任意指定数量的币,但是想收币还得先确认自己<a class="link" href="https://medium.com/lightningto-me/practical-solutions-to-inbound-capacity-problem-in-lightning-network-60224aa13393" target="_blank" rel="noopener" >有充足的输入容量</a> (inbound capacity, 就是所有活跃通道的远端余额之和,不然钱过不来)。</p> <p>好吧,也许有一天(18个月?),Lightning 实验室能把这些概念全都隐藏起来,让用户无感,但这么做将无可避免地导致更强的中心化 (一个集中的访问入口,包含了交易所,商家,瞭望塔,良好连接的节点等等)</p> <p><strong>Blockstream 恶意满满 (a hostile actor in the space)。</strong> 他们希望比特币失败。</p> <p>的确有几个优秀的专家在那里工作,但是那些少数的优秀员工的大部分时间和精力,都花在比特币核心和密码学研究上,他们倾向于远离 Blockstream 的破坏性产品。比如你看 Pieter Wuille 就从不跟 Liquid (Blockstream 的侧链方案) 扯上什么关系。他们的市场宣传居然把 Liquid 说成是“去信任的 (trustless)”,这连创始人 Matt Corallo 都看不下去了(Matt Corallo <a class="link" href="https://twitter.com/thebluematt/status/1060101587584991233?lang=en" target="_blank" rel="noopener" >曾在 Twitter 上抱怨</a>:呃,都这样了还说什么 “去信任”, 这词能这么用吗?去什么信任啊,别扯淡了,莫非你们想重新定义 “trustless” 这个词吗?)</p> <p>Blockstream 干过的最糟糕的事是:他们让 “增加区块大小” 变得完全绝对无法实现 (absolutely impossible)。他们把一群暴徒煽动到了现在这个程度,以至于在可预见的十年里,区块尺寸无法得到任何增长,因为大量没受过教育的白痴会反对。 我已经无法看出 “硬分叉” 还被保留为一种(用于改进的)可能性,这太糟糕了,因为硬分叉实际上非常有用,而且“禁止 (通过) 硬分叉 (的方式来改进比特币)”的心态会伤害比特币,因为越来越多的技术债务会积累起来,而软分叉会让系统更乱更难维护,而有理由怀疑这就是他们想要的: 一个丑陋的系统,缓慢而拥挤,这样他们才好把他们的侧链作为更好的替代品来推销。</p> <p>在 Blockstream 之前,一般的共识是,在充分考虑且足够时间的准备下,通过硬分叉来升级系统是可行的。 而他们把这种共识从社区中驱逐出去。一个比特币支持者 (bitcoiner) 如果没有对 Blockstream 产生一定程度的怀疑的话,智商是堪忧的。你根本不需要认为这是什么该死的阴谋,只需要瞪大你的双眼看着,就是有这么一家公司,商业模式完全建立在 “只有比特币网络不好使,才有可能赚得到钱”之上。</p> <hr> <h4 id="关于回复一的评述">关于回复一的评述</h4> <p>Cøbra 在这个回复里一吐了胸中沉积多年的浑浊之气,道出了他逐渐淡出的原因:压在 BTC 头顶的两座大山—— (问题百出的) 闪电网络和 (乐见其“不成”的) Blockstream。他提到的诸多细节,但凡是一个对 bitcoin 历史有所了解的人,都会有所耳闻。</p> <p>Cøbra 在字里行间流露出的深深失望,与 Mike Hearn 当年在著名的告别帖 <a class="link" href="https://blog.plan99.net/the-resolution-of-the-bitcoin-experiment-dabb30201f7" target="_blank" rel="noopener" >The resolution of the Bitcoin experiment</a> 内毫无二致。Mike Hearn 指出区块尺寸锁死将是 bitcoin 项目失败的头号原因,而 Cøbra 则进一步指出和抨击这两个关键点:区块锁死后的扭曲和病态的解决方案闪电网络,及推动这一切发生的 Blockstream。</p> <p>作为一个 BTC 的支持者,这二者对于 Cøbra 是一个无解的死结。他的淡出是必然的。</p> <hr> <h3 id="回复二-主观货币-subjective-money">回复二: 主观货币 (subjective money)</h3> <p>如果说上一个回复里,Cøbra 解释了他淡出的原因,那么这一个回复才是真正有趣的部分。想要读懂这一部分,你需要对 bitcoin 的历史问题有了解,否则会略显晦涩,这里不多说了,看正文吧。</p> <ul> <li><a class="link" href="https://bitcointalk.org/index.php?topic=5248291.msg54499261#msg54499261" target="_blank" rel="noopener" >回复二原文链接</a></li> </ul> <p>关于比特币区块大小的争论留下了一笔有价值的遗产,它揭示了“比特币实际上如何运行”背后的政治。我记得当我开始接触比特币的时候,和其他人一样,我首先钦佩的是,这个系统是如何被设计为“没有任何人可以控制”。没有人可以冻结你的交易,没有人可以向你收费,没有中央权威机构。比特币不在乎你是一个杀人犯、圣徒、朝鲜独裁者还是一个12岁的孩子。比特币就是这样,它无需传递判断 (without passing judgement) 就对每个人都可用。 当你开始时,很容易看到比特币存在于客观现实之中,就像其他任何软件,甚至是黄金这样的物理存在。 但问题是,这不是真的,<strong>比特币是主观货币</strong> (subjective money)。中本聪(在当时)并未能充分意识到他的发明所蕴含的内在含义。这不是在挖苦他,因为大多数发明家都是如此 (无法预料到自己的发明会被如何使用,演变成什么样)。</p> <p>以实物黄金为例。</p> <p>黄金存在于客观现实中,虽然是否赋予黄金价值取决于你,但如果有人给你实物黄金,而你有工具来验证它的纯度,你将无法否认它是黄金的现实。黄金是最客观的货币形式,然而即便法定货币也是相当客观的。对于法定货币,虽然其价值和信任来自政府,但为解释和主观性留下的空间,却比你想象的要少。在唐纳德·特朗普是总统的情况下,假设你是一个难以抑制愤怒的自由主义者,作为一个对这个总统和政府的合法性毫无信任的人,你其实也并不会因为 “那些美元是特朗普印刷的,他的政府是非法的,因此那些不是真正的美元,抱歉。” 就拒绝接收美元。即使作为自由主义者,你已经失去了对政府行政部门(那些印出这些钱的人)领导人合法性的信任,法定货币的力量是如此强大,以至于很容易就可以“阻碍”你遵循自己的信仰去得出合乎逻辑的结论。你会很自然地坚持对美元的集体幻想 (collective delusion),其他的想法根本连成为杂念的机会都没有。</p> <p>不管出于什么原因,比特币和加密货币通常表现得更主观。</p> <p>也许是因为这些系统是个人主义的;你自己验证区块链,你不相信任何人为你做,对吧。比特币作为一种想法存在于每一个用户的头脑中,当系统(潜在的)变化发生时,接受还是拒绝,每个比特币用户的反应几乎都是不同的。 几乎所有的比特币使用者都会拒绝提升总量2100万这个限制;但是也有灰色地带,比如区块大小,这里的容忍度和改变阈值是非常主观的,而且用户之间的差异很大。我可能会同意区块大小加倍,而你也许不是。甚至你可能会想要缩小区块尺寸。假设比特币做了一个改动,将社区彻底一分为二:不管出于什么原因,一半的社区接受了变化,另一半拒绝了变化。这就会导致一个分叉,分叉双方具有相同数量的散列权力。这个时候,有谁能够客观地说哪个分支是真正的比特币?</p> <p>在这些问题上,比特币现金和比特币 SV 领先我们好几年,而不是像比特币那样忽视这些系统中存在的政治因素,他们似乎完全接受了它。从经济上讲,这会在短期内损害他们的利益,但是通过弄清楚这些问题,他们正在学习一些重要的经验教训。 有了 BSV 分支,我们看到了对于一条分歧链(a divergent chain),它可能比它的竞争原产地链更有价值,即使它也许只是暂时的。 当比特币使用者看到这些社区正在发生的事情时,他们会自然而然地感到厌恶,尤其是当他们听说比特币现金开发者向矿工征税之类的事情时,但他们感到厌恶是出于一种不安全感,因为这些不同的比特币衍生产品也暴露了比特币表面之下一直隐藏的东西。也许这种感受难以言传,但是在区块大小的争论之后,它是可以被清晰地感觉到的。我们不止一次感到,比特币比我们曾预想的更加主观,因此也比我们曾希望的更加脆弱。</p> <p>比特币并非政治无关 (not apolitical money),它是<strong>高度政治化的货币</strong> (hyperpoliticized money)。</p> <p>纵观历史,当社区中的人们彼此意见不一致时,他们就会互相残杀,直到胜利者出现。事实证明,这不是解决争端的理想方式,因此随着时间的推移,出现了更民主的决策方式。无论是通过暴力还是通过投票,政治都是在一个受限战场上进行,每个人都直觉地认为它不会超出这些界限。而加密货币系统的出现,将政治以不同的方式融入金钱本身,这些方式无法预测,而且在数千年的人类历史上也没有任何相似之处。 想象一下这样一个世界: 一个民主党人和一个共和党人不能彼此交换价值,没有一种中性的货币形式,因为他们在货币本身上存在分歧,一个使用民主党货币,另一个使用共和党货币 &ndash; 两者有完全不同的理想和目标。</p> <p>如果我把你放进时间机器,把你丢进2120年,假设比特币仍然存在(或者某种声称是它的东西)。 你将不得不花费数天时间研究每一次分割和分叉,每一次升级,每一次争端背后的权衡取舍,等等,才能最终确定哪一种在当时运行的比特币版本是真正的比特币。 在比特币现金 (Bitcoin Cash)因为利用比特币最大的缺陷而被抛弃多年后,人们仍然憎恨罗杰·弗(Roger Ver),因为它可以从主观上重新定义。如果比特币真的流行起来,罗杰 · 弗只是第一个,在未来几十年里,比特币将被世界上一些最有权势的人和实体一次又一次重新定义,就像 bch / bsv 那些寂寂无名的家伙一样,有一天,我们可能也会发现自己身处互联网的某个孤独的黑暗角落,在虚无之中喃喃着“真正的比特币”,而世界早已不再在乎。</p> <hr> <h4 id="关于回复二的评述">关于回复二的评述</h4> <p>在表达了对 bitcoin 现状的深深失望,尤其是对闪电网络的所谓“改进”和 Blockstream 的“控制致死”的愤懑和厌恶后,Cøbra 在这个帖子里,进一步道出了更深层次的原因:</p> <p>在经过了早期的理想主义阶段后,bitcoin 并不是我们想象的那样政治无关,反而展现出了高度政治化的特征。</p> <p>在系统中有权势或有影响力的人们,几乎总是可以通过类似分叉的机制来表达自己的异见。这一次次的分裂,使得比特币系统变得脆弱的可能性大大增加。这不难理解,如果一个系统在变强大之前,就分裂得过分碎片化,是不可能变得真正有影响力的。</p> <p>如有可能,bitcoin 应该被建设成为一个稳健的货币系统 (sound money),不应该被操弄为用来表达政治意图的工具。大自然孕育一个生机勃勃的热带雨林生态系统,也许需要成千上万年;而无节制的捕猎,砍伐和开垦的所谓“利用”,很容易就能迅速地摧毁它。</p> <p>有别于其他的物种,人类几乎是无法脱离母体,需要母亲照顾时间最长的物种。显然,在第一个区块产生十余年后的今天,比特币的哺乳期仍未结束。在缺乏正确的引导的情况下,Cøbra 字里行间所担心的,比特币系统在现实中的主观化,在未来可能的虚无化,将非常有可能发生 (如果不是已经发生的话)。</p> <p>这个社会实验并不具有如我们所期望的那么强的反脆弱性。 如果我们不能持续地向正确的方向前行,块头再大,也将轰然倒地。</p> 2020.05 Craig 关于财产权的说明 (2015) https://gulu-dev.com/post/2020-05-16-craig-about-property-right/ Sat, 16 May 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-05-16-craig-about-property-right/ <img src="proxy.php?url=https://gulu-dev.com/post/2020-05-16-craig-about-property-right/bsv.png" alt="Featured image of post 2020.05 Craig 关于财产权的说明 (2015)" /><p>在 2015 年,Craig 关于财产权有一些值得一读的叙述。</p> <p>我在翻阅旧材料的时候,偶然发现——在2015年11月的一次开放论坛上,Craig 关于财产权有一些值得一读的叙述。这里首先摘录了提问 (来自台下的 Steven) 和 Craig 现场即兴的回答,然后是我的简单评述,最后是完整的视频链接。</p> <p><img src="https://gulu-dev.com/post/2020-05-16-craig-about-property-right/2020-05-16-craig-about-property-right.jpg" width="1250" height="650" srcset="https://gulu-dev.com/post/2020-05-16-craig-about-property-right/2020-05-16-craig-about-property-right_hu46561614842d022e216dda12406c163e_251260_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-05-16-craig-about-property-right/2020-05-16-craig-about-property-right_hu46561614842d022e216dda12406c163e_251260_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="2020-05-16-craig-about-property-right" class="gallery-image" data-flex-grow="192" data-flex-basis="461px" ></p> <h2 id="问答部分">问答部分</h2> <p>提问: (来自彩云小译) 你最近一直在推特上讨论财产,财产的性质,财产权,所以我想请你详细解释一下,也许比特币在其中扮演了一个角色。</p> <blockquote> <p>You&rsquo;ve been tweeting recently about property, the nature of property, property rights and so I&rsquo;d like to maybe ask you to elaborate a little bit on that, maybe have bitcoin plays a role in that.</p> </blockquote> <p>(再次说明下,这是2015年11月的材料)</p> <p>Craig 的回答:</p> <blockquote> <p>&ldquo;One of the most fundamental rights of being human is the ability to own and trade property. Every other thing that we do, other than trade, is done by animals, plants, or combinations of the above. There are tool building animals, there are all sorts of things. But what we do that is really unique is we trade. To do that fairly needs property. We need to be able to control our own freedoms and the only way to do that is to basically have the right to property, to ownership, to transfer &ndash; to decide what we want to do. That also means not telling people what we have. If we don&rsquo;t want to go out there and say I am a billionaire or I am running xyz or this is my life&hellip; I shouldn&rsquo;t have to tell people that. I should have the right to live frugally if I want to and to invest in business without telling people I am a billionaire&hellip; or that I am whatever &ndash; like some people have to these days because governments try to make us. We should be able to choose how we live and that is the fundamental right of property. That means being able to dispose of property as we want; to be able to share it, to take it &ndash; and that is what it is all about. Once we get things to where we have redeemable contracts and we link them to the blockchain. Where we can link money, and goods, digital rights and ownership into something that can&rsquo;t be changed. A fundamental open, honest, truthful asset &ndash; the blockchain. That&rsquo;s when we are going to see real freedom in the world.&rdquo;</p> </blockquote> <p>(来自彩云小译)</p> <p>“人类最基本的权利之一是拥有和交易财产的能力。除了贸易,我们所做的其他任何事情都是由动物、植物或者以上几种的组合来完成的。有制造工具的动物,还有各种各样的东西。但是我们所做的真正独特的是我们交易。要做到这一点,就需要财产。</p> <p>我们需要能够控制我们自己的自由,唯一的方法就是基本上拥有财产权、所有权、转让权——决定我们想做什么。这也意味着不要告诉别人我们拥有什么。如果我们不想出去说我是个亿万富翁,或者我在经营某某公司,或者这就是我的生活&hellip; &hellip; 我不应该告诉别人这些。我应该有权过节俭的生活,如果我想的话,我应该有权投资商业而不告诉人们我是亿万富翁&hellip; 或者我是什么人&mdash;- 就像现在有些人不得不这样做,因为政府试图让我们这样做。我们应该能够选择我们的生活方式,这是财产的基本权利。这意味着我们可以随心所欲地处置财产,可以分享财产,可以占有财产&mdash;- 这就是财产的意义所在。</p> <p>一旦我们得到的东西,我们有可赎回的合同,我们把他们连接到区块链。我们可以把货币、商品、数字权利和所有权联系起来,变成不可改变的东西。一个基本的开放,诚实,真实的资产&mdash;- 区块链。这就是我们在世界上看到真正自由的时刻。”</p> <h2 id="评述部分">评述部分</h2> <p>这个即兴的回答是远程视频的,有一小部分听不太清,但应该不影响原意。Craig 说到以下几点:</p> <ul> <li>交易是人区别于动物的显著特征之一,而财产权是交易得以进行的基础,因此对于人类社会的个体而言,<strong>财产权是根本性的权利</strong>。</li> <li>作为人类的个体 (这里我们以小A为代号),财产权的意义主要表现为下面两点: <ol> <li>小A是否可以自由地与他人交易,很大程度上取决于小A是否能掌控和支配他自身财富的所有权 (<strong>可支配的财产才可交易</strong>)</li> <li>在掌控和支配的基础上,小A有权利保密,在需要的时候,不让其他的人或组织知晓他财产的具体情况 (<strong>可控的隐私保护</strong>)</li> </ol> </li> <li>区块链构造了一种开放,可信的机制,把货币,商品及对应的所有权联系起来,允许个体享有更好的支配权,(在不损失必要的可追溯性的前提下) 享有更好的隐私,从而更强有力地保障了财产权。(<strong>区块链作为不可变证据序列的用途和意义</strong>)</li> </ul> <hr> <h2 id="完整视频">完整视频</h2> <p>最后是完整的视频链接 :</p> <p><a class="link" href="https://www.youtube.com/watch?v=LdvQTwjVmrE" target="_blank" rel="noopener" >All-Star Panel: Ed Moy, Joseph VaughnPerling, Trace Mayer, Nick Szabo, Dr. Craig Wright</a> (<a class="link" href="https://www.youtube.com/watch?v=LdvQTwjVmrE&amp;t=2898s" target="_blank" rel="noopener" >点这里直接跳转至第 48 分 18 秒</a>)</p> 2020.04 Wolfram 万物理论的简要解释 https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory/ Thu, 23 Apr 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory/ <h1 id="202004-wolfram-万物理论的简要解释">2020.04 Wolfram 万物理论的简要解释</h1> <p><img src="https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory/wolfram.jpg" width="952" height="820" srcset="https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory/wolfram_hu1605ef1c043d2b10a5d0f35d7c45871a_101284_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory/wolfram_hu1605ef1c043d2b10a5d0f35d7c45871a_101284_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="若干宇宙模型图" class="gallery-image" data-flex-grow="116" data-flex-basis="278px" ></p> <p>开始前先说一下,这是一个极简的笔记版,所以没有图,图可以到<a class="link" href="https://gulu-dev.com/post/2020-04-23-wolfram-fundamental-theory#toc_8" target="_blank" rel="noopener" >参考一节的 1 &amp; 4</a> 中看到。</p> <h2 id="400字浓缩解释tldr太长不看版">400字浓缩解释(tl;dr太长不看版)</h2> <ul> <li><strong>空间</strong> 空间是离散点的集合。</li> <li><strong>时间</strong> 时间是规则的逐步应用,是迭代的记录。</li> <li><strong>因果不变性</strong> 迭代产生的多条路径汇聚到相同的结果就是因果不变性。</li> <li><strong>个体选择</strong> 独立的个体选择仍然真实存在,但广义上可能不会影响结果。</li> <li><strong>相对论解释</strong> <ul> <li>运动是超图的 (无意识) 倾斜;光锥是超图的边缘斜率;</li> <li>黑洞是因果图的断裂 (独立宇宙);时间迭代曲线是超图的闭合循环。</li> </ul> </li> <li><strong>量子力学解释</strong> <ul> <li>全局客观现实拥有全路径;经验局部性使得“运用量子解释来描述事物”变得可行;</li> <li>从系统的内部视角去看,只存在一个可被观察者感知的客观实在;从全局看则受因果不变性支配。</li> </ul> </li> <li><strong>宇宙</strong> 宇宙从本质结构上是离散的,所有东西在同步扩张;从三维视角来看,所有东西都随着迭代变得更精细 (精细度存在超越性)。</li> </ul> <h2 id="背景">背景</h2> <ul> <li>为什么这个问题此前没有被解决? <ul> <li>此前由于缺乏足够的现代的计算范式 (modern paradigm of computation) 所以无法使用&quot;正确&quot;的方式去思考事物及他们之间的联系。</li> </ul> </li> <li>揭示物理学的根本理论有何意义? <ul> <li>揭示和理解万物理论,会对我们如何思考一般性事物,产生深远的长期影响。</li> </ul> </li> <li>古希腊哲学家关于世界的本源的看似暧昧的描述 (如四种元素等) 也许是有意义的。万物也许源自于简单的规则 (这也是为什么宇宙是可被理解的) (everything we see in the world might actually be the result of something simple and formalizable underneath)</li> </ul> <h2 id="简单规则多次迭代后的复杂性">简单规则:多次迭代后的复杂性</h2> <ul> <li>eg 1. 元胞自动机 rule30</li> <li>eg 2. 雪花元胞 (分形未必自相似) :从一个六边形的黑色元胞开始,如果一个元胞相邻的元胞有黑色的话,这个元胞就变为黑色。</li> <li>推广至宇宙生成规则 (可能的): <a class="link" href="https://www.wolframphysics.org/universes/" target="_blank" rel="noopener" >Registry of Notable Universe Models</a></li> <li>如果将某个规则无限迭代下去,会生成我们的宇宙吗?上图中的迭代步数只有几千步,要想利用这些规则来窥见我们真实的宇宙可能需要10^500甚至更多的迭代步数。这个迭代步数远超计算机的极限,所以恐怕我们无法用这个方法来得到我们的宇宙了。但是Wolfram在其中发现了一些和物理学对应的现象。</li> </ul> <h2 id="对物理学现象的解释">对物理学现象的解释</h2> <h3 id="空间时间和因果不变性">空间,时间和因果不变性</h3> <ul> <li>空间:是一群 <strong>抽象的、离散的点的集合</strong> ,从更大的尺度上看,这群点就成了连续的“空间”。 <ul> <li>Wolfram认为,离散空间的概念对“万物理论”至关重要。</li> </ul> </li> <li>空间的维度:非整数。 <ul> <li>在某种意义上讲,我们上面得到的各种模型都只有“空间”(点和线、面都是抽象的,就像水分子模型中的点和线一样),而我们的宇宙中所有事物都一定“由空间构成”。换句话说,这些图形构成了“空间”结构,而万物都在这个“空间”中。因此,着意味着例如电子或光子之类的粒子一定与某个图形的特征相对应。</li> <li>Wolfram估计,能够代表我们真实宇宙的图形的“元素”,要比涵盖了我们宇宙中的一切的“空间结构”还要高出10^200倍。</li> </ul> </li> <li>时间: <ul> <li>霍金:时间是能量的变化。能量的扩散(膨胀)称为正时间,正时间流逝速度与扩散的速度成正比。</li> <li>Wolfram认为,“在我们的模型中,时间只是 <strong>规则的逐步应用</strong>。”</li> <li>事件并不是和规则所对应的唯一顺序!规则只是去寻找那些抽象点间的相互关系,但是在当有多个可选择的事件时,规则并未指定去选择具体哪个事件。</li> <li>这意味着时间定义了初始规则和结果,过程可以有多条路径 (只要能完成规则指定的要求) 在这样的多路径系统中,每个路径都对应着一个可能的事件发生顺序。 <ul> <li>eg1: 现在到晚上了,生理规则告诉你,该去喂饱肚子了。但是这个生理规则并未指定你去吃什么。你可以先来杯奶茶,或者直接去吃正餐,然后餐后来杯咖啡,说不定还会再来几个串。不管你选择那个事件,你都完成了这个规则指定的要求。</li> <li>eg2: 想象有一只蚂蚁从原点开始走,那么即使这只蚂蚁在每一步都选择了它所认为的“独立的”道路,即使它认为它走路的“历史”是独一无二的,但是它最后也有可能与其他“独一无二的道路”到达同样的地方。</li> </ul> </li> <li>Wolfram认为, <strong>时间是事物之间的因果关系</strong> 。就像模型中所展示的,时间是规则在迭代时不断修改宇宙的抽象结构的一种应用。 <ul> <li>Time is about causal relationships between things. It’s the progressive application of rules, that continually modify the abstract structure that defines the contents of the universe.</li> </ul> </li> <li>时间是超图中事件、路径和抽象关系改变的记录。超图的变化是时间产生的因素,时间的存在体现了超图中抽象关系的改变过程。这些改变过程体现为时间,时间记录了这些过程,即“历史”。</li> <li>联系:fatalism, immutable time-chain (blockchain)</li> </ul> </li> <li>因果关系 <ul> <li>字符替换系统:产生的分支总是奇妙地合并在一起。</li> <li>Wolfram将这种分支-合并现象称为 <strong>“因果不变性”</strong>(causal invariance)。因果不变性是证明相对论的核心,是量子力学为何存在有意义的客观现实。</li> </ul> </li> </ul> <h3 id="相对论宇宙和量子力学">相对论,宇宙和量子力学</h3> <ul> <li>相对论 <ul> <li>关于观察者的独立性 <ul> <li>如果你想给整个宇宙建模,你作为宇宙的一部分,你也会在这个宇宙中。那么这个关系就成了,宇宙中有你,你建立了一个模型,模型中有整个宇宙……无限套娃下去。</li> <li>因此,给整个宇宙建模是不可行的。作为观察者,我们不可能“知道宇宙中正在发生些什么”。观察者所经历的只是一系列迭代更新的事件,这些事件可能恰好受到宇宙中其他地方发生的事件的影响。</li> </ul> </li> <li>关于运动与静止 <ul> <li>当观察者静止时,观察者的经历是垂直于时刻切片的一个竖直线</li> <li>当观察者运动时时,在时刻切片上的运动轨迹会发生偏移,此时会 <strong>自然地、无意识地构建一个倾斜的叶状图</strong> (观察者认为自己是相对静止的)</li> <li>这就是运动和静止的本质区别。</li> </ul> </li> <li>关于超图的斜率和光锥 <ul> <li>Wolfram此前在元胞自动机的研究中就发现,不管是什么规则,生成的图形边缘的斜率是存在一个最大值。(这个最大值就是光锥)</li> <li>观察者的运动速度不可能超过光速,因为在前面的因果图是不能倾斜超过45°的,如果超过了45°,那么“果”就发生在了“因”之前。</li> </ul> </li> </ul> </li> <li>黑洞 <ul> <li>黑洞的定义特征是事件视界的存在:光信号无法穿越,因果关系实际上已断开。</li> <li>在这个因果图刚开始的时候,因果关系相连着,但是从某个点开始因果图分开了,形成了一个事件视界。一个视界外发生的事是不会影响这个视界中发生的事件。这就是宇宙中某个区域可以“因果破裂”而形成黑洞的方式。</li> <li>实际上,在Wolfram的模型中,“破裂”可能更彻底一些:不仅因果图会断开,超图甚至可以分为几个断开的部分,而每个部分实际上形成了一个完整的“独立的宇宙”</li> <li>黑洞与因果不变性的矛盾 <ul> <li>我们在因果不变性中讲到,因果图中的路径总在会发生合并。但是当像上图中超图断开了连接,那么因果图中的路径最终便不会合并了。</li> <li>在此情况下,观察者必须“冻结时间”。在他们的叶状图中,连续的时间片段只会堆积起来,并且永远不会进入到断开的部分中。</li> <li>这个结论和广义相对论十分一致。对于一个离黑洞很远的观察者来说,似乎任何东西(包括光)跌入黑洞都需要无穷的时间。</li> </ul> </li> <li>广义相对论里的时间闭合曲线 (时光旅行) <ul> <li>规则导致的迭代变成了循环:我们觉得我们是在“时间中前进”,但其实我们只是在循环中一次次地循环着。</li> </ul> </li> </ul> </li> <li>宇宙 <ul> <li>宇宙可以从一个小小的超图开始,或者从一个自我循环(Self-loop)的超图开始。接着,根据应用的规则,它开始逐渐扩展。在一些特定的规则下,超图的尺寸在均匀地增加。在其他一些规则下,尺寸可能会发生波动。</li> <li>连续 or 离散? <ul> <li>即使超图的大小始终在增加,着并不意味着我们一定会注意到。我们看到的所有东西也许同时在扩张,所以实际上空间的粒度越来越精细。因此,宇宙在结构上是离散的,但离散程度相对于我们的尺度来说是越来越小的。而且如果这种趋势足够快的话,我们将永远无法观测到宇宙的“离散型”(因为每当我们在测量宇宙的离散度时,在得到结果之前,实际上宇宙又变得更加细分了)。</li> </ul> </li> <li>维度坍缩 <ul> <li>也许整个宇宙的超图始终在扩张,但是总有其中的一部分断开连接,变成了一个个不同大小的黑洞。更高的维度通过分离成了断联的“黑洞”分支坍缩。</li> <li>在传统的宇宙学中有一个迷:早期宇宙的不同部分是如何彼此“交流”的?比如,这些部分是如何消除互相干扰的?但是如果宇宙实际上是从无限维开始,慢慢降维到有限维的,那么这个问题就有了很好的解答。</li> <li>那些可以帮助我们判断宇宙早期阶段的大多数特征,都很快地被“加密”了,所以无法去重构它。 (信息落入黑洞,相当于 blockchain 上私钥弄丢了)</li> </ul> </li> </ul> </li> <li>量子力学 <ul> <li>(薛猫) 在量子力学中,一个系统往往在“并行”地做不同的事情,作为观察者的我们,只能观测到这些可能的结果。</li> <li>如果总是存在着很多不同的历史路径,那么我们如何认为,这个世界上发生了确定的事情? <ul> <li>量子力学的标准解释:我们是用确定概率的状态的“叠加态”来描述(例如,薛定谔的猫处于“生”与“死”的叠加态,不能简单的说,猫有一半的概率“生”,一半的概率“死”)。但这个说法总是让人迷惑,人们对客观现实的印象似乎是确定的(即猫要么“生”,要么“死”)。</li> <li>Wolfram认为,另一种可能的解释是,某种程度上每一种可能都存在一个现实的分支,而我们只是观测到我们的意识来到的那个分支。 <ul> <li>最终存在一个全局的客观现实(Global objective reality),它拥有不同的路径系统。但是我们的经验局限性(The locality of our experience)造成了我们只能用概率和量子力学的标准形式主义来描述事物。</li> </ul> </li> </ul> </li> <li>因果不变性:尽管从系统“外部”看,会有各种不同的路径系统,但是因果不变性意味着事件之间的因果关系网络始终是完全相同的。 <ul> <li>就像相对论一样,即使从系统外部看,似乎有许多可能的“时间线程”,但如果身在其中,从系统内部看,因果不变性在某种程度上最终意味着只有一个时间线程,或者说,实际上最终只有一个客观现实。</li> </ul> </li> <li>观察者选择在某一个状态时“冻结时间”,本质上是:在全局的多路径图中,还有其他各种状态的“量子力学”演化,但是观察者设定了他们自己的量子观测框架,以便他们选出一个特定的、确定的、经典的结果。</li> <li>建造量子计算机最大的难点:保持在一个特定的状态 <ul> <li>要想成功冻结时间,多路径系统的机构将迫使观察者构造越来越复杂的时间切片。</li> <li>如果我们想造出一个量子比特,我们必须将其隔离在量子空间中,就像黑洞中的事件视界在空间中被隔离一样。</li> </ul> </li> </ul> </li> </ul> <h3 id="终极规则-符合经验现实的模型">终极规则 (符合经验现实的模型)</h3> <ul> <li>要获得最终的物理学基础理论,我们仍然需要找到一个特定的规则。这个规则能够构建一个3维的空间,还要有符合我们宇宙的膨胀速率,基本粒子的特定质量和性质等等。</li> <li>“我自己探索简单规则产生的宇宙已经有40年了,我不得不说,即使到现在,我仍然经常被极其简单的规则产生的无法预料的复杂性感到震惊。”</li> <li>“在我们已经发现的科学定律中,我们可以看到,这个规则至少不会有很高的复杂性。但是,这个规则到底会有多简洁呢?我们不知道。”</li> </ul> <h2 id="参考">参考</h2> <ol> <li><a class="link" href="https://writings.stephenwolfram.com/2020/04/finally-we-may-have-a-path-to-the-fundamental-theory-of-physics-and-its-beautiful/" target="_blank" rel="noopener" >Finally We May Have a Path to the Fundamental Theory of Physics… and It’s Beautiful</a></li> <li><a class="link" href="https://writings.stephenwolfram.com/2020/04/how-we-got-here-the-backstory-of-the-wolfram-physics-project/" target="_blank" rel="noopener" >How We Got Here: The Backstory of the Wolfram Physics Project</a></li> <li><a class="link" href="https://www.wolframphysics.org/universes/" target="_blank" rel="noopener" >(the Wolfram Physics Project) Registry of Notable Universe Models</a></li> <li><a class="link" href="https://www.zhihu.com/question/387862824/answer/1157830985" target="_blank" rel="noopener" >(知乎) 如何看待Stephen Wolfram声称万物理论已被发现?</a></li> </ol> 2020.01 BSV 中国大会的动容一刻 https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment/ Fri, 31 Jan 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment/ <img src="proxy.php?url=https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment/bsv.png" alt="Featured image of post 2020.01 BSV 中国大会的动容一刻" /><p>2019年12月8日,BSV China Conference 现场。</p> <hr> <p>在 Craig 和 Jimmy 的炉边谈话末尾,Jimmy 问 Craig 有什么想对从中国各地赶来的 bsv 开发者想说的 (在前一天,大家已经有了不少深入的交流,我也当面请教了博士关于 onchain-gaming 的一些细节),Craig 说了一段 very touching 的话。当时我在现场,深受触动,回珠海的飞机上,在手机的记事本上凭回忆记下了这段话。</p> <blockquote> <p>I want you to be wealthy, through hard work. Wealth is not money. Wealth is the knowledge you learned, is the product you built. Wealth, is the education that you gave to your children, wealth is the education you gave to yourself. Wealth is something you are proud of, not something you&rsquo;ll hide.</p> </blockquote> <p><img src="https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment/chatlog.jpg" width="1080" height="1390" srcset="https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment/chatlog_hu848875f24bd82076bd50572759b85e2d_169088_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2020-01-31-bsv-beijing-touching-moment/chatlog_hu848875f24bd82076bd50572759b85e2d_169088_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="截图" class="gallery-image" data-flex-grow="77" data-flex-basis="186px" ></p> <p>2020年01月31日,因为新冠病毒肆虐无法外出,我在 <a class="link" href="https://youtu.be/mqWm6Kwf0KY" target="_blank" rel="noopener" >Youtube</a> 找到了那天炉边谈话的视频,翻到了最后,摘下了博士所说的全文。</p> <blockquote> <p>I want to see you all create something that makes you wealthy. not get rich quickly. Wealth. Wealth is not just money, wealth is goods, wealth is services, wealth is things we consume, wealth is the education we give our children, wealth is the education we give ourselves. Wealth is holidays, being able to take time off, have a new car, go to a dentist, whatever. I want you to create a system that will make you wealthy, not because you hodl coins that someone else making you wealthy. I want you to build a system you can be proud of. I want you to build on top of Bitcoin and be wealthy because of your hard work. So that your family, your kids will look at you and go, &ldquo;I&rsquo;m proud. That&rsquo;s my dad. I&rsquo;m proud. That&rsquo;s my mum. I&rsquo;m proud. That&rsquo;s my brother. I&rsquo;m proud. That&rsquo;s my sister, my son, my daughter, etc.&rdquo;. I want you to build a system with those who love you do it. not because you were lucky but because you worked, and you strove, and you created something that made the world proud.</p> </blockquote> <p>我简单译了一下,辞不达意,还请见谅。</p> <blockquote> <p>我希望能看到你们通过创造价值而 (变得) 富有,而不是 (因) 投机赚到快钱 (而自得)。财富。财富,不只是钱,财富是产品,是服务,是可供消费之物,是可给予孩子的教育,是可给予自己的教育。财富是 (能稍事歇息的) 假日,(工作之余) 休憩的时光,(为家人购置的) 崭新的汽车,(开始有余力照顾自己而) 预约的牙医,所有这些。 我希望你能有机会去构造一个系统 (小则项目,大则事业) 而变得富有,而不是只会牢牢攥紧手里的币 (所谓的 HODL),靠掠夺那些后来者的血汗而暴富。我希望能看到你为自己构造的系统而骄傲,我希望能看到你在 Bitcoin 的基础上,通过艰辛的努力而有所收获。只有如此,你的家庭,你的孩子,在提到你的时候才会说,“这就是那个让我骄傲到不行的老爹/老妈/老哥/老姐。” 为这些在乎你的人去工作,不靠运气,靠努力,挣扎,奋斗,去创造那个让整个世界脱帽致敬的东西。</p> </blockquote> <p>不知道其他人听到后什么感受,这是一段令我非常动容的话。</p> <p>谨记于此,深自勉之。</p> 2020.01 《嵇康之死》小记 https://gulu-dev.com/post/2020-01-26-ji-kang/ Sun, 26 Jan 2020 00:00:00 +0000 https://gulu-dev.com/post/2020-01-26-ji-kang/ <p>阅读 《嵇康之死》 小记。</p> <p><img src="https://gulu-dev.com/content/post/2020/2020-01-26-ji-kang/title.jpg" loading="lazy" alt="ji-kang" ></p> <h3 id="气质音乐人格魅力">气质,音乐,人格魅力</h3> <ul> <li>引1:“甚至有人指出,中国古代诗人真称得上阳刚者,只有一个嵇康,其余如李白、苏轼、辛弃疾等人的阳刚豪放,都是为了要做文章而装出来的,而嵇康的阳刚是<strong>从内心深处投射出来的光明与正大</strong>,是天生的豪迈之气,是与万物并生、天地为一的精神。”</li> <li>引2:“在这篇文章(《琴赋》)的序中,嵇康指出了一个音乐史上的重要现象。他说,关于器乐歌舞,历代文人都曾对之进行过文学描写,但 <strong>“称其材干,则以危苦为上,赋其声音,则以悲哀为主,美其感化,则以垂涕为贵”</strong>,这就是说,除去文学描写,从音乐的角度来看,大多数优秀的乐曲都是情调悲伤的。嵇康说这话的时间大约是在三世纪中期。公元2000年,音乐家小泽征尔、作家大江健三郎谈话时也谈到,世界上大概有百分之九十的乐曲都是悲伤黯淡的,因为人生的基调就是悲伤黯淡的,也许,对于寂寞的人生来说,正因为有了这个悲伤的基调,喜悦才有意义。嵇康在一千七百五十年前就指出了这一点,说明他对音乐有真实的感悟,是一个真正的音乐家。”</li> <li>引3:“从统治者的角度来看,在那样的时代,真正能威胁到皇权专制绝对权威的力量,正是来自那些通达世理、理智健全且具有文化号召力,尤其还具有强大的人格魅力的不合作者,而特别不能容忍的是,嵇康这样一个引人注目的优秀人物<strong>非但采取了不合作的政治态度,甚至还给出了一种实实在在可以践行的生活方式</strong>,哪怕在旁人看来这种生活方式可能不免艰辛苦难,但那毕竟是能够借以独立于权势的一统天下之外、维护自己独立人格与自由心灵的可靠生存态度。如果天下士人尽皆如此,势将不成其为势,体制将不成其为体制,看似不合作的疏离,看似无害的平静生活,实则构成了对于权势独大的致命威胁。司马昭之杀嵇康,正是道势之争到此地步时必有之举。”</li> </ul> <h3 id="真正理解嵇康的人李清照">真正理解嵇康的人:李清照</h3> <p>不过,真正理解嵇康内心世界的人,应该是公元十二世纪初期的女诗人李清照。生当宋室南渡之际的李清照,身历颠沛流离之苦,对于个人在社会机制失效、战争与动乱降临中的渺小与无助、艰难与痛苦感触甚深,这时候,她眼中的历史就显得与众不同了。她有一首《咏史》诗说:</p> <blockquote> <p>两汉本继绍,新室如赘疣。<br> 所以嵇中散,至死薄殷周。</p> </blockquote> <p>这首只有二十个字的诗很难读懂,虽然向来说“诗无达诂”,但这首诗未免让人猜测诗人不可明说的难言之隐。</p> <p>也许,我在这里的理解是有些许近似原意的:嵇康自称他“非汤武而薄周孔”,大多数后人着眼于他后半句自述所包含的思想上对于孔子儒家学说或儒教理论的不屑和批评态度,但是“非汤武”到底何所指却无人关注。李清照读史至两汉时代,她认为西汉、东汉之间的新莽王朝虽然号称锐意革故鼎新,创建也不能说少,但其结果是适足以淆乱天下,而两汉之间,经济、文化制度自然续接传承,新莽一朝夹处其间实为赘疣,于社会有害无益,名为建树,实则祸害天下。由此想到嵇康之所以“非汤武”,则是认为向来为文人所乐道的商汤周武革命,文化上、制度上虽说是多有建树,允为后世文明所祖述,亦历来为文人所称颂,然而华夏自古就有传承有绪的文化传统,殷、周两代所谓革故鼎新,只是不惜为新利益集团服务而祸害天下之举,与新莽一朝一样,若以人类生存的目的就是追求个人幸福的视角来看,于后世有害无益,所以尤为嵇康所菲薄。</p> <p>由李清照所理解的嵇康思想中包含的对中国文化这种颠覆性的思考,真可谓惊世骇俗,石破天惊。或者,我们据此可以说,嵇康在政治上可能就是一个无政府主义者,一个重视个人价值、重视个人生存意义、重视个人生命的幸福感而鄙薄一切以政治正确的借口来压榨、剥夺、欺凌、侮辱个人价值与个人尊严的自由主义思想者。虽然我自己都不能完全认同我从李清照诗中读出来的她对于嵇康思想的理解,但却不得不说,李清照这首诗是嵇康死后至今的一千七百五十多年间,对他的精神境界最深刻的认识。</p> <p>这样一个人,如果不被他生活其中的皇权专制社会独裁者所杀掉,那才是非常奇怪的事。</p> <h3 id="小注">小注</h3> <ul> <li>书中有钟会和司马昭诛杀嵇康的前因后果,真实的历史比虚构的小说更传奇。</li> <li>竹林七贤中,嵇康,阮籍,向秀有学术成就,其他四个只是充数,为了让人们可以政治正确地讨论嵇康。</li> <li>向秀注庄子很丰富,但已失传,后被郭象所窃。详可见张远山的《庄子传》《庄子奥义》等书。</li> </ul> 2019.12 为什么这么多人讨厌 Bitcoin SV? https://gulu-dev.com/post/2019-12-29-why-hate-bsv/ Sun, 29 Dec 2019 00:00:00 +0000 https://gulu-dev.com/post/2019-12-29-why-hate-bsv/ <img src="proxy.php?url=https://gulu-dev.com/post/2019-12-29-why-hate-bsv/bsv.png" alt="Featured image of post 2019.12 为什么这么多人讨厌 Bitcoin SV?" /><p>这是一篇翻译文章。听过路总大过滤器理论的同学,看了这一篇恐怕都会心一笑。此篇网上已经有了翻译,不过看起来机翻占比多了些,而且一些关键点上需要点技术背景才能表达出真意。</p> <p>今晚正好有空,(在彩云小译的基础上) 我动手翻译了下,让全文的词句更通顺与合理,并增加一部分注解,把一些关键点解释得更清晰明白。</p> <p>在文末,我添加了完整的逻辑链,供参考。</p> <ul> <li><a class="link" href="https://personacryptona.com/essays/why-do-so-many-people-hate-bitcoin-sv" target="_blank" rel="noopener" >原文链接: https://personacryptona.com/essays/why-do-so-many-people-hate-bitcoin-sv</a> (2021-03-11 注: 该链接已失效)</li> </ul> <p>这篇文章的作者写了一系列散文,都很有趣,有机会我再把系列翻译出来。</p> <hr> <h2 id="原文译">原文译</h2> <p>学生: 今天你能回答我的问题吗? 我想知道为什么其他加密货币世界如此痛恨比特币 SV。<br> 师父: 我会的。 但是,正如我昨天告诉你的那样,这个答案对你没有帮助。</p> <p>学生: 为什么?<br> 师父: 因为这不是你真正的问题。</p> <p>学生: 我真正的问题是什么?<br> 师父: 你其实只是想弄明白,怎么才能说服加密货币世界里的其他人,让他们把比特币 SV 当回事。</p> <p>学生: 我想是这样的,你能回答这个问题吗?<br> 师父: 我可以。但是你不会满意这个答案的。</p> <p>学生: 为什么? 答案是什么?<br> 师父: 这是不可能的,除非你打算摧毁 BSV 这个项目。</p> <p>学生: 但是其他的项目,看起来大多也挺像回事的呀。<br> 师父: 要理解我为什么这么讲,你必须认识到,他们正在嘲笑比特币 SV 的点是什么。</p> <p>学生: 他们在嘲笑什么?<br> 师父: 当你在星期六晚上去洛杉矶新开的夜店时,你看到了什么?</p> <p>你看到一群年轻人都在费尽心思去给其他人留下深刻印象。 他们穿着他们能弄到的最好的衣服,最好的首饰,300美元的发型,限量版的手表,时髦的皮鞋。这儿的游戏规则是: 谁看起来最有钱,谁就会赢得最多的关注、尊重和赞赏。 他们的目的大多只是靠这个去吸引一个性伙伴。</p> <p>夜店里的每个人都会去装模作样地展示自己最好的一面。 但有一个事实没有人能够看到。 这是一个可以用进化论完美诠释的游戏。那些衣着打扮最入时最精致,在(对生存本身毫无价值的)外表上大笔花费的人,要么本来就在日常生活中掌控着更多的资源,要么就有一个出众的手艺,或一门日进斗金的生意。很显然,这样的人是夜店里最受欢迎的人,是这个场合里最佳的求偶对象。</p> <p>学生: 你这么说起来感觉有点好笑。 但是,是的,我想这就是正在发生的事情。<br> 师父: 但是这里面有一个终极的谎言(或者说游戏),没人能轻易揭示谜底。 即使是最训练有素的眼睛也看不出来,穿着昂贵衣服的人,是真的拥有很多资源,还是说他们只是把所有的钱都拿来打扮自己的外表,却压根没考虑过如何成为一个好伴侣或好父母。 这就是为什么这个游戏并不容易玩。人们只能不断地发出信号,并去寻找匹配或不匹配的信号。</p> <p>但是,有这么个人,他总是会赢。</p> <p>学生: 谁?<br> 师父: 一个不修边幅,没啥打扮,看起来连头发都是自己剪的男人,穿着运动鞋,牛仔裤,皱巴巴的衬衫。 他会有什么故事?</p> <p>学生: 我猜他可能是穷得叮当响,或者也可能是太有钱了,不需要什么打扮去讨好别人。<br> 师父: 是这么回事。 但我们假设,这个人也想去吸引一个玩伴。 他为什么不在衣服上花点心思呢?</p> <p>学生: 我不明白。<br> 师父: 这个人穿着休闲装来夜店,其他人要想一下子想到你上面说的原因,还是需要点智商的。 这个人只想吸引最聪明的配偶,这样自然就会有聪明的后代。</p> <p>学生: 所以你的意思是,那些只靠第一眼就断定他很穷的女人,显然聪明不到哪儿去,自然就被自动地排除在了他的潜在考虑范围之外。<br> 师父: 没错。</p> <p>学生: 好吧,但是要是大家都只不过觉得他很穷,没人搭理他怎么办?<br> 师父: 如果他真的很穷,那么什么也不会发生。 如果他是刻意为之,那么总有一个精明的观察者,最终会捕捉到一个没法伪造的信号。</p> <p>学生: 啥信号?<br> 师父: 有一个非常聪明的女人站在远处,看着这个男人,想弄明白,为什么他穿个马马虎虎就跑到夜店来,而不是像其他男人和女人那样衣着华丽。 她看了他一会儿,看着他点了一杯苏打水,想知道为什么他不像那些夜店主角 (这里原文是 &ldquo;the supposed ballers&rdquo;,就是对球赛有控制力的大牌,通常是球队的核心,这里泛指夜店里最受欢迎那类人) 那样去开一瓶高档香槟。 他要么对喝酒没有兴趣,要么就是买不起而已。 她仍然很好奇,但不太确定。</p> <p>然后她看到了信号。 这个人没抬头,只是低头喝着水时,一个著名的,非常有钱的生意人,走过来跟他握手,坐在他旁边。 然后,一个一线电影明星也走到这个男人面前,坐到他旁边跟他攀谈起来。显然,那个穿着随意的男人是这里的主角。 整个晚上,她都在观察这个男人。 那些无名小卒对这个人不屑一顾,而跟他坐在一起的人却是俱乐部里最有钱最有名的人。 她仍然不知道这个男人是谁,但是她看得出来,坐过去的大佬显然知道一些她不知道的事情。 她问身边其他女人对他的看法,而她们一开始看到他的衣服时就“取消关注”了。</p> <p>她决定去向他作自我介绍。 她诚实地承认,并不知道他是谁,但是她很好奇。 他知道,她看得出他没必要穿最昂贵的衣服,自然,她比俱乐部里的大多数人都聪明。</p> <p>学生: 嗯,所以你想说的是,那些需要给别人留下深刻印象的人,就得穿着时髦的衣服。而在这种场合随便穿的人,要么是穷人,要么非常富有。<br> 师父: 百万富翁穿西装。 亿万富翁们穿短裤。 新晋律师每天穿着最好的西装去上班。 公司老板穿什么?</p> <p>学生: 我不知道。<br> 师父: 随便穿什么都行。</p> <p>学生: 是这么回事。 但我不明白,这怎么能解释为什么人们讨厌比特币 SV。<br> 师父: 我不是在回答这个问题,我是在回答你真正的问题。</p> <p>学生: 你是说,如何说服别人尊重比特币 SV。<br> 师父: 是的。</p> <p>学生: 没明白。 怎样才能做到这一点?<br> 师父: 现在唯一能做到这一点的方法,就是“穿上最时髦的衣服”,去赢得大众的支持。 但这是一场注定失败的战斗。 每个新出来的山寨币,都有“漂亮的衣服”。 花哨的服装比赛是一场 “比谁更low” 的竞赛 (the race to the bottom)。 这是一场不可能赢的比赛。 不管你穿什么,别人都会模仿。 其他人没法把这个当成有用的信号,去发现真正的价值。 骗子都穿着华丽的衣服。</p> <p>学生: 嗯,请您接着说,师父。<br> 师父: 但是,有一个更值得玩的游戏。 穿日常的衣服就行。 忘掉最新的流行语和时髦的金融科技废话,去创作那些在现实世界中真正起作用的东西,吸引那些真正聪明,能洞察这一切是如何运作的人。</p> <p>学生: 但是我们最终还是得需要得到大众的认可吧。<br> 师父: 的确如此。 在刚那个类比里,夜店里的人,都是对加密货币感兴趣的人。 吸引这些人,对比特币 SV 而言太小了。 <em><code>夜店之外,有数十亿人最终需要用到比特币 SV。</code></em> 为了触及他们,我们只需要吸引这个夜店的所有人中,最聪明的那些人。要是穿着花哨的衣服,或者专注于在夜店攒名声,是无法引起他们的注意的。</p> <p>学生: 我明白了。 所以你是在说,现在的各种币,只不过是在挖空心思讨好夜店里的这些人,而这些人实际上已经在加密货币圈子里了。<br> 师父: 是这样。</p> <p>学生: 但是这只是一个小圈子里的游戏,而比特币 SV 正在玩一场更大的游戏?<br> 师父: 没错。 我们可以做需要做的一切,为了赢得夜店里的“声望”。 我们不再尝试去做真正意义上的普及 (注: maximalism, 即追求 Bitcoin 作为一个网络的效用最大化),我们可以加重放保护,我们可以说说他们项目的好话,跟他们友好相处 (注: 即所谓的 community-friendly/socialism),我们可以搞搞 POS,不断去谈论 DeFi (所谓的去中心化金融),弄一个本质上是庞氏的类似 DAI 这样的稳定币,同意那些所谓的专家的“不可能在链上扩容”的观点,去“集成”那些花样繁多的代币 (注: 这里的 inflating 不是通胀之意,是说这类代币越搞越多),以及其他一打事情,就像那些“热点”项目一样。</p> <p>学生: 但这样的话,我们就永远也没法接触到夜店之外的真正大众了。<br> 师父: 你开始明白了。 这个项目太重要了,不能以这么“小”的方式思考。 环顾一下比特币 SV 项目,你会发现,相对于其市值而言,比特币 SV 项目上正在建设的项目比其他任何项目都要多。 这是因为我们吸引了所有的建设者,聪明人。 我们知道,真正意义上的大众不会因为某个币而来,只会因为这些能带来价值的项目而来。当你想在墙上挂一幅画时,你会买锤子和钉子,你真正想要的不是锤子或钉子,你是想把这幅画挂在墙上。</p> <p>学生: 我明白了。 所以你是说,我们必须首先吸引那些能制造出人们真正想要的东西的建设者。<br> 师父: 如果你想吸引大众,你必须能够吸引开发者和建设者。 所有其他的币,特别是 BTC,都有能力吸引投机者和赌徒。他们总是穿着漂亮的衣服在夜店里炫耀,但是他们也许实际上只不过住在一个破烂的公寓里,坐公共汽车赶到夜店。</p> <p>学生: 好的。那现在请你回答我的第一个问题:为什么加密货币圈子里的那些人如此痛恨 BSV?<br> 师父: 有两个原因。</p> <p>第一个原因是这个:BSV 是对他们的威胁。 如果 BSV 成功了,它必然会从其他币那里夺走价值。 如果你拥有 BTC,并且 BSV 的扩容实际上可行 (正如 Satoshi 最初在多年前所说的那样) ,那么你的 BTC 的购买力将下降。 那些不管什么原因梭了 BTC 的人中,有许多人都知道,BSV 的成功对他们的财富是一种生存性的威胁。</p> <p>学生: 他们可不这么说。 他们说 BSV 只不过是一个早晚会消亡的笑话。<br> 师父: 为什么他们不这样说&quot;歌币&quot; (SongCoin) 呢?</p> <p>学生: &ldquo;歌币&rdquo; 又是个啥?<br> 师父: 以前的一个已经失败,最终归零的项目。</p> <p>学生: 我从没听说过。<br> 师父: 那些 BTC 的鼓吹者们是如此地关注 BSV,而不是所有那些事实上的确失败了的项目,是有充分理由的。 如果 BSV 的确是一个笑话,就像&quot;歌币&quot;一样,他们压根彻底就不会关注这个币。</p> <p>学生: 的确如此,他们谈论 BSV,比谈论他们自己的东西多多了。<br> 师父: 嗯。</p> <p>学生: 好吧,那第二个原因是什么?<br> 师父: 由于 BTC 对于真实世界的实际商业毫无用处,这个币的投资者实际上什么也干不了,结果他们就发展了一种新的爱好。</p> <p>学生: 什么爱好?<br> 师父: 一种通过在社交媒体上反复谈论 BTC 来相互较量的游戏。</p> <p>学生: 你是说真的吗?<br> 师父: 除了这个,拿着 BTC,其他也没什么事好干了。 他们都知道这点。 他们只是坐在那里等待他们的投资价值上升。 因为你不能通过 BTC 建立真正的业务来赚钱,也没法玩“通过竞争去增加自己的持币比例”这样的更有逻辑的游戏,于是他们只能去玩一个谁能得到最多关注的游戏。</p> <p>学生: 怎样才能得到最多的关注?<br> 师父: 为了得到 BTC 粉丝最多的关注,你得弄清楚每个人在想什么,或者他们期望的未来是什么样子,然后把这些信息换种花样,再成功地兜售回给他们。 如果你能使用一个以前从来没有被用到的表达方式,甚至是用上了数学,或者什么看起来高大上的图表,就会得到额外的分数。 这里的每个人都会去追随那些“善于说出别人想听到的话”的人。 如果你曾说过一些人们不喜欢的话,即使是真的,你也会被降级,取消关注,封锁,或者彻底的鞭挞。 这场注意力游戏你就输了。</p> <p>学生: 你确定是这么回事吗?<br> 师父: 去看看那些 BTC 上的开发人员吧。 看看他们有多少粉丝。 这些是 BTC 社区声称的最聪明的人。 然而,他们并不是最受关注和被聆听的。 注意,我并不是说我认可他们。 许多高效的开发人员或多或少地处于默默无闻的状态。 与此同时,那些从来没有创造过任何有用的东西,从来没有经营过一家盈利的公司,持有的 BTC 很少,却有着成千上万的追随者的人,却对项目的方向有很大的影响力。 之所以会发生这种情况,是因为最初参与 BTC 的人们,那些真正想为世界创造有用的东西的人们,想要讨论和解决实际的问题的人,对玩这个 (谁能获得最多的关注者) 这个新游戏不感兴趣,他们都走了。这些人的离开产生了一个真空,这个真空很快就被那些对任何事情都一无所知的低价值人群填补了。 他们每年都做出荒谬的价格预测,一次又一次地出错,然而每个人都关注他们,因为他们对真相兴趣不大,他们只是希望别人告诉他们,(因为他们持有 BTC) 未来他们会混得不错。</p> <p>学生: 这么说别人是低价值个体 (low value individuals) ,好像挺冒犯的吧。<br> 师父: 事实有时就是这么冒犯。</p> <p>(原文完)</p> <hr> <h2 id="简评为什么-bsv-是币圈公敌">简评:为什么 BSV 是币圈公敌?</h2> <p>这是个有意思的问题。人是社会性动物,在难以辨明对错 (或时间等资源不足) 的情况下,与大多数人站在一起,选择同样的立场与行动,让群体中的个体感到有安全感。研究大众的选择,我们只需要知道所谓的意见领袖,行业权威,矿工和开发者,他们为什么会普遍性地敌视 BSV,就可以弄清楚真相了。</p> <p>我们来把文中由学生逐步发问引出的逻辑展开捋顺,梳理一下,让逻辑链更清晰和连贯。</p> <p>在 Bitcoin 过去的 10 年发展里,除了围绕 Bitcoin Core 的主要开发者和以开源协作方式参与的外部开发者,我们见证了一波又一波的一般投资者和各路投机分子,以各种不同的身份涌入这个系统。这些人除了引入更多的资金,推高比特币作为投机资产的总市值之外,对 Bitcoin 的全球采用 (Global Adoption) 没有任何实质性的帮助。我们也看到,除了 Bitcoin 之外,最大的区块链项目以太坊,本来的期望是通过所谓的智能合约,构造通用的计算模型,来塑造全球下一代的商业模型,结果唯一被广泛应用的就是 token issuing 被用来作为上面提到的参与者的快速融资和套现的工具。</p> <p>Bitcoin 作为电子现金的开创性和革命性的特征,并未随着总市值的增长真正展现出来。在多年的开发之后,Bitcoin 仍然只是一个玩具,没有被现实的商业及互联网项目所广泛采用。由于停留在玩具阶段,大部分 Bitcoin 的利益相关者,选择并强化了一种对他们有利的叙事:<code>**Bitcoin 是价值储存 (Store of value)**</code>。这个叙事抛弃了 Bitcoin 作为电子现金的叙事 (被认为是不可行,或 “非常困难无法实现”),本质上,这仍然反映了极早期时 BM 对 Satoshi 的质疑,也成为了人们的普遍质疑:Bitcoin 是否永远长不大,是否处理能力只有这么一点,是否只能作为一个结算网络 (Settlement Network) ,小微支付是否只能交由闪电网络这类链下扩容 (Off-chain Scaling) 方案,如此种种。</p> <p>而与其他项目大有不同的是,BSV 的选择是:<code>**坚决而毫不妥协的链上扩容**</code>。通过这一点,使得 Bitcoin 重新变得有机会成为真正的价值网络。与此同时,BSV 的发起人及利益相关者,通过一系列精心选择的立场表态,和刻意的反社交媒体的行动,来过滤掉那些对 <code>**“比特币(作为一项科技)被全球采用”**</code> 这一愿景缺乏兴趣的一般投资者和投机分子。这个过滤比它乍看上去要重要得多,我们知道,Satoshi 在离开时指定了 Gavin 作为接班人,而 Gavin 正是为了所谓的政治正确,一下子引入了太多的异见者,形成了新的权力漩涡,很轻易地就在政治斗争中丢失了最重要的 Bitcoin 领路人角色。</p> <p>Bitcoin SV 不会也不能再重复这一错误。</p> <p>Fortunately, this time, it should be in the right hands.</p> <hr> <ul> <li>历史 <ul> <li><code>2021-03-11</code> 迁移到新 blog,并对不通顺的字句做了修正</li> <li><code>2020-06-16</code> 新增编号并入库</li> <li><code>2019-12-29</code> 初版</li> </ul> </li> </ul> 2019.10 比特币的量子抵抗 https://gulu-dev.com/post/2019-10-28-bitcoin-quantum-resistance/ Mon, 28 Oct 2019 22:00:00 +0000 https://gulu-dev.com/post/2019-10-28-bitcoin-quantum-resistance/ <img src="proxy.php?url=https://gulu-dev.com/post/2019-10-28-bitcoin-quantum-resistance/bitcoin-qr.jpg" alt="Featured image of post 2019.10 比特币的量子抵抗" /><p>前两天刷屏的 Google 量子计算,已被证实其新闻效应远大于实质的技术突破(breakthrough)。目前所能知道的信息是,实际上远未很好地处理解决量子退相干(Quantum Decoherence)效应。我们距离可以执行传统计算的量子计算机还有很远的路要走。但思考一下比特币的量子抵抗能力,仍是有意义的。</p> <h3 id="量子抵抗">量子抵抗</h3> <p>看了下量子计算抵抗 (quantum-resistant, QR),得出了以下几点 (待补充)</p> <ol> <li>从公钥弄出私钥是可能最先被破解的,但是到主公钥 (xpub) 本身信息量不足,目前可能性相对较低,到 seed 可能性也较低。</li> <li>一旦有了主公钥,再配合任意一个单个破解的私钥,可以推导出这个 xpub 下的所有私钥,这样虽然没得到 seed 效果也差不多了。</li> <li>更换新的 QR 签名算法的话,如果使用目前已知较好的 Lamport signature 每个签名至少长达若干 KB (目前长度的 40-170 倍),所以不先充分扩容就不用谈了。</li> </ol> <h3 id="具体的安全实践">具体的安全实践</h3> <ol> <li>每个地址只使用一次,把公钥破解的性价比降到最低。</li> <li>对于大量的币,尽量均匀切分开,每个地址只存少量的币。</li> <li>不要在任何场合暴露自己的主公钥 (xpub) 一些离线冷钱包告诉你,可以在在线钱包里导入 xpub 来查看资金变动情况,不要这么做,如果你需要查询余额,直接查看具体的地址即可,不要在联网的机器上输入你的 xpub。</li> </ol> <h3 id="参考">参考</h3> <ol> <li><a class="link" href="https://en.bitcoin.it/wiki/Quantum_computing_and_Bitcoin" target="_blank" rel="noopener" >https://en.bitcoin.it/wiki/Quantum_computing_and_Bitcoin</a></li> <li><a class="link" href="https://bitcointalk.org/index.php?topic=5063390.0" target="_blank" rel="noopener" >https://bitcointalk.org/index.php?topic=5063390.0</a></li> <li><a class="link" href="https://en.wikipedia.org/wiki/Post-quantum_cryptography" target="_blank" rel="noopener" >https://en.wikipedia.org/wiki/Post-quantum_cryptography</a></li> <li><a class="link" href="https://en.wikipedia.org/wiki/Quantum_decoherence" target="_blank" rel="noopener" >https://en.wikipedia.org/wiki/Quantum_decoherence</a></li> </ol> 2019.09 区块链与游戏结合的再思考 https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/ Wed, 11 Sep 2019 00:00:00 +0000 https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/ <p>2015年2月份,我在知乎上回答了一个问题:<a class="link" href="https://www.zhihu.com/question/27621853/answer/40488719" target="_blank" rel="noopener" >如果使用电子加密貨幣來充當遊戲貨幣體系的一環,會對遊戲有什麽影響?</a> 限于我本人当时的认识,那篇文章只是浅尝辄止了一下各种可能性。在文章的“花式脑洞展览会”一节,我设想了几种理想化场景,如更丰富和方便的玩家自治机制,玩家自主发行游戏内货币,甚至于完全的链上去中心化游戏的开发和运营,等等。这些设想和目标,以当时眼光看颇有调侃之意,然而相比4年前,现如今已经有了更好的基础设施,一些脑洞也逐渐地变得可能。</p> <p>这一年中,我在区块链游戏上做了一些尝试和实践,也见证了链游在这一年间的激荡和起落。与4年前相比,区块链和游戏的结合这块,我也有了新的理解和认识。</p> <hr> <h2 id="什么不是区块链游戏">什么不是区块链游戏?</h2> <p>在继续探讨之前,我想先来聊聊——<strong>什么不是区块链游戏</strong>。</p> <p><img src="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-01.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-01_hu6cba53f35ce30819f1acec0faf5f6ad4_66711_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-01_hu6cba53f35ce30819f1acec0faf5f6ad4_66711_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="图一" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>且待我一一说来。</p> <h3 id="虚拟资产炒作不是区块链游戏">虚拟资产炒作不是区块链游戏</h3> <p>17年底,加密猫 (CryptoKitty) 火了一阵子。眼见一只虚拟猫咪卖出十二万美元的天价,不少人惊呼,区块链游戏的时代来了。但时间证明,这充其量只是一个典型得不能更典型的炒作泡沫。即使从投资和投机角度去看,也是一个空洞而单调的标的,跟游戏更是八杆子打不着的两回事。</p> <p>这也难怪,电子游戏之所以长盛不衰,要么是赋予了人在虚拟环境中沉浸,探索和体验的机会;要么是构建了人与人之间的有效互动——缺乏<strong>有效而持续的互动</strong>,也就失去了构成游戏的最关键因素。</p> <p>然而,从另一面看,自由市场和价格投机,的确是客观存在的强烈而持续的需求。那么是否可能建造一个游乐场,既能维系游戏内的长期而稳健的货币体系,又能提供足够的市场化机制来满足交易和投机的需求呢?</p> <p>我认为,只要游戏的体量和尺度足够大,提供的特征和玩法与真实世界足够匹配,这种内外循环就是有可能实现的。如同《西部世界》和《头号玩家》,在这一类游戏中,玩家的感官体验有着接近真实的可信度,可使用的道具有着足够多样化的用途,这些趋近于现实世界的体验,最终会沉淀到虚拟道具的价值上。这些具有足够沉淀价值的虚拟道具,构筑了多轮博弈和长期交易的坚实基础,自然能维持长期交易之上的投机价值。</p> <h3 id="传销和资金盘不是区块链游戏">[传]销和资金盘不是区块链游戏</h3> <p>从18年年中爆红的 fomo3d 开始,18年下半年和19年上半年,披着链游外皮的[传]销和资金盘似乎成了这个圈子里的主流。从一开始 fomo3d 对[传]销的改进(钥匙机制和终极激励)到后来某某链的各种模式,各路人马极尽所能,对传统的[传]销做了各种微创新,也部分地改善了这种庞氏骗局的健壮性。但事实证明,不管怎么改,这种强行植入的“共识”本质上极为脆弱,无论是黑客阻断交易顺利拿到大奖,还是某交易所砸盘+禁提币事件(交易所割项目方导致社区反弹),看似黑天鹅不断,实则都是失控边缘的偶然中的必然。套用一句话:“模式币的脆弱在于&hellip;所以如同风中蜡烛随便都可以倒掉,即便它曾经在历史给予的缝隙中精彩狂乱的表演,也很难避免最终坍塌的结果。”</p> <p>简单说,对[传]销和资金盘而言,游戏的这一层薄薄的外壳<strong>似有实无,不足为论</strong>。即使不站在道德与法制的高地上批判,单纯从项目角度就足以看清,这类项目十有八九难以逃脱“过把瘾就死”的命运。普通参与者只记住一条即可:无论以任何理由<strong>拉/让/劝</strong>你买币的行为,都是[传]销,就可以了。</p> <h3 id="菠博菜彩和抽水不是区块链游戏">菠[博]菜[彩]和抽水不是区块链游戏</h3> <p>从最早的 SatoshiDice 开始,在区块链上“公平地摇骰子”,一直是一个经久不衰的应用。这里我们说菠菜和抽水不是区块链游戏,似乎有点矫枉过正了,毕竟斗地主和麻将也流行了多年,并(看起来)会一直流行下去不是吗。</p> <p>抛开大部分国家对线上菠菜的强力监管不谈,这里单单讨论菠菜本身好了。千百年来,菠菜在无数次的多轮博弈下,早已形成了固定的套路。不要说古已有之的牌九或麻将,光说澳门的xx乐和xx机,这里面对概率和人性都有着千锤百炼的精妙平衡。不信你试试看,在不脱离已有的框架下,试着去发明一种规则和玩法。尝试下就会知道,规则上越是简单,越是有无尽的门道,越是难以去做所谓的“改进”,也越难发明新的有效玩法,这就是为什么你会看到 EOS 上摇了一年多骰子的上百种菠菜,到现在不管怎么改,都是还是离不开摇骰子的原因。</p> <p>这是一个相对封闭的体系,创新是不被鼓励的。(好不容易设计了套能躺赚的体系,在可以预见的将来都会持续稳定盈利,没事干嘛要瞎折腾)简单说,菠菜只欢迎真金白银,不欢迎(或至少是不鼓励)(不能提升盈利能力的)创新,这与游戏及泛互联网行业求新求变,强调设计,快速变更,产品迭代的理念,是有内在冲突的。</p> <hr> <h2 id="对链上游戏的再思考">对链上游戏的再思考</h2> <p>一口气说了这么多“什么不是链游”,那么有着真正区块链基因的下一代游戏,究竟应该长什么样呢?</p> <h3 id="虚拟道具的资产化和交易">虚拟道具的资产化和交易</h3> <p>先从我们熟知的虚拟道具(如屠龙宝刀)说起吧——利用类似 ERC721 这样的非同质代币机制,我们可以建立单个游戏角色/道具在链上的对应实体,更进一步,在链上映射任意规模的游戏内资产。</p> <p><img src="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-02.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-02_huc0a054a520eae8905dc0e9823b5aa49a_75258_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-02_huc0a054a520eae8905dc0e9823b5aa49a_75258_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="图二" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>然而,目前支持ERC721资产类型的交易所不多,交易量也很低,跟 5173 这样的传统交易平台还远无法相提并论。究其原因,归根结底是技术问题,具体说有三点:</p> <ol> <li>游戏道具映射到ERC721资产,有一定的双向技术门槛,需要理解游戏架构,才能为特定资产类型设计出<strong>既紧凑,又有足够灵活度与扩充空间</strong>的属性集。</li> <li>交易所支持ERC721作为独立标的物(类似炒鞋app或拍卖行),提供完善出价/拍卖/成交的机制,与现有的交易对机制有较大的架构区别,实现上有一定的技术成本。</li> <li>实现特定的与资产流通相关联的游戏逻辑,需要游戏开发者理解链上资产的属性,并有一定的合约编写能力(比如,如何定义可消耗物品,如何实现道具合成,能否实现倒计时交易 countdown transaction,条件化交易 conditional transaction,等等)</li> </ol> <p>这三点里,尤其重要而又要求较高是第三点。因为游戏并不仅仅是软件开发和功能实现,更是一个结合具体的玩家画像去做的持续化运营项目,而运营则需要足够的灵活度去配置各类活动。比如,假设本周五(2019-09-13)官方需要发一个中午 12:00-14:00 兑换月饼券的活动,就会需要在时间窗口内释放定量的游戏资产,以及相应的如何通过配置有效期,去引导二级市场流动性等问题。</p> <hr> <h3 id="非资产类数据重新由玩家所有和支配">非资产类数据重新由玩家所有和支配</h3> <p>上面我们讨论了游戏内的资产,而游戏内还有大量的非资产类数据,如玩家的分数/段位,成就/奖杯,任务,社交网络,等等,这些数据虽然不是可流通的资产,但对游戏的作用同样重要。对这一类数据,与区块链结合的价值在哪里呢?</p> <p>通过类似 MetaNet 这样的协议,我们可以将游戏数据的所有权和支配权还给玩家。</p> <p>在展开讨论前,可以先读一下这一篇:<a class="link" href="https://mp.weixin.qq.com/s/xzrc6tuWRmW65br8hqlh0Q" target="_blank" rel="noopener" >歌单,用户数据和 Metanet</a>。</p> <p>完整起见,这里我简单复述一下。现有的互联网上,所有用户的数据都围绕着单个互联网应用,不属于用户本人,而归创造这些应用的互联网公司所有。如你发过的朋友圈由微信所存储及掌管,难以导出。即使你能手动一条条导出,也会丢失评论和点赞等相关的数据。</p> <p><img src="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-03.jpg" width="859" height="572" srcset="https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-03_hu8c2e4741bcead3ab7bd66e2f95585cfa_51323_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2019-09-11-blockchain-game-rethink/blockchain-game-rethink-03_hu8c2e4741bcead3ab7bd66e2f95585cfa_51323_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="图三" class="gallery-image" data-flex-grow="150" data-flex-basis="360px" ></p> <p>关于互联网公司的隔离和信息孤岛效应,已经被越来越多的人所关注,而借助 metanet,可以做到在不影响商业公司数据访问权的前提下,让用户重新掌控自己的数据(正如掌控自己的币)。如果运用得当,这会带来惊人的能量释放。对于游戏而言,这种剧烈的能量甚至有望点燃新的希望之火~</p> <p>想想 Minecraft (我的世界) 中无穷无尽的玩家作品吧,如何与玩家一起挖掘这些作品中沉淀的商业价值,是一个值得思考的问题。这一点可以独立展开,后续再探讨。</p> <hr> <h3 id="下一代next-gen游戏的形态">下一代(Next-Gen)游戏的形态</h3> <p>游戏行业,是互联网的各种相关产业里,喜欢把“<strong>次世代</strong>”挂在嘴上的行业,没有之一。多年以来,大型游戏,一直是计算机图形学和实时渲染技术的第一推动力。看看每年的 GDC 就知道,无论何时,游戏行业对下一代游戏形态,永远充满着热切的憧憬与渴望。</p> <p>闲下来时,我偶尔会思考一个问题:<strong>跟《头号玩家》所塑造的&quot;绿洲&quot;世界相比,我们目前这一代游戏,到底还差了什么?</strong></p> <p>是逼真的渲染画面吗?</p> <p>不太像。现有的顶级游戏画面,并不比你在电影中看到的虚拟世界差。过去两年中,已经有越来越多的商业电影使用游戏引擎制作,这意味着你在游戏里看到的渲染结果,在某些情况下,已经能与所谓的“电影级画质”相提并论。对画质去做改善的边际效应已经比较低,即使再逼真10倍,带来的临境感(Presence)也并不会提高太多。</p> <p>是真实的 AI 或角色行为吗?</p> <p>也许吧。但即使是再逼真的 AI 角色,也充其量只是增加了游戏的 Environmental Reality (环境真实感),而这同样是边际效应递减的。</p> <p>此话怎讲呢?不可否认,《头号玩家》里丰富而逼真的,最激烈最极致的体验,仍是来自于<strong>玩家与玩家之间的对抗</strong>。而《西部世界》里,NPC 也是因为具有了某种人的内核(所谓的“觉醒”)故事才开始变得引人入胜。</p> <p>事实上,在变成所谓的“超智能体”前,不管 AI 做了什么大幅度的改进与优化,对玩家而言,大概率都跟现有的 NPC 没啥本质的不同。当你想完成任务的时候,不管你面对的是那个只会说一句“时间就是金钱,我的朋友”的地精 NPC,还是会撒娇卖萌的小爱同学,恐怕大概率都是瞅一眼任务目标和任务奖励就匆匆转身离去。而如果真正有了所谓的“超智能体”,那恐怕我们的生活本身就已经发生了翻天覆地的变化——搞不好我们自己都已经是 secondary species (次级物种)了都说不定。到那时,别说把人家做成游戏里的角色了,搞不好人家要把你当成 NPC 了~</p> <p>这就是关于游戏 AI 的终极悖论:<strong>小打小闹吧,你永远是个 NPC,玩真的吧,搞不好一不小心就玩脱了。</strong></p> <hr> <p>说回上面的题目,我认为,跟《头号玩家》里的“绿洲”所营造出来的拟真游戏世界相比,当前世代的游戏所塑造的虚拟世界,最缺乏的,是<strong>与现实世界互相融合的坚实可靠的价值基础</strong>。</p> <p>你不会真的把游戏里的金币(包括钻石)当成有价值的东西,对吧?</p> <p>还记得《头号玩家》里大家在战场上厮杀时,都不忘捡起对方掉落的金币这一幕么,这个游戏中的行为是如此的自然,真实和可信,以至于跟你在微信里收到红包时的心理感受,没什么太大区别。在“绿洲”里,虚拟世界与真实世界的相互融合,为参与者提供了连贯且一致的价值传导。这个传导越可信,越即时,越顺畅,虚拟世界与真实世界的联系也就越紧密。</p> <p>当你在游戏里斩杀了一条恶龙,与(在现实世界里)完成一场大师级钢琴演奏,踢了一场冠军联赛,可以获得相仿的价值回报时,虚拟和现实之间的墙才会被打破,两者的界限才会开始变得模糊。</p> <p>那么问题来了,为什么是区块链技术(以及在此之上的 metanet 协议),而不是某个商业公司,能提供这种坚实的价值传导呢?这就回到了话题的开端——因为商业公司有足够的利益动机,阻断这种价值流通,把你留在他的产品里,排他性地为其贡献<strong>你作为一个活跃用户</strong>的数据和流量。</p> <p>而在由区块链支撑的游戏世界里,通过 metanet 协议,我们得以重新让用户像掌控和支配自己的币那样,重新掌控自己的虚拟物品和个人游戏数据,而不是被游戏公司所左右(一旦停服,游戏里的一切都灰飞烟灭,从此长眠于某个不会再被访问的数据库压缩档案里)。只要你足够有耐心,甚至可以收藏一个上古游戏的初代角色,等游戏公司倒闭很多年之后,再去古董市场上出售,获得超额回报。</p> <hr> <p>玩家真正意义上“拥有”并可以“处置”自己的虚拟物品及数据,会带来其他一些更为深远的影响——通过游戏来赚取财富或声望,将不再是某一小部分人的专利,而将成为现实人生规划和成就的一个很好的补充。正如去年已在美国西弗吉尼亚州实施的基于区块链的全民选举那样,区块链上的电子竞技,其影响力,规模,商业价值,也许会轻松超越真实世界的奥运会,极低成本地做到全民参赛并完成自动完税的奖励发放,这是真实世界里的结算系统所难以想象的。</p> <p>区块链上的虚拟世界,才刚刚开始。</p> 2019.07 链上运算:从 ETH 到 BSV https://gulu-dev.com/post/2019-07-07-on-chain-computing-evolving-from-eth-to-bsv/ Sun, 07 Jul 2019 00:00:00 +0000 https://gulu-dev.com/post/2019-07-07-on-chain-computing-evolving-from-eth-to-bsv/ <img src="proxy.php?url=https://gulu-dev.com/post/2019-07-07-on-chain-computing-evolving-from-eth-to-bsv/onchain-computing.jpg" alt="Featured image of post 2019.07 链上运算:从 ETH 到 BSV" /><ul> <li>2022-06-05 [按] 此文写于三年前,那时我对 BSV 报有极大的期望。以三年来的发展情况看,BSV 的开发大大落后于预期,而 ETH 生态则愈加蓬勃兴旺。鉴之,鉴之。</li> </ul> <hr> <h2 id="简短说明">简短说明</h2> <ul> <li>这是一篇较浅显的关于链上运算模型的文章,直接来源见:<a class="link" href="https://www.yours.org/content/blockchain-computing-on-ethereum-and-bitcoin-sv-57371d4d5297" target="_blank" rel="noopener" >Blockchain Computing on Ethereum and Bitcoin SV</a></li> <li>本文<strong>并非原文直译</strong>,未免误导之嫌,请读原文为宜</li> <li>本文 (出于个人需要) 若干处做了个人化的内容补充 (通常会标注 「GL-Note」)</li> <li>本文提纲 <ol> <li>链上运算模型 (ETH)</li> <li>链上运算模型 (BSV)</li> <li>新模型的关键点是什么?</li> <li>ETH 与 BSV 的模型对比</li> <li>(使用新模型) 构建去中心化超算</li> <li>个人化的一些零碎的思考</li> </ol> </li> </ul> <hr> <h2 id="1-以太坊的链上运算模型">1. 以太坊的链上运算模型</h2> <p>众所周知,以太坊设计目标是成为一台“世界计算机” (World Computer),能够进行交易以外的运算。关于这一点,人们常这么去比喻——跟以太坊的计算能力相比,比特币就好像是个计算器,只有一些预定义的简单运算。</p> <p>那么以太坊是如何实现链上通用计算的呢?</p> <p>在以太坊上,人们使用 (不那么成熟的) Solidity 去实现计算逻辑,来保证与以太坊的共识相兼容。写好的代码通过一笔交易提交到链上,就成了一份所谓的“智能合约”。当这份合约被执行 (矿工将其写入区块) 时,所有的节点都需要执行并验证这份合约 (来确保有效性及一致性)。ETH 通过引入 Gas 来定义操作的运算量 (computing power) 与消耗费用 (fee) 之间的关系。</p> <p>很明显,这个思路的扩展性是有限的。</p> <ul> <li>整个网络的处理能力取决于网络中最弱的节点。 ( 「GL-Note」 此处存疑,没有验证能力或验证效率过低的节点实际上并不会影响到全网的整体推进)</li> <li>整个区块链上堆积着主网上线之后所有的运算,任何新加入网络的节点都需要从头同步,并 (仅出于历史原因) 完整地执行所有这些运算。</li> </ul> <p>以太坊的节点同步缓慢而且运算量巨大,全网的运算能力基本已达瓶颈,并且大大限制了可执行操作的范围。在这种情况下,不管是所谓“链上超算 (supercomputer onchain)” 还是 GB 级别的数据处理,目前看是不现实的。现有的方案有三: 1) 把复杂运算从合约挪到 DApps 的具体实现里,2) 寄望于 Plasma 这样的侧链, 3) 把操作及结果隐藏到状态通道 (State Channels, 类似闪电网络的支付通道) 里。</p> <p>即使有这些问题,以“智能合约”方式实现的链上运算仍然成为了事实上的主流方式。一些后来的区块链尝试模仿并改进,如改用成熟编程语言,多链协作,其他的共识算法 (DPOS) 等等。 (「GL-Note」 在运算的同步执行这块,似无太大的变化)</p> <hr> <h2 id="2-bsv-的链上运算模型">2. BSV 的链上运算模型</h2> <p>跟以太坊“拿区块链当CPU用”不同的是,BSV 的思路更倾向于<strong>把区块链当做一个数据库和操作系统</strong>。</p> <p>在 _unwriter 于 Bitcoin Cash 上开发 BitDB 时,他应该是头一个“真正”把区块链当数据库用的。后来分叉时 _unwriter 选择了 BSV 的路线并留下了一篇有价值的 Blog:</p> <ul> <li><a class="link" href="https://medium.com/@_unwriter/the-resolution-of-the-bitcoin-cash-experiment-52b86d8cd187" target="_blank" rel="noopener" >The resolution of the Bitcoin Cash experiment</a></li> <li>【中】 (黄酥酥@微博) <a class="link" href="https://weibo.com/ttarticle/p/show?id=2309404312035490985667" target="_blank" rel="noopener" >深度解析比特币现金实验</a></li> </ul> <p>紧接着在 Craig Wright 发布了 Metanet 之后,_unwriter 发布了一系列相关的工具,这些工具所共通的设计基础,都是将区块链当做一个数据库和操作系统 (更进一步,当做某种意义上的互联网) 来使用。</p> <hr> <ul> <li><a class="link" href="https://bitdb.network/" target="_blank" rel="noopener" >BitDB</a> 去中心化数据库,将区块链写入 Mongodb,并让所有的操作更易于检索。</li> <li><a class="link" href="https://genesis.bitdb.network/" target="_blank" rel="noopener" >Genesis</a> 一个 BSV 专属的 bitdb 节点 (于2018年11月分叉后)</li> <li><a class="link" href="https://babel.bitdb.network/" target="_blank" rel="noopener" >Babel</a> 一个定制 bitdb 节点,只关心 OP_RETURN 内存储的数据,不关心典型的交易</li> <li><a class="link" href="https://docs.planaria.network/#/" target="_blank" rel="noopener" >Planaria(变形虫)</a> 一个更通用的 BitDB,支持自由定制规则,可以在链上存储和检索任意形态的数据</li> <li><a class="link" href="https://bottle.bitdb.network/" target="_blank" rel="noopener" >Bottle</a> <strong>Bitcoin Browser</strong> 一个定制 bitdb 节点,比特币浏览器(注意这是真正意义上的浏览器,不是 block explorer),使用 B:// 和 C:// 定位所有的资源,真正的 serverless,完全容纳于 Bitcoin Blockchain 的边界内。</li> <li><a class="link" href="https://bitcom.bitdb.network/#/" target="_blank" rel="noopener" >BitCom</a> <strong>Bitcoin Computer</strong> 仿 Unix 文件系统,使用 Bitquery 加载目录,方便定义二级应用程序协议</li> <li><a class="link" href="https://github.com/unwriter/datapay" target="_blank" rel="noopener" >DataPay</a> 轻量级 js 库,最简洁的发送带有数据的 tx 库,没有之一 (MoneyButton 和 Bitpay 都在用)。</li> <li><a class="link" href="https://medium.com/@_unwriter/babel-230f73ed5dcb" target="_blank" rel="noopener" >Babel - A BitDB Node for Data-Only Bitcoin Applications</a></li> <li>(hqm@知乎) <a class="link" href="https://zhuanlan.zhihu.com/p/62287840" target="_blank" rel="noopener" >Bitcoin SV的开发哲学——变形虫框架</a></li> <li>(hqm@知乎) <a class="link" href="https://zhuanlan.zhihu.com/p/64697171" target="_blank" rel="noopener" >BSV Planaria框架技术总结一 节点搭建</a></li> <li>(hqm@知乎) <a class="link" href="https://zhuanlan.zhihu.com/p/64796784" target="_blank" rel="noopener" >BSV Planaria框架技术总结二 Bitquery</a></li> </ul> <hr> <p>简单说,Planaria 可被用来 (以数据库的形式) 存储和检索,DataPay可被用来写入,BitCom 可被用来定义访问协议 (类似文档后缀名的作用),Bottle 用来连接不同类型的数据和资源并展现到用户面前。 (「GL-Note」 连起来看,这已经是一个事实上成型的冯诺依曼架构了)</p> <p>2019年1月份时 nChain 挖出的块中包含了一条 100KB <code>OP_RETURN</code> 的交易,从那时起,220 字节的限制被打破。几个小时之后,_unwriter 就发布了一个网站,以 serverless 网站的形态展示了 《爱丽丝梦游仙境》 中的一章。(「GL-Note」 这里得用 <a class="link" href="https://zeronet.io/" target="_blank" rel="noopener" >ZeroNet</a> 杠一下,不过跟 <a class="link" href="https://alice.bitdb.network/" target="_blank" rel="noopener" >爱丽丝梦游比特仙境</a> 相比,ZeroNet 只是套了个 Bitcoin+Torrent 的壳)</p> <h2 id="3-新模型的关键点是什么">3. 新模型的关键点是什么?</h2> <p>从那时起,一种新的链上运算模型出现了。运算本身不在链上。只有指令 (类似一段脚本代码或一个程序库) 以“文件”形式在链上储存。</p> <p><strong>区块链原来不是 CPU,而是文件系统。</strong></p> <p>当用户执行一个链上运算时,实际上只是在本地运行需要的操作 (如在浏览器里执行一段 js 代码)。由于其他节点并不关心执行过程,对应的运算在链下执行,只有当产生有意义的结果时才上链,最终展现出来的成果,要么是一笔有意义的<strong>交易</strong> (以 tx 形式),要么是一份有意义的<strong>数据</strong> (以 <code>OP_RETURN</code> 形式)。</p> <p>利用这台超级计算机,可以使用 (链上已经存在的) 任意语言及任意库,不需要担心“合约”与共识的兼容,也不需要 (过分地) 担心容量和尺寸。<strong>数据和脚本在链上,而运算在链下。</strong> 这就是 ETH 和 BSV 在模型上的最大不同。</p> <p>但这样一来人们很自然会问,如果所谓“区块链运算模型”都不发生在链上,那么这个模型的意义何在呢?如果关键的操作没有所有节点同步执行,谁来保证这个运算是有效,合法,且符合预期的?这相比老一套的数据库和互联网又有啥优势呢?</p> <p>关键在于以下两点是不可变且已被认证的 (immutable and authentificated):</p> <ol> <li>操作上链,保证了执行流程在需要时,可以随时被验证。(按需验证)</li> <li>结果上链,保证了执行结果在需要时,可以随时被验证。(按需验证)</li> </ol> <p>「GL-Note」 不管代码是否开源,给定程序的执行流程和操作 (protocol/spec) 已经立此存照,全网见证;不管数据是否有加密,上链后就不会再被主动或被动地“丢失”。这两样都比自己维护一个服务器更健壮,成本也更低。</p> <hr> <p>「GL-Note」 拿游戏举个例子:</p> <p>比如我现在写了个游戏,我把游戏的所有代码都上传到一个 tx (tx_sourcecode) 里。所有这个游戏的链上产出 tx_gamedata 都是跟 tx_sourcecode 关联的。矿工只是无脑地把 tx_sourcecode 和 tx_gamedata 提交上去(只要交够了矿工费)。注意:针对我这个游戏,矿工并不是利益攸关方,游戏是否盈利,对其并无直接的利害关系。而游戏运营方出于维护游戏的正常运营,则有强烈动机去运行从 source code -&gt; game data 的运算。谁对这个行为有疑问,谁可以利用 tx_sourcecode 自己去运行逻辑验证。</p> <p>跟朋友讨论时,朋友提到,即使同一份 lua 代码,在不同机器不同运行环境上都不尽相同,如何能保证结果是 verifiable 呢? ETH 那里不会有这个问题,是因为需要当即对结果达成一致。</p> <p>而我认为,这实际上不需要矿工来保证,因为他们不是利益相关方。游戏行为不一致,受损的是游戏厂商,矿工本质上是无所谓的,只要你给够交易费我就给你打包上链,并没有“验证所有逻辑一致性”这个义务 (就像矿工没有“验证所有链上的数据都是符合当地法律法规”的义务,同样的道理)。游戏和应用开发者才关心这一点。 而且进一步讲, lua/python 这类语言本身的设计目的就是尽可能在不同环境下尽可能给出一致的执行结果,如果做不到,自然会有更 well-define 的语言去填补这个需求。实际产品里,弄个受限的运行时(沙盒)应该就可以了。</p> <hr> <h2 id="4-eth-与-bsv-的模型对比">4. ETH 与 BSV 的模型对比</h2> <p>ETH 对于合约的强制链上执行,本质上是将“矿工验证交易”的过程通用化,并做了进一步扩展。客户端发起一笔 (与某个合约相关的) 交易,本质上相当于触发了一个全网执行的动作。</p> <p>而 BSV 的路径截然不同。当一个网站以 severless 方式运行时,客户端可以随时去修改和操作某一部分数据,除了这部分数据作为结果保存到链上以外,其他不关心这个网站上的这部分数据的矿工和用户,基本是无感的。</p> <p>ETH 的合约可以在满足特定情况时主动地创建交易,而 BSV 链上只有代码和结果,只能被动地接收用户发起的交易。(「GL-Note」 但实践中问题不大,只需要用 bitdb 去监听特定的事件并触发交易,也可以达到一样的效果。注意:这个监听及响应的代码逻辑同样也上链,所以同样是 verifiable 的)</p> <p>这里原文顺带提了一下 BSV 日后的操作码恢复和扩展,及可能的图灵完备。这些等日后有了再谈吧,目前就不多说了。</p> <hr> <h2 id="5-使用新模型-构建去中心化超算">5. (使用新模型) 构建去中心化超算</h2> <p>一个思维实验:我们现在打算构造两个超算,分别位于 ETH 和 BSV 上,它们上面需要跑一个 60MB 的程序,来计算 1GB 的气候数据。</p> <p>先看看以太坊,</p> <p>在以太坊上存 1GB 数据是比较困难的,这差不多是一个礼拜的数据量。把 60MB 的程序放上去会相对容易点,但也会需要脚本做不小的改动,用掉不少 Gas,而且可能会造成安全隐患。接着,考虑实际的运行,从运算能力角度讲,这样的计算量级可能会消耗大量的 Gas。</p> <p>人们意识到,与其在以太坊上构造超算,不如构造一个链上的超算市场 (a marketplace for supercomputing) 更有意义,Golem 正是这么做的——用智能合约来满足去中心化算力交易 (supercomputer power) 的需求。 (「GL-Note」 与 NiceHash 相比除了更 General-Purpose 一点外,并无太大不同)</p> <p>再来看 Bitcoin SV,</p> <p>首先,1GB 算不上太大的数据量,Ryan X. Charles 就曾上传了 1.4GB 的图片。然后,是 60MB 的指令序列,二者都发到链上。这时候借助一个链上的交易网站,可以撮合提供计算能力的人和有需求的人。这样,区块链就成了一个共享的文件系统。</p> <p>(「GL-Note」 这思路感觉有点绕弯了。实事求是地讲,从实践上看,更简单的做法是运营者直接用算力去支撑运算需求,然后卖服务,毕竟 SaaS 还是比 dex 门槛低不少的)</p> <hr> <h2 id="6-个人化的一些零碎的思考">6. 个人化的一些零碎的思考</h2> <p>「GL-Note」 原文的最后一节是作者的一些杂感,我就不逐句照搬了。这一节是我另加的,是我在形成本文时的一些个人化的杂感。</p> <p>上面对 ETH 和 BSV 做了不少的对比,这里我也来做一个对比。ETH 网络中,矿工,节点和其他参与者之间的关系是复杂的,没有良好定义的,整个网络<strong>缺乏自发地向更好方向进化的能力</strong>。而 BSV 的区块链网络中,矿工,服务提供者,服务参与者,他们从整个架构设计的角度讲,是良好定义的,从本质上都是可以<strong>只</strong>关心 self-interest 的。</p> <p>展开来说一下吧。譬如矿工,所有的运营动机,只是单纯地为了从长远看不断提高自身利润率,至于运算,爱谁算谁算,我把你们的数据存到链上,只是我打包赚钱的一个天然副产品而已。再譬如服务提供者,把区块链当商品用,我付矿工费,得到公开账本+永不丢失的数据+透明可验证的舆论效果,这么一算,比去阿里云上租个服务器更省钱省事更划算 (初期可能简陋点,没有商业云上那么多工具,当然这都是机会)。再譬如服务使用者,比较区块链上的 Twetch 跟中心化的微博,简直不好比的好吧。删帖封号,更别提什么大数据侵犯隐私了。拿着私钥,自己对自己的数据有足够的掌控权。说句题外话,也许日后有一天,一个人的数据,也许比这个人本身更值钱。</p> <p>我相信,关于链上运算的讨论只是刚刚开始。目前的现状其实很像 DOS 时代的 PC 产业, _unwriter 实现的一系列工具,正如 DOS 下,我们得以通过基本的手段,来朴素地控制 PC 的硬件资源。然而,从工具到操作系统,这种快速成长的可操作性,在当年反过来倒逼了硬件标准化,直至后来的只剩下 Win-tel,这里面有着深刻的历史必然性。同样的,区块链从无数不知所谓的创新项目,迅速收敛至最有扩展性,最有可操作性的链上,“书同文,车同轨”,才有机会活下来,创造更长远的价值,更深刻地改变人类社会。</p> Archives https://gulu-dev.com/archives/ Tue, 28 May 2019 00:00:00 +0000 https://gulu-dev.com/archives/ 2018.08 火币“区块链+游戏”产业专题报告 (干货版) https://gulu-dev.com/post/2018-08-04-huobi-blockchain-game-industry-report/ Sat, 04 Aug 2018 00:00:00 +0000 https://gulu-dev.com/post/2018-08-04-huobi-blockchain-game-industry-report/ <ul> <li>火币的“区块链+游戏”报告对从业者是有价值的参考,本文是其脱水干货版,供快速参考。</li> <li>由于去掉了一般性阐述,可能会在可读性上稍差一些,但好处是信息比较集中和紧凑,便于核心内容提取</li> <li>原文见:<a class="link" href="https://mp.weixin.qq.com/s/4n6ibjZgepY9g7iEXLDUAw" target="_blank" rel="noopener" >火币区块链产业专题报告:游戏篇——“新的财富金矿?游戏产业的割裂与重构”</a></li> </ul> <hr> <h2 id="从区块链行业角度看到的传统游戏的痛点">从区块链行业角度看到的传统游戏的痛点</h2> <ol> <li>数值不透明,规则可任意更改 (中心化弊端,可通过透明合约保障),游戏开发者承担全部开发责任,不及预期导致用户流失; (开发迭代压力与用户消费速度的失衡)</li> <li>渠道、发行垄断收益;(成熟产业特性,优质而小众的产品出头困难,游戏开发投入大、不确定性高)</li> <li>虚拟资产不属于用户,亦无法顺畅实现价值流通; (传统游戏内在价值的封闭和不流通) (网易藏宝阁-&gt;Steam-&gt;C5Game/IGXE/5173 各自封闭且受限)</li> <li>游戏内生恶性通货膨胀倾向,玩家利益得不到保证; (调节供给,限制流通,取消交易,以牺牲游戏可玩性来缓解通胀,效果有限)</li> <li>游戏间体系不互通,玩家沉没成本高昂。 (游戏停服,玩家投入难以回补)</li> </ol> <hr> <h2 id="区块链行业特性及与游戏行业的契合点">区块链行业特性及与游戏行业的契合点</h2> <ul> <li>最契合区块链的应用场景是原生数字化领域的,非与现实发生巨大联系的,并且多适用于: <ul> <li>交流效率低,信任成本高的领域(产业链条长、环节众多);</li> <li>对真实性、共识有极大需求的领域(不透明、黑箱);</li> <li>以及长尾流量、资源分散、参与者之间利益不一致的领域(需要激励)。</li> </ul> </li> <li>游戏行业契合度 <ul> <li>原生数字化</li> <li>游戏用户群体年轻,容易接受新鲜事物,有望在未来 (与区块链这一新事物) 形成融合和合力。</li> <li>游戏不分语言,具备天生的跨国界属性,易传播,易出现爆款,点燃市场</li> </ul> </li> </ul> <hr> <h2 id="区块链对游戏行业的重构">区块链对游戏行业的重构</h2> <ol> <li>上链,公平可信,同时Token激励让游戏社区化,革新不再单纯是开发者的责任; <ul> <li>对于传统的游戏来说,上链所带来的透明化,意味着可为其带来超额利润的优势不再,同时,代码透明化后,一款游戏的可复制性较强,容易被抄袭。因而我们认为,区块链游戏,必将是从没有历史包袱的纯区块链游戏开发者开始的,并逐步迁移、倒逼传统游戏开发者加入区块链化进程。</li> <li>而传统游戏的区块链化,亦将首先从改良优化的角度出发,逐步深入,且会经过游戏币上链,到游戏资产上链,再到核心游戏逻辑上链(智能合约运行游戏),最终到游戏整体上链这一缓慢和需要适应的过程。</li> <li>社区可以在Token经济激励的前提下,根据自己的需求,选择和实现各自的治理方式和游戏规则。区块链有望将游戏社区化,有望大幅催生UGC,并延长部分游戏的生命周期。</li> </ul> </li> <li>借助区块链,打破渠道垄断,产生新的自分销网络; <ul> <li>中小的渠道商本身因为没有自己的SDK,数据依附于大厂,存在一定的不信任问题。有了区块链、Token后,就可以有一个完全透明、去中心化的激励型分发接口,记录用户所有的下载、注册、激活、消费等等行为,并不可篡改,从而建立起共识,更为之后广告商的买量业务奠定了真实性的基础;而区块链本身的可追溯性,天生适合应对长尾、分散又关系错综复杂的流量,即可以非常明晰地看到流量的来源,并进行对应的分析,结合Token,对不同的渠道调整激励策略。最终使得中小独立开发者、中小渠道和用户实现共赢共生的关系。</li> </ul> </li> <li>用户真实拥有游戏内资产(True Ownership),并可借助智能合约去信任流通; <ul> <li>每一个虚拟资产都可以归结为一个独一无二的Token,称之为Non-Fungible Token(NFT),并通过加密方式存储在用户的区块链账户地址之中,安全、可靠,所有权完全归属用户,成为“稀缺性”、“实用性”和“观赏性”结合的收藏品,大大提升了虚拟资产的价值</li> <li>存储在链上的虚拟资产,也可以通过智能合约形式,在去信任的环境下进行无摩擦点对点交易,并可以在区块链上看到所有成交历史,真实可信。</li> </ul> </li> <li>区块链的跨应用账本特性,使同款IP资产可以被复用,大大增加游戏间的交互性及玩法。 <ul> <li>由于区块链的跨应用账本特性,同一款IP资产可以被复用,例如IP资产以太猫可以被用在其他游戏之中,或是进行二次改造,游戏与游戏之间是可以互通及联动的,而区块链本身由此也成为了一个素材库,可为开发者所随时调用,引领UGC的新时代。</li> </ul> </li> <li>重塑游戏内经济体系。 <ul> <li>游戏经济体系设计,不再是传统的数值策划,而是通证经济,即可构建一个总量有共识,友好开放的游戏Token体系,并可实现游戏价值评估尺度的范式转移。</li> <li>(1)友好开放的游戏Token体系(2)总量有共识(3)价值评估尺度的范式转移</li> <li>传统游戏的价值评估,也取决于为开发商创造了多少利润,而非给玩家带来了多少利益。然而区块链游戏的本质是社区,是共赢,是共有。</li> </ul> </li> </ol> <hr> <h2 id="阻碍区块链游戏大范围部署的瓶颈以及区块链游戏的缺陷">阻碍区块链游戏大范围部署的瓶颈,以及区块链游戏的缺陷</h2> <ul> <li>用户量小,Dapp运行需载入钱包,消耗资源,秘钥管理困难,门槛高</li> <li>游戏区块链化仍在初级阶段,而大部分区块链游戏系原生,且玩法原始 <ul> <li>由于目前主流区块链底层智能合约运行均需要消耗Gas费用,因而若游戏机制过于复杂,可能会导致用户频繁签名,一定程度上也影响了用户在游戏中的体验</li> <li>目前的区块链游戏,大部分并非传统游戏的区块链化,实际系原生的区块链游戏,游戏类型相对原始,对于大部分真正的玩家来说,难以具备很强的吸引力,区块链游戏的用户群体,与真正的游戏硬核玩家,重合度相对较低。</li> </ul> </li> <li>底层不足,大型游戏的运行追求零延迟,然而目前公链性能不符要求</li> <li>目前区块链游戏投机性较明显,尚未找到可持续的用户留存方式 <ul> <li>区块链游戏潜在具备了公开透明、资产可交易等等优势,然而由于该部分特性对于玩家来说并不直观,同时区块链游戏的体验尚还不能与传统游戏相比,为了吸引用户的涌入,区块链游戏大多直接或间接打着让用户赚钱的旗号进行传播,而用户赚钱的来源,常常来自于新玩家的投入,具备极强的投机性特征,实际已经偏离了“游戏”本身定义的初衷。该种模式一定程序上能快速吸引流量,然而由于游戏本身的持续依赖“接盘”者的存在,因而往往面临后劲不足的问题,部分用户高位“接盘”,游戏新增用户难以为继时,游戏便开始走下坡路,游戏的生命周期被投机性所吞噬和透支。</li> </ul> </li> </ul> <hr> <h2 id="区块链游戏产业链的五大板块-各类实例见文章本身">“区块链+游戏”产业链的五大板块 (各类实例见文章本身)</h2> <ol> <li>基础设施及开发者工具;</li> <li>跨游戏虚拟资产交易市场;</li> <li>区块链游戏;</li> <li>基于区块链的游戏分发平台、社区;</li> <li>周边工具与服务。</li> </ol> <blockquote> <p>“区块链+游戏”的红利,将从基础设施及开发者工具开始,让更多的游戏上链,并以爆款区块链游戏的大量出现为标志达到高潮;而随着区块链游戏以及相应虚拟资产数量的不断增多,跨游戏虚拟资产交易市场,以及基于区块链的游戏分发平台/社区的也会逐步兴起,成为流量汇集的中心;而周边工具、服务,随着“区块链+游戏”行业的发展,则具有较为明确和稳定的发展。</p> </blockquote> <hr> <h2 id="区块链游戏赛道未来的潜在爆发点">“区块链+游戏”赛道未来的潜在爆发点</h2> <ul> <li>含原生Token的游戏能适应更为复杂的商业模式,而不含原生Token的游戏,则更适用小而美、简单的商业逻辑;</li> <li>区块链怎样与游戏进行融合?我们认为,区块链与游戏融合,是在含Token的前提下,构建一个开发者、发行商与游戏玩家利益一致的永续生态,它的思想将会体现在如下三个方面: <ul> <li>激励机制:游戏中的“Proof-of-Work”与“Proof-of-Stake”</li> <li>游戏价值集中在NFT资产之上,通过Token价值输出变现</li> <li>充分激发UGC创造力、玩家社区贡献的容器</li> </ul> </li> <li>区块链游戏(或游戏区块链化)的可持续商业模式在哪里? <ul> <li>同时,区块链结合游戏的核心即游戏内资产(NFT),其是稀缺性、实用性及观赏性结合的收藏品,设计精髓在于为用户创造稀缺性和唯一性的价值。</li> <li>未来,预计以ERC721为标准的游戏虚拟资产市值将会逐步膨胀,成为自ERC20同质化Token之后又一大爆点。而这将经历两个过程,首先是原生区块链游戏的ERC721资产市值的增长和流转,其次是传统游戏厂商手中的IP,尤其是一部分老牌的IP的Token化,借助区块链重新焕发活力,而这部分虚拟资产,很可能会通过授权的形式进入这个市场。</li> <li>另外,区块链游戏商业模式将<strong>围绕资产交易进行</strong>,并从原生区块链游戏的ERC721资产开始,逐步拓展到传统游戏IP,尤其是老牌IP。</li> </ul> </li> <li>区块链游戏的新趋势与玩法构想 <ul> <li>实物ERC721—桌游的Token化改造 (Playtable)</li> <li>(其他案例略)</li> </ul> </li> </ul> 2018.01 Dice (EA) 工作室游戏开发技术概览 https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/ Thu, 25 Jan 2018 00:00:00 +0000 https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/ <img src="proxy.php?url=https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/title.png" alt="Featured image of post 2018.01 Dice (EA) 工作室游戏开发技术概览" /><ul> <li><strong>2022-05-01 补记</strong> <ul> <li>这是我在 2018 年离开西山居前不久做的一次技术分享,原文副标题是 “<strong>Dice 如何改造引擎适应现代技术发展</strong>”。</li> <li>这次整理的过程里重新看了一遍,还有不少新的收获和启发。</li> </ul> </li> </ul> <h3 id="材料分享">材料分享</h3> <p>这份材料的内容还是非常扎实的,一共有75页。</p> <p><a class="link" href="2018-dice.pdf" >分享全文 PDF 链接</a></p> <h3 id="intro">Intro</h3> <ul> <li>对 Dice 到目前为止已公开的 55 份技术资料做了筛选和提炼,形成一份相对完整的信息体系,提取重点并在讨论会上做快速的讲解。</li> </ul> <h3 id="听众受益">听众受益</h3> <ul> <li>了解一线游戏工作室的技术关注热点,趋势判断,及若干实践中的分析,判断和取舍</li> <li>了解如何在成熟的技术体系中保持系统的低熵,持续吸纳和消化新的技术并改善架构</li> <li>在使用成熟工具链生产内容时,如何降低持续引入的新技术对已制作资源和已建立流程的冲击</li> <li>如何在通用化 (复用技术组件,工具,内容和资源) 和独特性 (避免画面和 Gameplay 的同质化) 之间避免失衡</li> <li>Massive Procedural Content Generation (大体量的过程化的内容生成技术)</li> <li>Game Engine Mobilizing (游戏引擎的移动化)</li> </ul> <h3 id="大纲">大纲</h3> <p>本次分享的大纲,主要是三个部分:移动化,围绕数据的改造和 FrameGraph。</p> <p><img src="https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/outline.png" width="1360" height="764" srcset="https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/outline_hu69e5435565c65008d0817fa89807663c_492961_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/outline_hu69e5435565c65008d0817fa89807663c_492961_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Outline" class="gallery-image" data-flex-grow="178" data-flex-basis="427px" ></p> <h3 id="topics"><strong>Topics</strong></h3> <p>这是当时从55分材料当中提取出的最有价值的10篇。</p> <ol> <li>(2007) Frostbite: 渲染架构 (过程化纹理和着色)</li> <li>(2008) Battlefield: 基于节点的着色框架,命令缓冲并行化,软光栅裁剪实现</li> <li>(2009) Frostbite: 引擎并行化,CPU/GPU Jobs</li> <li>(2011) Battlefield 3: 粒子光影和间接光,完整绘制流程,图形选项和性能工具</li> <li>(2013) Frostbite: 资源流水线扩展:存储和数据库,构建模型,网络缓存</li> <li>(2015) Battlefield 4: 渲染流水线的挑战和改造 - 降低复杂度,预计算,利用屏幕空间</li> <li>(2016) Frostbite 3: 族裁剪 (Cluster Culling),朝向裁剪 (Orientation Culling),微裁剪 (Primitive Culling) 深度裁剪 (Depth Culling) 打包绘制 (Draw Compaction)</li> <li>(2017) Frostbite 架构对比 (2007 vs 2017) FrameGraph 实现,临时资源 (Transient Resource) 管理,帧流程的图形化 (可搜索 PDF) 异步模型,内存布局复用</li> <li>代码技术:局部栈分配,面向数据设计,二进制膨胀</li> <li>移动平台技术: GL/Metal 对比分析,光照系统改造,基于局部存储的优化 (tile memory)</li> </ol> <h3 id="一些分享前的准备脚本">一些分享前的准备脚本</h3> <p><strong>什么是“围绕数据设计”?</strong></p> <ul> <li>Data-Oriented Design</li> <li>把关注点从“对象和交互”转移到“数据和读写”。</li> <li>现代体系的内存访问敏感性强,提供紧凑的数据,理解和配合 Cache 的行为越来越重要。</li> <li>当把行为线程化时,对数据读写的明确约定能够极大地简化逻辑,避免不必要的锁。</li> <li>良好的面向对象设计,会把逻辑和数据耦合在一起,而线程同步往往只关心数据本身。耦合带来维护负担。(POD-struct 的同步几乎总是比对象同步要简明)</li> </ul> <p><strong>面向对象的例子</strong> (数据不紧凑,难以优化)</p> <ul> <li>80 个时钟周期的运算实际消耗了 7680 个周期</li> <li>98.96% 的时间花费在实际运算逻辑之外,惊人的浪费</li> <li>反过来从输出出发,仅仅提供运算所需的最少量的输入数据</li> <li>紧凑的数据,更有利于批量处理</li> <li>消耗在内存寻址上的时间降低到原来的 25% (1900 ~ 7600)</li> </ul> <p><strong>内存布局的对比 (OOD vs DOD)</strong></p> <p>核心关注点是内存的利用效率</p> <ul> <li>优先针对数据优化,而不是代码</li> <li>绝大部分代码的性能实际上取决于内存访问 (bound by memory access)</li> <li>用于运算的数据集 (native data) 可以跟源数据集 (source data) 分离 <ul> <li>(就好像 native code 和 source code 那样)</li> <li>可以理解为,为了更高效的访问,我们把数据“编译”为更紧凑的组织形式</li> </ul> </li> </ul> <p>三个例子:</p> <ul> <li>场景中的逻辑对象 (如触发器 Trigger) <ul> <li>原始数据由场景树和链表加载并持有</li> <li>但生成紧凑数组用于专项处理</li> </ul> </li> <li>裁剪系统 (所有对象的包围数据本来是各自持有并维护) <ul> <li>从层次化数据结构中,提取包围信息并填充到线性数组</li> <li>忽略父子关系,一次性迭代并处理所有对象</li> <li>比原有系统快 3 倍 (3x),代码大幅简化 (code size 1/5)</li> <li>更容易并行化</li> <li>&lt;简略介绍&gt; Culling The Battlefield</li> </ul> </li> <li>Path-finding <ul> <li>&lt;简略介绍&gt; AStepTowardsDataOrientation</li> </ul> </li> </ul> <p><strong>高级内存话题 (域内的栈上分配)</strong></p> <h3 id="资料来源">资料来源</h3> <p><img src="https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/topiclist.png" width="907" height="2122" srcset="https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/topiclist_hu53df85aeae3e784c5d0e6f49200da099_315392_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2018-01-25-ea-dice-tech-overview/topiclist_hu53df85aeae3e784c5d0e6f49200da099_315392_1024x0_resize_box_3.png 1024w" loading="lazy" alt="topiclist.png" class="gallery-image" data-flex-grow="42" data-flex-basis="102px" ></p> <p>本次分享所参考的 55 份材料来源在这里:</p> <p><a class="link" href="https://github.com/mc-gulu/dev-awesomenesses/blob/master/awesome-frostbite-engine.md" target="_blank" rel="noopener" >dev-awesomenesses/awesome-frostbite-engine.md</a></p> <p>由于年久失修,其中绝大部分链接现在已不可用,DICE 也已经不再开放他的技术文档公开访问了。</p> 2017.11 逆转?是的,也许就在本周末 https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/ Fri, 24 Nov 2017 23:47:00 +0000 https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/ <p>这两天里发生的若干事件,纷纷指向了一个震撼性的结果:</p> <p><strong>Bitcoin Cash (或许) 将翻盘成功,取代 Bitcoin Core 成为“真正的” Bitcoin。</strong></p> <p>而这一事件最早可能在本周末就会发生。</p> <hr> <p>在《逆转》一书的前言里,马尔科姆·格拉德威尔讲述了传奇的“大卫和歌利亚”的故事,描述了一个普通人是如何面对巨人并战而胜之的。</p> <p><img src="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/1.jpg" width="638" height="418" srcset="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/1_hu6ca3e83c73cb2f8f3d7b8441dddf7e37_94301_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/1_hu6ca3e83c73cb2f8f3d7b8441dddf7e37_94301_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="1" class="gallery-image" data-flex-grow="152" data-flex-basis="366px" ></p> <blockquote> <p>歌利亚是一个重装步兵。他认为,即将和他战斗的会是另一个重装步兵,他们会像提图斯 曼利乌斯和高卢战士那样决斗。当他说“到我跟前来,我要拿你身上的嫩肉去喂天空的鸟儿和地上的野兽”,关键句是“到我跟前来”。他的意思是到我跟前来,我们才能近身搏斗。而扫罗之所以想让大卫穿盔甲、配剑,也是因为他做了同样的一种假设,他认为大卫要和歌利亚近身肉搏。 然而大卫并不想遵循决斗的惯例。他告诉扫罗,他放羊的时候曾经杀死过熊和狮子,他这么说不仅表现出他的勇气,同时也表明了一件事:他打算像对付野兽那样来对付歌利亚,他要做一个投石手。 他跑向歌利亚。因为没有穿盔甲,所以他速度很快,动作很灵便。他拿了一颗石子放在皮囊里,不停地甩动,速度越来越快,每秒约6 ~ 7转。他将投石器瞄准了歌利亚的前额——这是巨人唯一的弱点。最近,弹道学专家埃坦赫希和以色列国防军进行了一系列计算,结果表明一个专业的投石手在35 米的距离内投出的常规大小的石子,能以每秒钟34 米的速度击中歌利亚的头。这个速度足够将石子射入歌利亚的头颅,令其失去意识或者死亡。从制动能力来说,这种威力相当于一把大型的现代手枪。赫希写道:“我们发现大卫投出石子并击中歌利亚的整个过程只持续了一秒多,时间太短了,以致歌利亚根本来不及保护自己。而事实上,在那段时间内他根本没有移动过半步。” 歌利亚能做什么呢?要知道那时他身上的盔甲足有100 磅重。他准备来一场近身搏斗,这样他就可以站着不动;身上的盔甲能够帮他挡住攻击,他就可以将矛用力地刺向敌人的身躯。他看到大卫走过来的时候,首先是蔑视,接着是惊讶,然后便只有恐惧——他似乎明白了,这场斗争和他期望的斗争不一样。</p> </blockquote> <hr> <p>先看下图,</p> <p><img src="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/2.png" width="1006" height="532" srcset="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/2_hubada7fd5ad2ec9ed692f6f91a62ac972_80340_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/2_hubada7fd5ad2ec9ed692f6f91a62ac972_80340_1024x0_resize_box_3.png 1024w" loading="lazy" alt="2" class="gallery-image" data-flex-grow="189" data-flex-basis="453px" ></p> <p>这是过去一个月的 BTC/BCH 算力对比图,其中黄色为 BTC 蓝色为 BCH。由于比特币每 2016 个区块调整一次难度 (约两周),过去的一个月里,比特币主链分别在 10.27 和 11.11 两次调整难度。而图上可以看出,<strong>这两次难度调整时,均有大量的算力转移到 BCH 链上来</strong>。这是什么情况?</p> <p>首先,这显然不是区块奖励的收益高低驱动的。因为上一个周期 BTC 的难度是<strong>下降</strong>的,也就是说矿工挖 BTC 收益是上升的,但仍出现了大量算力一致转移到 BCH 的情况。</p> <p>其次,我们先暂时放下矿工的动机,看看最大的受害者是谁。由于 BCH 是动态调整难度 (最近又硬分叉部署了动态调整的改良版 DAA) 算力的变化对其影响不大,而 BTC 就不行了,同样难度下突然损失大量算力的后果是出块困难,大大延长了每个区块被挖出的时间。11.11 日的难度调整时,BTC 约半个小时才能出一个块 (远大于平常 10 分钟一个块的节奏) 导致 mempool 大量未确认的交易的积压,如下图所示:</p> <p><img src="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/3.jpg" width="953" height="619" srcset="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/3_hua6263aa06219c8b5628e6546422dba30_67492_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/3_hua6263aa06219c8b5628e6546422dba30_67492_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="3" class="gallery-image" data-flex-grow="153" data-flex-basis="369px" ></p> <p>上图为 11.11-11.12 日期间 BTC 的交易积压情况。</p> <p>以上分析可以看出,<strong>大量的算力在有节奏地对 BTC 发起攻击</strong>。为什么总是挑难度调整时来实施呢?——因为这样 BTC 将被迫忍受最长的反应时间 (两周)。</p> <p><img src="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/5.jpg" width="1886" height="123" srcset="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/5_huee5c350902f5ec7a4d08fe9b6537fa8c_29662_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/5_huee5c350902f5ec7a4d08fe9b6537fa8c_29662_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="5" class="gallery-image" data-flex-grow="1533" data-flex-basis="3680px" ></p> <p>具体的攻击上面的截图已经说得非常清楚明白,就不再赘述了。</p> <hr> <p>由于有不少矿池是所谓的“机枪池”,也就是根据当前的收益自动判断挖哪个,那么一旦 BCH 价格被拉升,很快就会出现压倒性的算力切换,这是可以被攻击的一方利用的“放大效应”。</p> <p>具体的操作步骤上就是:</p> <ol> <li>攻击者切换直接掌握的算力到 BCH</li> <li>攻击者卖出 BTC 买入 BCH 推高价格</li> <li>BCH 矿工收益高推动更大的算力切换</li> <li>算力切换后,BTC 出块困难,加剧拥堵,用户难以转账,交易费急剧升高</li> <li>BTC 涌现大量卖单,算力进一步倒戈,各大交易所恐慌性下跌</li> <li>如果攻击者不愿意承受过大的风险,可适时将算力切回 BTC,促使出块恢复正常,价格回升,同时买回大量 BTC 做下次攻击的准备</li> </ol> <p>整个过程不仅形成有效攻击,而且基本资金零风险,可以正反双向操作,通过操纵行为巨额套利。我真的很佩服这样的策划和实施能力,扩容的技术和道德高地 (更高效的交易处理,更低手续费) 也占了,舆论也基本上都站在这一边,的确是控制全局的大手笔。</p> <hr> <p>补充一些细节,看我在知乎想法上的回复:</p> <p><img src="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/4.jpg" width="954" height="1389" srcset="https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/4_hu331eb529beb0abaf3d35cbb81f0764de_227204_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-11-25-bitcoin-weekend/4_hu331eb529beb0abaf3d35cbb81f0764de_227204_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="4" class="gallery-image" data-flex-grow="68" data-flex-basis="164px" ></p> <hr> <p>Adam Beck 这两天也是昏招迭出。给所谓的 HalongMining 站台,被曝对 Bitcoin Gold 幕后支持,唉,慌不择路,拙计啊。</p> <hr> <p>另,不要觉得 BTC 难度降了一点算力就不能跌了,上一次降难度算力照样猛跌,原因上面已经说过了,价格上涨带来的收益远大于那么丁点难度下降多挖的一点币。</p> <p>2017 年的下半场,真的比上半场更精彩。</p> <hr> <ul> <li>Written on [2017-11-24] in 2h</li> <li>Arguments in this article don&rsquo;t provide any form of financial advices for readers.</li> </ul> 2017.09 FontPruner 字体精简工具 https://gulu-dev.com/post/2017-09-15-font-pruner/ Fri, 15 Sep 2017 23:47:00 +0000 https://gulu-dev.com/post/2017-09-15-font-pruner/ <h1 id="fontpruner-字体精简工具">FontPruner 字体精简工具</h1> <h2 id="需求">需求</h2> <p><a class="link" href="https://github.com/GameBuildingBlocks/FontPruner" target="_blank" rel="noopener" >FontPruner</a> 是我们 16 年底开发的一个小工具,可以通过白名单机制来消除字体文件 (.ttf) 中大量的冗余符号,减小字体文件的尺寸,从而降低其包体占用和内存开销。</p> <p>我们知道,中文字体普遍个头比较大,小的 3-5 MB,大一些的 10-20 MB,有的甚至还更大一些。而通过 <a class="link" href="https://fontforge.github.io/en-US/" target="_blank" rel="noopener" >FontForge</a> 这样的工具可以看出,汉字仅占字体文件的一半不到(其他为日韩等语言和特殊符号),而其中常用 3000 字又仅占汉字总量的5%,所以理论上这个开销能缩减到原尺寸的 1/10 以内。</p> <p>(下图是我机器上已安装的字体列表)</p> <p><img src="https://gulu-dev.com/font-types.png" loading="lazy" alt="font-types.png" ></p> <hr> <h2 id="思路">思路</h2> <p>要精简这些字体的尺寸,一个简单的做法是,找到中文,英文,标点符号它们各自的起止点,把在范围之外的其他语言字符去掉。这样做逻辑上很简单,但是有几个问题,一是游戏文本中可能用到特殊字符没有在我们框定的范围内,二是汉字中有 80% 是实际上使用频率极低的冷僻字,而偏偏是这些冷僻字笔画特别多,用于描述字形的 glyph data 的数据量,比常用的简化字要大得多。</p> <p>那么有没有更好的方法呢?</p> <p>一个改进的方案是,扫描所有游戏中可能出现的文本信息,生成一个<strong>活跃字表</strong> (Active Character Table, ACT),然后把这个表之外的字符全部剔除。</p> <p>使用这种白名单的方式,我们可以更精确地释放那些冷僻字占用的空间,但需要注意的事情有两点:</p> <ol> <li>当游戏内容改变时,要及时更新我们的 ACT 表。</li> <li>聊天框的用户输入可能出现任意字符,建议直接使用系统已有的默认字体</li> </ol> <p>这个白名单方案有一个额外的好处,就是能顺便帮我我们找出所有游戏内的可显示文本,有助于统一规范化游戏内所有文本的引用,便于之后可能的本地化工作。</p> <hr> <h2 id="原理">原理</h2> <p>基于上面说的白名单方案,我们制作了工具 FontPruner,它的主要工作原理如下:</p> <p><img src="https://gulu-dev.com/1-method.png" loading="lazy" alt="p1.png" ></p> <p>简单地解释一下,FontPruner 首先收集游戏内可能出现在屏幕上的各类文本,生成白名单(也就是曾经在游戏中出现过所有字符的集合),然后调用 sfntly 对字体文件做精简处理。</p> <hr> <h2 id="流程">流程</h2> <p>具体的处理流程如下所示:</p> <p><img src="https://gulu-dev.com/2-workflow.png" loading="lazy" alt="p2.png" ></p> <p>我们可以定期在 Build 机器上执行此工具,这样游戏内容变更时,可以自动更新对应的字体文件。</p> <hr> <h2 id="效果">效果</h2> <p>接下来是精简的具体效果:</p> <p><img src="https://gulu-dev.com/3-result.png" loading="lazy" alt="p3.png" ></p> <p>上图分别是 ttf 包含 1/4/100/200/500 个中文字体时,文件尺寸的变化情况。根据该测试数据可知,在包含 5000 常用字的情况下,对于一个 28MB 的字体文件,我们可以把字体的开销控制在 3-5MB 左右。</p> <hr> <h2 id="资源">资源</h2> <p>关于此工具的更多信息,请访问下面的链接:</p> <p><a class="link" href="https://github.com/GameBuildingBlocks/FontPruner" target="_blank" rel="noopener" >https://github.com/GameBuildingBlocks/FontPruner</a></p> <p>如果基于此工具有好的想法,欢迎一起讨论~</p> <hr> <ul> <li>Written on [2017-09-08] in 2h; Posted on [2017-09-15]</li> <li>本文首发于 <a class="link" href="http://mp.weixin.qq.com/s/x-ERZMCBWkJRiRsG2GwirQ" target="_blank" rel="noopener" >西山居技术</a></li> <li>本文同时发于 <a class="link" href="https://zhuanlan.zhihu.com/p/29382867" target="_blank" rel="noopener" >知乎专栏 - 游戏人间</a></li> </ul> 2017.09 为什么说在去中心化的系统中,对权力的运用会削弱权力本身? https://gulu-dev.com/post/2017-09-07-the-power-in-dac/ Thu, 07 Sep 2017 18:05:00 +0000 https://gulu-dev.com/post/2017-09-07-the-power-in-dac/ <p>在昨晚,我在知乎的“想法”上发了<a class="link" href="https://www.zhihu.com/pin/888871030276386816" target="_blank" rel="noopener" >一条想法</a>:</p> <blockquote> <ol> <li>市场的反应证明了对 ico 的清理是及时和到位的。</li> <li>监管手段和政策客观上促使全球市场的进一步去中心化。在去中心化的市场中,对权力的运用削弱了权力本身。(反者道之动)</li> </ol> </blockquote> <p>有同学表示,第二条没看懂,能解释的详细些吗。</p> <hr> <p>这里我简单解释一下。</p> <p>为什么说在去中心化的市场上,对权力的运用会削弱权力本身呢?</p> <p>一个空旷的房间里,放着大大小小若干只杯子。有的杯子大一些,有的小一些。有的杯子里水温比较高,而有的比较低。对于那些有较高温度的大水杯(较大的权力实体)来说,与不停的搅拌相比,静置能让温度降低得更慢一些。</p> <p>同样的,对于一个规模不断扩大的去中心化系统而言,长期来看,随着系统的成长,单一实体的影响力会逐步下降。在这个过程中,提高单一实体的影响力是相对困难的,需要长期经年累月的努力(比如智能合约平台的搭建,有充分安全保障的交易所建设,央行数字货币小组的研究,等等)。这些是 hard works,也是 do good things,套用前面的比喻,是相当于用火炉把单个水杯加热;而运用手中的权力和影响力来影响,引导,甚至于操纵或是驱赶整个系统,相对而言,是短期的有高度时效性的 easy work。搅动水杯,热量会更快地释放。头一两次,会造成强大的冲击,越往后,随着温度的降低,影响会相对越来越不明显。</p> <hr> <p>举三个例子。</p> <p>2013 年底,比特币价格到达近 $1100 (RMB-8000) 时,中国人民银行等五部委联合印发的《关于防范比特币风险的通知》使得过热的市场一触即溃,价格在随后的一年里跌至 $300 以下。</p> <p><img src="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p1.png" width="798" height="440" srcset="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p1_hu120cc6ecd422c3e7681fb8293bdf3c67_33084_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p1_hu120cc6ecd422c3e7681fb8293bdf3c67_33084_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p1" class="gallery-image" data-flex-grow="181" data-flex-basis="435px" ></p> <p>今年 (2017) 一月份,<strong>央行监管</strong> 和 <strong>暂停提现</strong> 使得国内的交易量瞬间溃缩,造成的后果是全球范围内的交易所蓬勃兴起,可谓是三个(国内的)交易所抑制下去,数十个(世界各地的)交易所成长起来。</p> <p>下图是暂停提现前 (2015.09-2016.10) 两年间 Trading Volume 的分布情况,可看出绝大部分交易发生在 okcoin(黄色),火币网(蓝色),BtcChina (紫色) 三大交易所。</p> <p><img src="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p2.png" width="1096" height="460" srcset="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p2_hue41f95406aa820c29c76d23a767e1a08_24951_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p2_hue41f95406aa820c29c76d23a767e1a08_24951_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p2" class="gallery-image" data-flex-grow="238" data-flex-basis="571px" ></p> <p>下图是暂停提现后 (2017.01 之后) Trading Volume 的分布情况,可以看出,央行的暂停提现,从客观上大大促进了全球市场的进一步去中心化。</p> <p><img src="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p3.png" width="862" height="486" srcset="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p3_hu779ae7dc2fdb7918beddf95f3f6de8ff_35332_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p3_hu779ae7dc2fdb7918beddf95f3f6de8ff_35332_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p3" class="gallery-image" data-flex-grow="177" data-flex-basis="425px" ></p> <p>在此政策前,中国交易所占据全球交易量的 90% 以上,而对比上下图可看出,在此政策之后全球市场变得相对均衡得多。而下图的价格上看,此政策虽释放了很强的监管信号,但并未影响到市场的总体趋势。</p> <p><img src="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p4.png" width="898" height="435" srcset="https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p4_hu8be3ee91e5ba7610a926d8364e8f0086_37406_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-09-07-the-power-in-dac/p4_hu8be3ee91e5ba7610a926d8364e8f0086_37406_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p4" class="gallery-image" data-flex-grow="206" data-flex-basis="495px" ></p> <p>可以看到,政策对价格趋势的影响显著削弱了:</p> <p>而这一次对 ico 的清理,市场看起来并没有遭到较大的负面影响。正相反,市场在短暂的恐慌后,释放出相对平稳和积极的信号。注意,这里我们并不讨论政策本身的优劣,只是讨论它可能造成的影响力趋势的变化。</p> <p>这是第一个例子,央行的三次主要出手,对于市场的整体影响力是<strong>逐渐降低</strong>的。再强调一下,这里我们不讨论主观上的意图,也不评价政策的优劣与好坏,只是看客观上的效果和影响范围。</p> <hr> <p>在持续了两年多的扩容之争里,Core 曾多次体现出其意图,尝试运用自己的影响力来引导和驱动整个社区,这从它对香港共识的前后态度,对 reddit 社区不同声音的处置方式,对先后出现的替代方案的不遗余力的攻击,都可以看出。</p> <p>那么实际效果如何呢,虽然 next / classic / unlimited 成了明日黄花,但 btc1 (segwit2x) 和 bitcoin abc (Bitcoin Cash)却浮出水面,成为有竞争力的实现。</p> <p>对权力的运用会削弱权力本身,这是第二个例子。</p> <hr> <p>我们知道,在比特币还是“一个人的玩具”时,为了让区块链可以持续工作,Satoshi 用单台计算机持续挖出了早期绝大部分比特币,数量约百万枚左右。这百万枚币足以影响任何时期的市场,但挖出后却从未被 Satoshi 动用过。(“生而不有,为而不恃”)这种程度的 Integrity 是有力的保证。正如 Satoshi 的处置这样,当在一个系统中影响力达到极大时,应采取的行动反而更应极尽克制而谨慎。(“圣人处无为之事 行不言之教”)</p> <p>想象一下,在另一个平行宇宙里,Satoshi 不仅没有彻底消失,而且时不时地去变现,在不同的交易市场和代码分支上投机和套利。那么很显然,他不可能像现实世界里这样,成为一个事实上的“传说”。(“夫唯弗居,是以不去”)</p> <p>(Satoshi 的最后一次露面是在2014年,人们怀疑 Dorian Nakamoto 就是他之后)</p> <blockquote> <p>I am not Dorian Nakamoto.</p> <ul> <li>Satoshi Nakamoto</li> </ul> </blockquote> <p><a class="link" href="https://bitcointalk.org/" target="_blank" rel="noopener" >bitcointalk.org</a> 上的<a class="link" href="https://bitcointalk.org/index.php?topic=763459.msg8603421#msg8603421" target="_blank" rel="noopener" >网友评论道</a>:</p> <blockquote> <p>&ldquo;i am not dorian nakamoto&rdquo; is written by real satoshi account. It doesn&rsquo;t matter who write it. It also doesn&rsquo;t matter who is the real satoshi, as long as first blocks remain unspent. That&rsquo;s the beauty of open source and Bitcoin.</p> </blockquote> <p>为了避免在这个系统中被认为是独一无二的主导,Satoshi 功成身退,完美地扮演了中国传统道家理论中“圣人”的角色。而一个高度理想化的去中心化社会,也正是所谓“小国寡民”的愿景。</p> <blockquote> <p>是以圣人处无为之事,行不言之教;万物作焉而不辞,生而不有,为而不恃, 功成而弗居。夫唯弗居,是以不去。 —— 老子《道德经》</p> </blockquote> <ul> <li>Written in 2h on [2017-09-07]</li> </ul> 2017.08 道可道,非常道;名可名,非常名。 https://gulu-dev.com/post/2017-08-29-tao/ Tue, 29 Aug 2017 22:11:00 +0000 https://gulu-dev.com/post/2017-08-29-tao/ <img src="proxy.php?url=https://gulu-dev.com/post/2017-08-29-tao/title.jpg" alt="Featured image of post 2017.08 道可道,非常道;名可名,非常名。" /><p>“道可道,非常道;名可名,非常名。”</p> <p>一直以来,道德经的第一章第一句以 “道可道” 作为开篇,有为全文划定基础,提纲挈领的重要作用。而这句话由于高度的概括性和抽象性,向来都不那么容易理解,就好像被算法加密过,我们很难跨越漫长时空对其解密,得到明文。而如果没有真正意义上理解这句话,就很难说读懂了道德经。</p> <hr> <p>概括地讲,这句话有下面这几种常见的解释:</p> <ol> <li>(林语堂) 可以说出来的道,便不是经常不变的道;可以叫的出来的名,便不是经常不变的名。</li> <li>(陈鼓应) (同上) 可以用言辞表达的道,就不是常道;可以说的出来的名,就不是常名。</li> <li>(释德清) 真常之道,本无相无名,不可言说。凡可言者,则非真常之道矣,故非常道。</li> </ol> <p>还有两个解释是知乎网友那里看到的,很有意思,一并收录:</p> <ul> <li><a class="link" href="https://www.zhihu.com/question/20393827/answer/37391226" target="_blank" rel="noopener" >@in-nek</a> <ul> <li>道, 基本概念是一条路,引申出来就是事物延伸出去的那个轨迹,再引申就是“规律”,或者说是最优的解决方案。规律是可以被说清楚的,或者可以被跟随的,或者可以遵循的……“道”是可以“道”的,但不是那种你想象中的静态的“道”,它是演变的,depend(依赖)的,递归的。对道进行描述,是道下一个发展的其中一个输入,改变这个输入,会改变道的发展本身。</li> </ul> </li> <li><a class="link" href="https://www.zhihu.com/question/20175885/answer/14452019" target="_blank" rel="noopener" >@Thruth</a> <ul> <li>(世间的一切)方法(啊),那些被众人所认可所以为美的方法,都不是达到永恒的正确方法</li> <li>(世间的一切)认知(啊),那些被众人所认可所以为美的认知,都不是对永恒的正确认知</li> </ul> </li> </ul> <hr> <p>说完了这些高人的看法,也讲一下我的认识吧,</p> <hr> <p><code>(“道可道,非常道”)</code> 规律可以被认识,进而用来<strong>对具体的人和事进行指引</strong>,(一旦这么做了) 具有普适性的规律就转化成了具体的有针对性的方法,就不再能被认为是一般性的规律,只是一种体现(即所谓的“相”)了。这里面,第二个“道”字是关键,我的理解是,它既不是“知道,了解,认识”,也不是“被用来表达”,而是一个直接引用本义的动词——“被用来(以道的方式)引导”。和“圣人不病,以其病病”里的第二个“病”字是类似的表达。</p> <p><code>(“名可名,非常名”)</code> 我们提炼概念,创造出名称,从而可以拿来指代事物;但这种从名到实的指代关系总是临时的,易变的,这是因为良好定义的概念和名称,是精确的,固定的,只能映射到事物一时一地的特征,而事物本身却在不断发展变化。二者会不断地背离,于是事物总是会偏离我们指定的道和名(理想与现实的偏差),却有内在的向“常道”和“常名”靠拢的动力,这就是“反者道之动”。</p> <p>除此之外,“道”还可以有很多角度,比如“性质”(事物具有的内在特性),从性质的角度看,我们可以说“质可质,非常质”——事情或事物的性质,可以用具体的特性来描述,认识,概括,表达,但任何具体的特性本身无法涵盖和超越所有特性的集合,也就无法代表其整体的性质,尤其是考虑到时间的变化因素)</p> <p>这里举出的三个角度,分别是客观规律(“道”),事物的外部特征(“名”),事物的内部特征(“质”)。</p> <p>总得说来,这十二个字表达了一种约束 (constraint),作为讨论的基础,也就是《道德经》在最底层的根本逻辑上的强约束——世间的万事万物,作为道的衍生,无论是从规律角度,还是从特征或性质等角度,都具有无法超脱的局限性。认识了这种普遍的局限性,反过来才可以理解“道”作为本体的超越性和完备性。</p> <hr> <p><img src="https://gulu-dev.com/post/2017-08-29-tao/tao.jpg" width="962" height="441" srcset="https://gulu-dev.com/post/2017-08-29-tao/tao_hu4081f15a7c7d08edcd223c19136c2122_89703_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-08-29-tao/tao_hu4081f15a7c7d08edcd223c19136c2122_89703_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tao" class="gallery-image" data-flex-grow="218" data-flex-basis="523px" ></p> <hr> <p>在结束前,举两个例子吧。</p> <ul> <li> <p>2008 年的四万亿,把中国从经济危机的边缘拉了回来(道可道),但现时对此政策的利弊和后续影响的讨论和反思却越来越多。(非常道)</p> </li> <li> <p>“对不起,我是警察。”(名可名) “有谁知道?!”(非常名)</p> </li> </ul> 2017.08 To Da Moon (for the 5th time) https://gulu-dev.com/post/2017-08-13-to-da-moon/ Sun, 13 Aug 2017 10:20:00 +0000 https://gulu-dev.com/post/2017-08-13-to-da-moon/ <img src="proxy.php?url=https://gulu-dev.com/post/2017-08-13-to-da-moon/to-the-moon.jpg" alt="Featured image of post 2017.08 To Da Moon (for the 5th time)" /><p>An interesting comparison:</p> <ul> <li><a class="link" href="https://newzoo.com/insights/articles/the-global-games-market-will-reach-108-9-billion-in-2017-with-mobile-taking-42/" target="_blank" rel="noopener" >The global games market will reach $108.9 billon in 2017 with mobile taking 42%</a></li> </ul> <p><img src="https://gulu-dev.com/post/2017-08-13-to-da-moon/2017-game-market.png" width="4000" height="2250" srcset="https://gulu-dev.com/post/2017-08-13-to-da-moon/2017-game-market_hu50c38e3dbc3e58949598962b17c2d258_188231_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-08-13-to-da-moon/2017-game-market_hu50c38e3dbc3e58949598962b17c2d258_188231_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p1" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <ul> <li><a class="link" href="https://coinmarketcap.com/charts/" target="_blank" rel="noopener" >Total market capitalization for all cryptocurrencies (last 3 months, May12-Aug12)</a></li> </ul> <p><img src="https://gulu-dev.com/post/2017-08-13-to-da-moon/2017-crypto-market-cap.png" width="728" height="404" srcset="https://gulu-dev.com/post/2017-08-13-to-da-moon/2017-crypto-market-cap_hu5b1614cebb5355238da6c25fbba853c4_22301_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-08-13-to-da-moon/2017-crypto-market-cap_hu5b1614cebb5355238da6c25fbba853c4_22301_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p2" class="gallery-image" data-flex-grow="180" data-flex-basis="432px" ></p> <hr> <p>2017 would be a big year for Bitcoin. The price of Bitcoin is somewhere nearby $4000 on [2017-08-13 08:37], and it wouldn&rsquo;t surprise me if it reaches $5000 sometime before November (the deployment of &ldquo;the 2MB part&rdquo;). Beyond the price bubble (which is on its way), it&rsquo;s highly possible that more hard-forks would show up at the end of the year, and the price of the longest POW chain (<strong>the</strong> Bitcoin by its definition) might fall back to $1200-$1500.</p> <hr> <p>3 potential possibilities for the next six-months from my perspective:</p> <ol> <li> <p>Segwit2x wins (&gt; 80%). Bitcoin starts a new era to <strong>scale periodically</strong>.</p> <ul> <li>Core(and the LN side chain solution) would be marginalized to be an enterprise-level solution provider.</li> <li>The future of Bitcoin development would be directed by &ldquo;the Knights of the Round Table&rdquo;.</li> <li>New BIPs would be more business-driven than tech-driven (which is a &ldquo;maturity sign&rdquo; to Bitcoin).</li> </ul> </li> <li> <p>Three (or more) forks live peacefully and separately. Bitcoin would become a more generalized concept of <strong>Bitcoin Family</strong> spectrum distribution.</p> <ul> <li>&ldquo;the Family&rdquo; consists of Core, btc1, Bitcoin ABC, etc&hellip;</li> <li>It would be a quite different model to &ldquo;Linux Kernel and Distributions&rdquo;, since there isn&rsquo;t a single &ldquo;root&rdquo; to drive the development and longterm vision.</li> </ul> </li> <li> <p>Core wins (&gt; 80%). The primary blockchain is locked at 1MB limit and <strong>fully controlled</strong> by Core.</p> <ul> <li>It&rsquo;s highly possible that Core is (and would be) the most strong dev-team on the planet.</li> <li>It&rsquo;s very likely that new emerging forks would make mistakes (especially when they are moving away from the most sophisticated implementation)</li> <li>Core would try its best to exploit any chance of vulnerabilities of the alternatives to strive to survive.</li> </ul> </li> </ol> <p>Gu Lu</p> <ul> <li>Written in 1.5h on [2017-08-13] at $4034.89 on Coindesk.</li> <li>Arguments in this article don&rsquo;t provide any form of financial advices for readers.</li> </ul> 2017.07 百万张高端显卡的30天集结 https://gulu-dev.com/post/2017-07-17-one-million-video-cards/ Mon, 17 Jul 2017 06:04:00 +0000 https://gulu-dev.com/post/2017-07-17-one-million-video-cards/ <p><img src="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/title.png" width="1064" height="505" srcset="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/title_hu35f7129c804ba5d27d4195c337fbb49e_186740_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-07-17-one-million-video-cards/title_hu35f7129c804ba5d27d4195c337fbb49e_186740_1024x0_resize_box_3.png 1024w" loading="lazy" alt="title" class="gallery-image" data-flex-grow="210" data-flex-basis="505px" ></p> <blockquote> <p>“全世界高端显卡,联合起来!”</p> </blockquote> <p>在过去的 30 天里,数以百万计的高性能的显卡涌入了以太坊 (ETH) 的算力网络。</p> <hr> <p>随着价格一路飙升,这段时间 ETH 挖矿的利润达到了前所未有的水平。而 ETH 的 POW 算法 EtHash (<a class="link" href="https://github.com/ethereum/wiki/wiki/Ethash-Design-Rationale" target="_blank" rel="noopener" >Ethash Design Rationale</a>) 目前还无法 ASIC 化,只能使用高端显卡来加速。大量的高端显卡在短期内迅速集结,ETH 全网算力形成了一个可怕的增长曲线。</p> <p><img src="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-1.png" width="1179" height="502" srcset="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-1_hub9d81f631141ada80b5d1d2251d47cfb_24820_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-1_hub9d81f631141ada80b5d1d2251d47cfb_24820_1024x0_resize_box_3.png 1024w" loading="lazy" alt="eth-1" class="gallery-image" data-flex-grow="234" data-flex-basis="563px" ></p> <p>从 2017-06-09 到 2017-07-09 的 30 天期间 (下图的阴影区域),ETH 全网算力由 39.5T 增长到 64.8T,增长的算力达到 25.3T——而一张 AMD 的 RX 570/580 显卡的算力通常是 22M-24M (修改 BIOS 优化后可达到 28M)。假设至少有一半的显卡被优化,那么通过简单的运算可以得到,仅下图选中的一个月内,需要百万张高端显卡集结入 ETH 的算力网络,才能形成这样的增长。</p> <p><img src="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-2.png" width="1166" height="485" srcset="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-2_hu6be45b76d7471b79acade9696ae2a9ef_26502_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-2_hu6be45b76d7471b79acade9696ae2a9ef_26502_1024x0_resize_box_3.png 1024w" loading="lazy" alt="eth-2" class="gallery-image" data-flex-grow="240" data-flex-basis="576px" ></p> <p>在这段时间内,这些显卡本身也由于缺货,从 RMB 1500-2000 的价格一路升至 3000-3500。假设按照较低的 2000 来计算,这 30 天内集结的显卡,至少价值 20 亿人民币。</p> <hr> <p>在这 30 天的头 10 天里,ETH 的价格一直维持在 RMB-2500 的历史高位上。这使得<a class="link" href="http://coinmarketcap.com/charts/" target="_blank" rel="noopener" >整个加密货币的市值分布</a>里,ETH 的市值比例迅速提高,到 2017-06-17 时,占比几乎追上了比特币 (BTC) 。</p> <p><img src="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-3.png" width="1107" height="755" srcset="https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-3_hu7f8ba46e98c46d2ca7689b7301bf9ef6_105803_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-07-17-one-million-video-cards/eth-3_hu7f8ba46e98c46d2ca7689b7301bf9ef6_105803_1024x0_resize_box_3.png 1024w" loading="lazy" alt="eth-3" class="gallery-image" data-flex-grow="146" data-flex-basis="351px" ></p> <p>在上图中的 2017-06-17,比特币的市值占比跌到了历史的低点 38.44%,而 ETH 在一个多月内由 5.32% 上升到 31.93%。考虑到比特币本身也在同期市值翻倍,这个比例的变化就更惊人了。 然而后来发生的事我们都知道了,从那时起开始的下跌中(本文写于 2017-07-17, 距离 2017-06-17 正好一个月),比特币的表现相对稳健,在整体缩水中,占比逐渐稳定地回升。虽然回到此前 80% 的统治性地位还很遥远,但进一步拉大与各种竞争币的差距,是可以期待的。</p> <hr> <p>持续了将近一个月的下跌,吹响了 2017 年上半场结束的哨声。而随着比特币扩容进入关键的全网部署阶段,今年下半场的好戏才刚刚开始。如果时间允许的话,我会陆续地写一些区块链技术文章,和一些有趣的行业观察和评论。</p> <p>Stay tuned.</p> <hr> <ul> <li>[2017-07-17] initially written. (1h)</li> <li>本文知乎链接: <a class="link" href="https://zhuanlan.zhihu.com/p/27920105" target="_blank" rel="noopener" >https://zhuanlan.zhihu.com/p/27920105</a></li> <li>本文 Blog 链接: <a class="link" href="http://gulu-dev.com/post/2017-07-17-one-million-video-cards" target="_blank" rel="noopener" >http://gulu-dev.com/post/2017-07-17-one-million-video-cards</a></li> <li>本文遵循 <a class="link" href="http://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener" >Creative Commons BY-NC-ND 4.0</a> 许可协议。</li> </ul> 2017.07 软件工程师的睡前二十问 https://gulu-dev.com/post/2017-07-04-20-questions/ Tue, 04 Jul 2017 19:31:00 +0000 https://gulu-dev.com/post/2017-07-04-20-questions/ <img src="proxy.php?url=https://gulu-dev.com/post/2017-07-04-20-questions/introspection.png" alt="Featured image of post 2017.07 软件工程师的睡前二十问" /><p>作为一个工程师,很早以前,我就养成了每周回顾 (Weekly Retrospective) 的习惯。可是直到不久前我才发现,每天晚上睡前的小回顾也很有帮助。以下是我在前段时间形成这个习惯的过程中,积累起来的二十个问题——工作和生活各十个。</p> <p>每天晚上睡前,我会用这个二十个问题穿针引线,帮助自己回顾一天的进展与得失。有了这个列表,就省得自己在困得精神恍惚时,还要茫然地问自己:“今天究竟干了些啥?”,也避免了想到哪儿写到哪儿的流水账。</p> <h3 id="工作">工作</h3> <ol> <li><strong>Completeness</strong>: 计划中的任务是否完成? <ul> <li>完成得勉强吗?(存在为了多做事情而把时间排得过满的情况吗?)</li> <li>完成得干净吗?(如果有留待日后处理的事项,记录下来了吗?)</li> <li>对完成的工作,要点,疑难和备忘等信息,记录在 Journal 里了吗?</li> <li>对未完成的工作,已经安置,并处于受控可恢复的搁置状态了吗?</li> </ul> </li> <li><strong>Scheduling</strong>: 这一天的工作计划本身合理吗?计划之外的情况有哪些?对这些情况能有更好的预见性吗?</li> <li><strong>Competance</strong>: 有感觉自己的能力无法胜任的任务吗?为此有进一步的打算和安排吗?</li> <li><strong>Responsiveness</strong>: 对于各种突发情况,同步处置(立即响应)和异步处置(搁置并延后响应)安排得合理吗?</li> <li><strong>Automation</strong>: 对这一天里完成的工作内容,有可以自动化或原子化的部分(以便日后以更小代价随时调用)吗?</li> <li><strong>Inspiration</strong>: 对这一天里闪现的有价值的想法和方案,是否做了妥善的处置?</li> <li><strong>Visibility</strong>: 对有业务交集的同事,你的工作对他们有价值吗?对他们足够可见吗?他们会关心并产生合理的质疑吗?</li> <li><strong>Dependency</strong>: 对这一天里出现的因为对外界的各类依赖而导致的等待或低效工作的时间,有应对或改善的方法吗?</li> <li><strong>Time Quality</strong>: 在这一天里,拥有的未被打断或干扰的“优质”工作时间有几个小时?这些优质时间“值回票价”了吗?</li> <li><strong>Overtime</strong>: 在这一天里,如果有加班,正常工作时段和加班时段各占工作价值的百分之多少?这些加班时间“值回票价”了吗?</li> </ol> <h3 id="生活">生活</h3> <ol> <li>作为一个理工男,我有在“没什么道理可讲”的时候强行讲道理吗?有太啰嗦的情况吗?</li> <li>在工作之外,我预留出足够的精神力量来主动调节和改善家庭氛围和温度吗?</li> <li>我关心家人的心情和感受了吗?跟他们分享开心的事情了吗?无意识地传递负面情绪了吗?</li> <li>家庭的短期,中期,长期的目标清晰和明确吗?财务状况的处置合理吗?</li> <li>在社会交往中,有公私事务处置失当,言谈举止失当,或者事后觉得可以做得更好的情况吗?</li> <li>有尝试认识新的朋友吗?他们可能带来的短期价值和长远价值分别在于哪些方面?</li> <li>在已有的社会交往中,寻找和巩固价值支撑点和提供点了吗?有新的价值发现吗?</li> <li>在这一天里,有没有主动去做“与眼下的工作和生活关联不大,却为长远目标服务”的事?</li> <li>这一天有花时间去做沉浸式的阅读,探索和思考吗? <ul> <li>有整理新吸收的信息,并对短期价值或长期价值做区分了吗?</li> </ul> </li> <li>这一天过得还好吗? <ul> <li>疲劳吗?休息时间够吗?</li> <li>焦虑感和压力上升了还是下降了?</li> <li>情绪的积极度如何?</li> <li>情绪的稳定性如何?</li> <li>有独处的 alone moments 吗?</li> <li>足够“真”吗?够勇敢吗?</li> <li>在入睡前,还有什么放不下的事情吗?</li> </ul> </li> </ol> <p>好了,这就是我入睡前会问自己的二十个问题。刚开始这个习惯时,我需要花上15-20分钟,而现在养成习惯以后,十分钟足矣。回答这些问题之后,往往心情平静,安稳而踏实,睡眠质量也改善了不少。希望对屏幕前你也能有所帮助和启发。</p> <hr> <ul> <li>[2017-07-04] initially written (1h)</li> </ul> <hr> <ul> <li>本文首发于西山居技术中心公众号 (<a class="link" href="http://mp.weixin.qq.com/s/hURSKfh9XncRBEoc3RketQ" target="_blank" rel="noopener" >链接</a>)</li> <li>本文同时发在我的知乎专栏 (<a class="link" href="https://zhuanlan.zhihu.com/p/27737076" target="_blank" rel="noopener" >链接</a>)</li> <li>永久链接: <a class="link" href="http://gulu-dev.com/post/2017-07-04-20-questions" target="_blank" rel="noopener" >http://gulu-dev.com/post/2017-07-04-20-questions</a></li> <li>本文遵循 <a class="link" href="http://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener" >Creative Commons BY-NC-ND 4.0</a> 许可协议。</li> </ul> <hr> <p>另:非常喜欢这条评论~</p> <p><img src="https://gulu-dev.com/post/2017-07-04-20-questions/comment.jpg" width="1078" height="574" srcset="https://gulu-dev.com/post/2017-07-04-20-questions/comment_hudc5b2a3772c28e648e11cbc49ca6f145_99717_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-07-04-20-questions/comment_hudc5b2a3772c28e648e11cbc49ca6f145_99717_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="comment" class="gallery-image" data-flex-grow="187" data-flex-basis="450px" ></p> 2017.05 Telemetry 3 特性指南 (2017) https://gulu-dev.com/post/2017-05-18-telemetry/ Thu, 18 May 2017 07:36:00 +0000 https://gulu-dev.com/post/2017-05-18-telemetry/ <p>Telemetry 是一款 CPU 性能剖析工具,最大的特点是能够在对游戏运行影响非常小的情况下获取非常详尽的运行时信息。以前在 Unreal 上工作时,我曾短期使用过 Telemetry 2,当时让我大开眼界的是,它不仅能对性能数据做详尽直观的可视化展现,而且有着难得的实时响应和平滑流畅的操作体验——这种数据采集实时性和操作响应实时性结合起来,使得跟其他的工具比起来,Telemetry 出奇的容易使用——基本上有经验的程序员捡起来,不到一分钟就能开始做分析和诊断了。</p> <p><img src="https://gulu-dev.com/post/2017-05-18-telemetry/tele_0.png" width="2560" height="1560" srcset="https://gulu-dev.com/post/2017-05-18-telemetry/tele_0_hu332b1223e8c7699632de33c81caf24c8_432276_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-05-18-telemetry/tele_0_hu332b1223e8c7699632de33c81caf24c8_432276_1024x0_resize_box_3.png 1024w" loading="lazy" alt="tele_0" class="gallery-image" data-flex-grow="164" data-flex-basis="393px" ></p> <p>前段时间我有机会看到了新版的 Telemetry 3 演示。其中让我印象最深的一个新版本功能,就是<strong>可视化的线程相关性</strong>——新的 Telemetry 可以用色块高亮提示不同线程之间 lock/wait 等同步的情况。现场拍了一张,见下图:</p> <p><img src="https://gulu-dev.com/post/2017-05-18-telemetry/tele_1.jpg" width="800" height="600" srcset="https://gulu-dev.com/post/2017-05-18-telemetry/tele_1_hu250944e54f44ad186c078cedfcfec7e8_99819_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-05-18-telemetry/tele_1_hu250944e54f44ad186c078cedfcfec7e8_99819_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tele_1" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>上图中,当鼠标指向某个线程的某一段执行开销时,绿色的色块高亮出与之相关的其他线程的活动。这个功能不仅让性能消耗一目了然,而且对辅助理解程序的行为也大有帮助(尤其是 deadlock / contention 相关的问题的调查)。</p> <p>由于 Telemetry 是支持写入到文件的,我们可以让 QA 的同学开着跑这一类难以重现的问题,跑出问题时直接上传录下来的文件就好了。顺便说一句,Telemetry 的 overhead 很低,据说新版本每秒钟可以有上百万个计时块,对宿主本身执行效率的干扰很低。</p> <p><img src="https://gulu-dev.com/post/2017-05-18-telemetry/tele_2.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2017-05-18-telemetry/tele_2_hu5767afa334eb18fee5852b1b505b425f_56265_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-05-18-telemetry/tele_2_hu5767afa334eb18fee5852b1b505b425f_56265_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tele_2" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>注意,观察上图中每个线程时间线上的标签,可以看到,这个线程在那一刻活跃的执行时间内跑在哪个物理核上,以及对应的 context switch 时间开销。(现场没拍照,这是视频截图,不够清晰,见谅)</p> <p>所有锁的时间开销汇总在最后,这样可以得到线程间共享资源的整体利用情况。</p> <p><img src="https://gulu-dev.com/post/2017-05-18-telemetry/tele_3.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2017-05-18-telemetry/tele_3_hu207658ca6bd914a65853dd1151ba68cb_35979_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-05-18-telemetry/tele_3_hu207658ca6bd914a65853dd1151ba68cb_35979_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tele_3" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>上图中可以看到按照CPU上每个核的执行情况组织的时间线。这样就可以针对不同的上下文灵活安排更合理的 thread affinity。</p> <p><img src="https://gulu-dev.com/post/2017-05-18-telemetry/tele_4.jpg" width="800" height="450" srcset="https://gulu-dev.com/post/2017-05-18-telemetry/tele_4_hua9ecabc6bd391c9482795c3e6687dcaf_38967_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-05-18-telemetry/tele_4_hua9ecabc6bd391c9482795c3e6687dcaf_38967_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tele_4" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>上图中可以看到手动打点的事件,这些定制事件可以跟内建的性能统计结合起来看。对那些在时间线上(单帧内)不是很显眼的函数,也可以通过汇总的列表来查看在整个生命周期中的累计开销情况。</p> <p><img src="https://gulu-dev.com/post/2017-05-18-telemetry/tele_5.jpg" width="800" height="600" srcset="https://gulu-dev.com/post/2017-05-18-telemetry/tele_5_hubc58815c9e41e507ff079a67ffd4cdd6_126705_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-05-18-telemetry/tele_5_hubc58815c9e41e507ff079a67ffd4cdd6_126705_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tele_5" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>在与 RAD 的工程师交流时,他们提到,RAD 正在开发 Telemetry 的 Mac 版。对于使用 Unity 来开发 iOS 移动游戏的团队来说,这不啻是打开了 Unity Profiler 之外的另一扇门。由于 Unity 工程的 iOS 版本经由 il2cpp 转化为 C++ 代码,理论上仅需对生成的 C++ 代码做简单的集成,即可使用 Telemetry 剖析 iOS 上的应用性能情况。这对于工具链相对不足的 mac/ios 来说,应会有不小的助益。</p> 2017.04 Visual Assist 特性和技巧 (2017) https://gulu-dev.com/post/2017-04-16-visual-assist-tips/ Sun, 16 Apr 2017 07:36:00 +0000 https://gulu-dev.com/post/2017-04-16-visual-assist-tips/ <img src="proxy.php?url=https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/title-0.jpg" alt="Featured image of post 2017.04 Visual Assist 特性和技巧 (2017)" /><p>(本文约 5400 字,阅读时间约 15 min)</p> <p><a class="link" href="http://www.wholetomato.com/" target="_blank" rel="noopener" >Visual Assist</a> 多年来始终是 Visual Studio 开发环境下的一款难以逾越的经典辅助插件。难得的是,这些年来它的重心一直围绕着 C++ 程序的编写辅助和增强。近年来,逐渐开放的微软加快了 Visual Studio 的演进速度,持续地吸收了不少 VA 的特性,但多年的积累使得 VA 的对应实现仍有着很强的竞争力。就拿代码重构里最基本的“重命名”来说,VA Rename 就比 VS Rename 步骤更少,速度更快,也更为方便和顺手一些。</p> <p>在上个月偶然间了解到新加的静态分析功能之后,我这两天下载了最新版本的 VA,发现比自己用了好一阵的老版本多了不少新东西,就想试着集中梳理一下。这篇文章里,那些经典功能如符号查找 (Find Symbols / Find References),函数列表 (List Methods in File),代码片段 (VA Snippets) 我就不再多说了,只是对相对较新的特性和技巧做了整理。</p> <h2 id="特性和功能">特性和功能</h2> <h3 id="code-inspection-静态分析和快速修正">Code Inspection 静态分析和快速修正</h3> <p>VA 开始支持基于 LLVM/Clang 的代码静态分析了。与 VS 原生的 Code Analysis 不同的是,VA 支持直接<strong>一键修复</strong>提示的问题。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci1.png" width="305" height="64" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci1_hu658fc0eba0a6b163ec0244aeca919503_7396_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci1_hu658fc0eba0a6b163ec0244aeca919503_7396_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ci1" class="gallery-image" data-flex-grow="476" data-flex-basis="1143px" ></p> <p>可以在汇总窗口里查看所有静态分析时找到的问题。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci2.png" width="455" height="191" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci2_hu43adea53978bf6a229a5b2c2c35cb922_6547_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci2_hu43adea53978bf6a229a5b2c2c35cb922_6547_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ci2" class="gallery-image" data-flex-grow="238" data-flex-basis="571px" ></p> <p>并选中关心的项目,右键 “Apply Quick Fixes” 修复。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci3.png" width="460" height="132" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci3_hudb0cf8bc5892ca972aa097e36d9490f3_5350_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci3_hudb0cf8bc5892ca972aa097e36d9490f3_5350_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ci3" class="gallery-image" data-flex-grow="348" data-flex-basis="836px" ></p> <p>对那些不关心的项目,也可以在选项里关掉单条提示类型,或是调整它们所在的 Level (就像 Warning Level 那样)。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci4.png" width="615" height="94" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci4_huf1aeddcaed79bd1db5058b202c9cf258_6795_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/ci4_huf1aeddcaed79bd1db5058b202c9cf258_6795_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ci4" class="gallery-image" data-flex-grow="654" data-flex-basis="1570px" ></p> <h3 id="debugging---va-step-filter">Debugging - <strong>VA Step Filter</strong></h3> <p>可以显式指定调试的时候自动跳过的函数 (比如标准库的函数 std::sort()),比 VS 自带的 autoexp.dat 好用很多</p> <ul> <li>可随时把当前函数添加到过滤列表,并即时生效</li> <li>可随时开启关闭任意一个指定的函数 (就像开启关闭一个或多个断点)</li> </ul> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/stepFilterBuiltInFilters.png" width="328" height="149" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/stepFilterBuiltInFilters_hu6dc837e870aa55ba443aa6ace140f93c_4273_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/stepFilterBuiltInFilters_hu6dc837e870aa55ba443aa6ace140f93c_4273_1024x0_resize_box_3.png 1024w" loading="lazy" alt="stepFilterBuiltInFilters" class="gallery-image" data-flex-grow="220" data-flex-basis="528px" ></p> <h3 id="debugging---address-resolver">Debugging - <strong>Address Resolver</strong></h3> <p>当 callstack 仅包含地址信息时,可以用这个工具来还原堆栈。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/addressResolver.png" width="721" height="281" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/addressResolver_hu88255df4d6b1509c4a7376f414855699_10865_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/addressResolver_hu88255df4d6b1509c4a7376f414855699_10865_1024x0_resize_box_3.png 1024w" loading="lazy" alt="addressResolver" class="gallery-image" data-flex-grow="256" data-flex-basis="615px" ></p> <p>关于定位 pdb,基址设置等方面更多的技巧在<a class="link" href="http://docs.wholetomato.com/default.asp?W718" target="_blank" rel="noopener" >这里</a>。</p> <h3 id="debugging---pdb-explorer">Debugging - <strong>Pdb Explorer</strong></h3> <p>这个工具可以查看,搜索和定位 pdb 内的符号。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/pdbExplorer.png" width="524" height="366" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/pdbExplorer_hucb9c2ce92b140a393df695cbea465a80_21646_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/pdbExplorer_hucb9c2ce92b140a393df695cbea465a80_21646_1024x0_resize_box_3.png 1024w" loading="lazy" alt="pdbExplorer" class="gallery-image" data-flex-grow="143" data-flex-basis="343px" ></p> <p>在调试的时候,可以直接把特定的符号拖到 Disassembly 窗口,非常方便。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/explorerGoto.png" width="584" height="499" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/explorerGoto_hu02e4d1df356d246f5b8b3e894e76dc18_23382_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/explorerGoto_hu02e4d1df356d246f5b8b3e894e76dc18_23382_1024x0_resize_box_3.png 1024w" loading="lazy" alt="explorerGoto" class="gallery-image" data-flex-grow="117" data-flex-basis="280px" ></p> <p>此工具更多的信息在<a class="link" href="http://docs.wholetomato.com/default.asp?W719" target="_blank" rel="noopener" >这里</a>。</p> <h3 id="debugging---memory-view">Debugging - <strong>Memory View</strong></h3> <p>用来调试 Dump 的内存查看窗口,可以显示特定的地址处的情形,通常配合 Disassembly 窗口和 Memory 窗口使用。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaMemoryViewDissasembly.png" width="632" height="334" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaMemoryViewDissasembly_hu1cda7613f9f45b5880b717d619c009ed_16435_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaMemoryViewDissasembly_hu1cda7613f9f45b5880b717d619c009ed_16435_1024x0_resize_box_3.png 1024w" loading="lazy" alt="vaMemoryViewDissasembly" class="gallery-image" data-flex-grow="189" data-flex-basis="454px" ></p> <p>此工具更多的信息在<a class="link" href="http://docs.wholetomato.com/default.asp?W660" target="_blank" rel="noopener" >这里</a>。</p> <h3 id="快捷键设定窗口">快捷键设定窗口</h3> <p>这个全新设计的面板列出了 VA 的所有快捷键及作用域,并可以跳转到 VS 的设置面板上去修改。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hotkey.png" width="623" height="719" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hotkey_hue578462bbef3d43c2f9dda841a170e3a_32386_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hotkey_hue578462bbef3d43c2f9dda841a170e3a_32386_1024x0_resize_box_3.png 1024w" loading="lazy" alt="hotkey" class="gallery-image" data-flex-grow="86" data-flex-basis="207px" ></p> <p>但是我实际上很少更改默认的快捷键设置,因为工作需要,会需要在不熟悉的机器上的 VS 内工作,如果自己的快捷键是定制的,就常常会遇到肌肉记忆不断失败的情况。总是用默认的快捷键位就可以避免这一点。</p> <h2 id="技巧">技巧</h2> <h3 id="开启-ctrl--鼠标滚轮对-smart-select-的支持">开启 Ctrl + 鼠标滚轮对 <strong>Smart Select</strong> 的支持</h3> <p>在新版的 VA 的选项 Options | Mouse 中找到下面两项并按照下图中的方式选择</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/mouse.png" width="488" height="63" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/mouse_hu43fc5c157854841eb7e1e3343dc072a7_3871_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/mouse_hu43fc5c157854841eb7e1e3343dc072a7_3871_1024x0_resize_box_3.png 1024w" loading="lazy" alt="mouse" class="gallery-image" data-flex-grow="774" data-flex-basis="1859px" ></p> <p>我们知道 Ctrl + 鼠标滚轮本来是用来即时调整字体大小的,但实际上很少会如此频繁地去放大和缩小代码的字体,有时候还容易造成误操作。换成 Smart Select 之后就可以非常快捷地选择光标所在的代码块了,比如说你的光标在下面的代码段里注释所在的地方:</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaSmartSelect.png" width="452" height="419" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaSmartSelect_hufc4aafd64e575f92f6715c70fea91603_7249_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaSmartSelect_hufc4aafd64e575f92f6715c70fea91603_7249_1024x0_resize_box_3.png 1024w" loading="lazy" alt="vaSmartSelect" class="gallery-image" data-flex-grow="107" data-flex-basis="258px" ></p> <p>那么按下 Ctrl 后,随着滚动鼠标滚轮,你会选中 if 的所有代码,继续滚动的话,接下来是 for 的所有代码,然后逐级向上,DoSomething,Bar,Foo,最后是整个文件。(选中顺序与图中左侧折叠代码块的 Outlining Bar 保持一致)而反向滚动则会逐级退回。熟练以后你会发现这个功能非常好使,会很少再需要手动去掐头去尾,选中一段代码了。</p> <h3 id="va-hashtags-增强注释可跳转可聚合的代码内书签"><strong>VA Hashtags</strong> 增强注释——可跳转可聚合的代码内书签</h3> <p>比书签 Bookmark 更好的是,hashtags 是代码的一部分,所有人共享并签入版本库,可跳转,相同的 tag 自动聚合到一起。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaHashtags.png" width="466" height="70" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaHashtags_hu7af2c324b3c5d8c1de77889a8440063a_2997_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaHashtags_hu7af2c324b3c5d8c1de77889a8440063a_2997_1024x0_resize_box_3.png 1024w" loading="lazy" alt="vaHashtags" class="gallery-image" data-flex-grow="665" data-flex-basis="1597px" ></p> <p>可以快速标出某个常用代码的位置 (如 main() 函数的位置):</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hashtagMain.png" width="96" height="76" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hashtagMain_hu13854223822ee9a06bff078e3e762bdc_1106_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hashtagMain_hu13854223822ee9a06bff078e3e762bdc_1106_1024x0_resize_box_3.png 1024w" loading="lazy" alt="hashtagMain" class="gallery-image" data-flex-grow="126" data-flex-basis="303px" ></p> <p>可以交叉引用并跳转:</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hashtagSeeDoSomething.png" width="175" height="108" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hashtagSeeDoSomething_huecc64345aad3f2747281e326ade915cd_1431_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/hashtagSeeDoSomething_huecc64345aad3f2747281e326ade915cd_1431_1024x0_resize_box_3.png 1024w" loading="lazy" alt="hashtagSeeDoSomething" class="gallery-image" data-flex-grow="162" data-flex-basis="388px" ></p> <p>可以在窗口内管理,搜索,分组和隐藏:</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/commaHash.png" width="393" height="107" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/commaHash_hu283de0fd25656ffe45413f92d7550d72_3793_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/commaHash_hu283de0fd25656ffe45413f92d7550d72_3793_1024x0_resize_box_3.png 1024w" loading="lazy" alt="commaHash" class="gallery-image" data-flex-grow="367" data-flex-basis="881px" ></p> <p>这个功能特别灵活,它本质上是对一堆信息做<strong>最小组织</strong> (minimal-structuring) 的过程,特点是<strong>即使数据量再大也始终能保持最小的维护负担</strong>。</p> <p>其实我们日常信息处理时,有很多分组/分目录都不合适的情况——比如发过的所有的朋友圈,只需要在单条记录内任意位置打上 #旅行 / #摘抄 / #笔记 等等… 假如朋友圈支持按照 hashtags 聚合,就可以一键找到所有的相关项。</p> <p>我的日常日志也依赖这类标记,比如 #电影 #2016 就可以一下找到我 2016 年看的所有电影。</p> <p>总的来说我现在非常偏爱这一类平坦组织结构,无需额外的抽象 (abstract) 和层次 (hierarchy),不用成天纠结分组/分类等问题,比树状的类文件夹系统实用和轻便很多。</p> <p>Flat is better than nested.</p> <h3 id="va-outline-大纲视图的快捷操作">VA Outline 大纲视图的快捷操作</h3> <p>这个视图浓缩了当前文件的大纲,便于随时跳转和审视整体结构。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline.png" width="322" height="291" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline_hu80192197c7d45127e97a05216cd1c519_12808_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline_hu80192197c7d45127e97a05216cd1c519_12808_1024x0_resize_box_3.png 1024w" loading="lazy" alt="vaOutline" class="gallery-image" data-flex-grow="110" data-flex-basis="265px" ></p> <p>可以直接在这个视图上拖拽来挪动代码块。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline2.png" width="393" height="110" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline2_hu40fd70c2d36ccf5180c3862081595854_7207_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline2_hu40fd70c2d36ccf5180c3862081595854_7207_1024x0_resize_box_3.png 1024w" loading="lazy" alt="vaOutline2" class="gallery-image" data-flex-grow="357" data-flex-basis="857px" ></p> <p>也可以直接在这个视图上选中一个或多个 block,右键执行各类重构命令,或者整体注释。</p> <p><img src="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline3.png" width="405" height="345" srcset="https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline3_hu505f13c3ca65e1d001a86a16d830b097_13565_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-16-visual-assist-tips/images/vaOutline3_hu505f13c3ca65e1d001a86a16d830b097_13565_1024x0_resize_box_3.png 1024w" loading="lazy" alt="vaOutline3" class="gallery-image" data-flex-grow="117" data-flex-basis="281px" ></p> <p>这个窗口本质上是把单个的代码块原子化了,简化了操作步骤,也避免了很多误操作。</p> <h2 id="更多的小技巧">更多的小技巧</h2> <ul> <li><strong>Goto Related</strong> 跳转到相关的各类符号 (声明/类型/使用地点) 是 Goto Implementation 的增强版</li> <li><strong>Clone Find References Results</strong> 可以将查找结果复制到独立窗口,这一过程可重复产生多个窗口,不像 Find in Files 只允许两个结果窗口</li> <li>一些用起来很顺手的文件操作相关的重构 <ul> <li><strong>Rename Files&hellip;</strong> 一键重命名同名的 .h/.cpp</li> <li><strong>Move Selection to New File&hellip;</strong> 一键把选中的代码挪入新文件</li> <li><strong>Move Implementation to Source File / Header File</strong> 太常用不解释了</li> </ul> </li> <li>大项目会产生若干 GB 级别的缓存文件,可以使用 Options | Clear 来清理这些磁盘空间</li> <li>有时在代码库里做了较大的更新之后,会出现新版本的代码下缓存失效没有及时更新,导致一些功能不起作用的情况。这时使用 Options | Rebuild 可以重建符号数据库(注意,上面的 Clear <a class="link" href="images/http://docs.wholetomato.com/default.asp?W138" >不会影响符号数据库</a>)</li> <li>如果VS没有安装在固态硬盘,把 VA_X.dll 的目录 mklink /j 符号链接到 ssd 上面会快很多。(谢谢评论中 Tyung 同学的补充)</li> </ul> <p>能看到这里说明你一定是真·VA粉了,如果你有什么独门技巧,欢迎在留言里与大家分享~ [抱拳]</p> <ul> <li>[2017-04-29] posted (.5h)</li> <li>[2017-04-16] revised (.5h)</li> <li>[2017-04-15] initially written (3h)</li> </ul> <hr> <ul> <li>本文同时发在我的知乎专栏 (<a class="link" href="https://zhuanlan.zhihu.com/p/26643499" target="_blank" rel="noopener" >链接</a>)</li> </ul> 2017.04 从 GDC 分享中汲取养分 https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/ Sun, 02 Apr 2017 09:45:00 +0000 https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/ <h1 id="从-gdc-分享中汲取养分">从 GDC 分享中汲取养分</h1> <p><img src="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/awkwardness-bingo.jpg" width="600" height="750" srcset="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/awkwardness-bingo_huda2bb0b4fcec2f43625cba3b90720a8c_111458_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/awkwardness-bingo_huda2bb0b4fcec2f43625cba3b90720a8c_111458_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="awkwardness" class="gallery-image" data-flex-grow="80" data-flex-basis="192px" ></p> <blockquote> <ul> <li><a class="link" href="https://twitter.com/sosowski/status/713024980825612289" target="_blank" rel="noopener" >GDC Awkwardness Bingo board</a>, @Sosowski on Twitter</li> </ul> </blockquote> <p>indienova 约我聊一下这个话题(开发者如何最大程度地利用 GDC 分享提高自己的技术水平?),我觉得灰常汗~ 因为我自己都还有一大摞 GDC Talks 等着翻牌子已经很久了。这些自己的存货都还没来得及消化吸收,更遑论为他人提供有效的利用方法——实在汗颜。也正是因为 GDC Vault 的免费内容不少,虽然明知收费的里面有价值的内容更多,也还是几次三番地压抑着自己购买年费会员的冲动。</p> <hr> <p>本文 6000 字左右,阅读时间 15 分钟。BTW, 如果没有细看开头图上的字,现在可以翻上去看一下——比正文有趣得太多,凡是参加过这类会议的同学,都会会心一笑吧~</p> <hr> <p>首先想说一下参加行业会议(包括 GDC)的一个感受:坦白地说,如果觉得经常参与各色的行业会议,就仿佛能从演讲者身上快速获取价值,直接转化到自身的业务水平和技能提高上,那一定是种错觉。这就好像整天刷知乎,仿佛动动手指就能把大V们的见解化为己用,其实是一样的迷思。原因很简单——招式虽可模仿,但若缺乏内功和心法的支撑,一味地追求好的工具和方法,只会变成中看不中用的花架子(这方面我自己有深刻的教训和体会)。所以我们不要学 xx 记者,水平有限,但是跑的比谁都快。</p> <p>那么内功从哪里来呢?多看书,提高理论水平;多写代码,提高实践水平。简单说,就是“<strong>多读多写,知行合一</strong>”。在这八个字的基础上,我们就可以来聊一聊从 GDC 这样的技术会议上可以获得的收获了。</p> <h2 id="分类">分类</h2> <p>以策划/美术/程序的角度来说,最简单快速的甄别手段,自然是一键过滤掉各自职责 (Design/Art/Programming) 之外的演讲,可是这样常会错过有趣的议题。我的个人体会是,想要有效地从行业会议中提取价值,可以从三个值得关注的维度出发:案例 (Stories)、实践 (Practices) 和方法 (Methods/Approaches)。经验上看,这样以性质划分会比原始的按照 Roles (Design/Art/Programming) 分类要有效得多——快速地厘定你想从一场演讲中获得什么,有助于你安排与之相匹配的时间和精力。这里简单地展开一下。</p> <h3 id="案例-stories-增长见识开阔视野">案例 (Stories) 增长见识,开阔视野</h3> <p>最常见的案例类型是<strong>一款游戏的 Postmortem</strong>,成功案例和失败案例都有。跟自己的项目经历结合起来,会非常有启发。通常的 Postmortem 会有 &ldquo;What Went Right&rdquo; 和 &ldquo;What Went Wrong&rdquo; 两方面,这里我拿手头上 &ldquo;Postmortems From Game Developers&rdquo; 里的 Diablo II 举个例子。</p> <blockquote> <p>Diablo II 有三个开发小组,每个小组十余人。</p> <ul> <li>programming</li> <li>character art (everything that moves)</li> <li>background art (everything that doesn’t move)</li> </ul> <p>这应该是若干年后的 Scrum 的原型了 (项目组织按照 Features 而非 Roles 分组)。</p> <p>What Went Right</p> <ol> <li>DIABLO II is still DIABLO (在仅保留了 1% &gt;代码和素材,重写了图形引擎,改变了所有的职业和技能的情况下,仍然能让所有玩过的人异口同声地认为这是原汁原味的 Diablo) <ul> <li>We used the term “kill/reward” to describe our basic gameplay.</li> <li>DIABLO II retained DIABLO’s randomly generated levels, monsters, and treasure.</li> <li>DIABLO and DIABLO II are easy to play. We used what we call the <strong>“Mom test”</strong> — could Mom figure this out without reading a manual?</li> </ul> </li> <li>Blizzard’s development process <ul> <li>First, we make the game playable as soon as possible in the development process. (<strong>The fun part has to be fun</strong>)</li> <li>Also, we constantly reevaluate gameplay and features. (&ldquo;we made two or three games and pared them down to the best one.&rdquo;)</li> <li>Another gigantic reason for our success is our open development process. (&ldquo;hire people who love games, and we make games that we want to play.&rdquo;)</li> </ul> </li> <li>Character skill tree, QA and Simultaneous worldwide release</li> </ol> </blockquote> <p><img src="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/d2.png" width="626" height="414" srcset="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/d2_hu6b5798a3ae0d073ac68d0448bc2fcf3a_162188_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/d2_hu6b5798a3ae0d073ac68d0448bc2fcf3a_162188_1024x0_resize_box_3.png 1024w" loading="lazy" alt="d2" class="gallery-image" data-flex-grow="151" data-flex-basis="362px" ></p> <blockquote> <p>&ldquo;If we like the game we are making—especially if, after two years of playing it, we are not bored to death—the game is clearly going to be a winner.&rdquo; - Blizzard</p> </blockquote> <p>以下是一些典型的 Postmortem 的案例分享:</p> <ul> <li><a class="link" href="https://www.youtube.com/watch?v=VscdPA6sUkc" target="_blank" rel="noopener" >(GDC2016) (Diablo) Diablo: A Classic Game Postmortem</a></li> <li><a class="link" href="http://gdcvault.com/play/1017813/Shout-at-the-Devil-The" target="_blank" rel="noopener" >(GDC2013) (Diablo III) Shout at the Devil: The Making of Diablo III</a></li> <li><a class="link" href="http://www.pixelprospector.com/the-big-list-of-postmortems/" target="_blank" rel="noopener" >The Big List Of Postmortems</a></li> <li><a class="link" href="http://www.gamasutra.com/features/postmortem/" target="_blank" rel="noopener" >Gamasutra&rsquo;s Postmortem List</a></li> <li>《Postmortems From Game Developers》</li> <li><a class="link" href="https://github.com/mc-gulu/game-dev-docs/commit/3214dc42768e9f1694d60be4c1721021edf0b305" target="_blank" rel="noopener" >(2007) 孙志超 - 对游戏开发总结的总结 从一场争论说开去</a></li> </ul> <hr> <p>在 <a class="link" href="https://zhuanlan.zhihu.com/p/25703934" target="_blank" rel="noopener" >GDC 2017 技术选荐合辑</a> 一文中,我首先聊到的 Creating &lsquo;League of Legends&rsquo; Champions: Our Production Framework Revealed 对我而言就属于此类。说到这里要补一句,每个人的发展方向不同,技能侧重点也不同——对有的人来说是 Practices 的内容对另一群人则是 Stories,反过来也一样。像这一场 LOL 的英雄制作,对于制作人,策划,美术和程序,会有不同的侧重和启发(横看成岭侧成峰)。</p> <h3 id="实践-practices---框架工具库经验积累">实践 (Practices) - 框架/工具/库,经验积累</h3> <p>实践 (Practices) 是对语言,框架,库,工具的设计,运用和改造,典型的例子有这些:</p> <ul> <li>(GDC2017) D3D12 Performance Tuning and Debugging with PIX and GPU Validation(如何用新版的 DX12 的 Validation Layer 和 PIX 来诊断流水线)</li> <li>(GDC2017) Surviving Apocalypse on Mainstream Graphics Optimizing DayZ using IntelGPA(如何使用 IntelGPA 来做图形分析和优化)</li> <li><a class="link" href="http://gdcvault.com/play/1024390/How-GitHub-Works-with-Unity" target="_blank" rel="noopener" >(GDC2017) How GitHub Works with Unity</a> (怎么利用 GitHub 的特性来更有效地管理 Unity 工程)</li> </ul> <h3 id="方法-methodsapproaches---思路和方法各类取舍及利弊分析">方法 (Methods/Approaches) - 思路和方法,各类取舍及利弊分析</h3> <p>方法 (Methods/Approaches) 通常是一套抽象的思维模型,一个解决问题的思路,典型的例子有这些:</p> <ul> <li>(GDC2017) FrameGraph: Extensible Rendering Architecture in Frostbite (一套具有扩展性的渲染体系)</li> <li>(GDC2017) Data Binding Architectures for Rapid UI Creation in Unity (一套方便美术和程序合作的界面数据绑定方案)</li> </ul> <h3 id="方法和实践的对比">方法和实践的对比</h3> <p>通常而言,实践(尤其是所谓最佳实践 Best Practice)往往是可以直接在手头的工作中复用的第一手经验。而方法则不一定适用,往往需要透彻的思考和深入的理解,然后再针对特定的情境做适用性的改造。</p> <p>进一步讲,实践是“手头的工具” (hand-toolbox),而方法则是“头脑的工具” (mind-toolbox)。所谓“形而上者谓之道,形而下者谓之器”,从抽象和具象两方面与此二者对应起来。</p> <h3 id="三分类的用处-在-topic-forests-里更有效地提取信息">三分类的用处 :在 Topic Forests 里更有效地提取信息</h3> <p>在短短的五天内,GDC 的演讲超过五百场,而且还需要留出一些时间去逛逛 Expo,现场体验一下不同的游戏,引擎和服务。时间会非常的紧张,没有什么比选择了不合适的议题更让人沮丧了。上面三种分类的最大好处,就是把自己从无数噪音中隔离出来,先默认大家都是角度各异的案例 Stories (听一下纯涨见识),只有非常适合自己需求和段位的 Topic,才值得提升成实践或方法,去细致认真地听和做笔记。</p> <p><img src="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/GDC-Tips.png" width="1019" height="661" srcset="https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/GDC-Tips_hude396a13b77557f697821093867396cb_70911_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-04-02-gdc-learning-tips/GDC-Tips_hude396a13b77557f697821093867396cb_70911_1024x0_resize_box_3.png 1024w" loading="lazy" alt="GDC-Tips" class="gallery-image" data-flex-grow="154" data-flex-basis="369px" ></p> <p>排日程的时候尽量优先排实践或方法,没有合适的再穿插着排案例。</p> <h2 id="handy-tips">Handy Tips</h2> <ul> <li><strong>不要错过会后讨论</strong> - 如果觉得一个演讲有价值,那 QA 时别急着赶下一场,不要错过那些真正感兴趣的人留下来的小讨论,听听私下里大家的交谈 (n &lt;-&gt; n) 能获得比台上演讲时 (1 -&gt; n) 多得多的细节</li> <li><strong>结对复述</strong> - 晚上如无特别的理由,不要把时间都花在 social party 上。尽量找那些跟你选了不同演讲的同事和朋友,跟他们互相复述自己听到的要点,会有两个收获:通过自己的语言来表达,能帮助自己整理和强化吸收到的信息;听别人的复述,可以弥补一些错过课程的遗憾。</li> <li><strong>装备齐全</strong> - 听课时的合理装备是 iPhone + (Mac Book 或 Surface Book 或 纸笔) 如果只用手机的话,光忙着音频/拍照/笔记来回切换了,有一个笔记本会好很多。其实这种场合下的拍照特别适合用 Google Glass 可以把双手解放出来,但我估计拍摄精度和电池续航都成问题。</li> <li><strong>快速手动验证</strong> - 维护一个 Test Lab 工程,有助于趁热乎快速地实验各种听到的想法和思路,如果验证有效就能直接转化成自己的技能点。(不动手就是纸上谈兵)</li> </ul> <p>[注]</p> <ul> <li>本文首先发布于公众号<a class="link" href="https://indienova.com/indie-game-development/learn-more-from-gdc/" target="_blank" rel="noopener" >indienova</a>。</li> <li>Permanent Link: <a class="link" href="http://gulu-dev.com/post/2017-04-02-gdc-assimilating-tips" target="_blank" rel="noopener" >http://gulu-dev.com/post/2017-04-02-gdc-assimilating-tips</a></li> </ul> 2017.03 GDC 2017 技术选荐合辑 https://gulu-dev.com/post/2017-03-11-gdc17/ Sat, 11 Mar 2017 14:45:00 +0000 https://gulu-dev.com/post/2017-03-11-gdc17/ <p>上一次参加 GDC 是在七年前。有趣的是,2010 那年,John Carmack 获得了终生成就奖 (Lifetime Achievement Award);而 2017 年的 GDC 上,获得这个奖项的是 Tim Sweeney。这是两位真正的行业传奇。他们的代码,令我满心钦佩且受教良多。</p> <hr> <p>这一篇快速记录中,我粗粗地整理了一下自己听过的演讲,并把那些觉得很有收获的标注了一下。还有不少演讲是因为分身乏术错过的,这里也一并记了,这样晚些时候可以到 GDC Vault 里去听回放。</p> <p><img src="https://gulu-dev.com/post/2017-03-11-gdc17/topics.png" width="906" height="985" srcset="https://gulu-dev.com/post/2017-03-11-gdc17/topics_hu219d1db79d298cc02d388028c5279395_134294_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-03-11-gdc17/topics_hu219d1db79d298cc02d388028c5279395_134294_1024x0_resize_box_3.png 1024w" loading="lazy" alt="topics" class="gallery-image" data-flex-grow="91" data-flex-basis="220px" ></p> <ul> <li>维护和更新地址:<a class="link" href="https://github.com/mc-gulu/dev-awesomenesses/blob/master/awesome-gdc17.md" target="_blank" rel="noopener" >(github.com) dev-awesomenesses/awesome-gdc17</a></li> </ul> <hr> <p>还有一个<a class="link" href="http://mp.weixin.qq.com/s/C0vkeiCsXKXLGtsA3pB7ag" target="_blank" rel="noopener" >半小时的访谈</a>,是陈杰老师和我一起最后一天晚上做的。头一次上镜头,感觉自己不忍直视 (捂脸逃~~</p> <p>对内容感兴趣又不想花时间看视频的同学,可以扫视下面的零散文字——这其实是我在录节目前记录的一些简短脚本 (就在我手上端着的 Surface 里)。</p> <hr> <p>GDC17 总体感觉:<strong>严谨</strong></p> <p>传统 hack -&gt; 规模越来越大,复杂度 hold 不住 -&gt; 机器性能的提高使得不少 hack 不再有意义 -&gt; 通过严谨的设计来简化问题</p> <p>主要分享三堂课内容:</p> <ul> <li><strong>英雄联盟的制作</strong></li> <li><strong>守望先锋的逻辑状态和同步</strong></li> <li><strong>寒霜引擎的图形渲染架构改造</strong></li> </ul> <p><strong>1. (LOL) 游戏制作 Production</strong></p> <p>a. 英雄的制作 (DNA)</p> <p>新做的内容需要先把握好 DNA,这样你的新内容就有根,就有灵魂</p> <ul> <li><strong>Design</strong> 就是你的核心设计,简单的说就是玩什么,对英雄而言就是技能和对应的技巧和玩法</li> <li><strong>Narrative</strong> 叙事,来历,前因后果,三个问题:你是谁,你从哪里来,你到哪里去</li> <li><strong>Art</strong> 艺术形象,不完全是原画 (考虑实现性),是一种感觉 (feeling),画面感</li> </ul> <p>(此处小结为本文新增) 总得来说, D 对应了交互体验,N 对应了世界观,A 对应了视觉和感性,合起来的 DNA 是确保新内容的感染力的核心,</p> <p>b. 预制作周期</p> <p>一个英雄的制作周期是 9 个月,在开始做实际成品之前的所有阶段(统称为预制作阶段)的时间加起来,足足5个月。</p> <p>也就是说磨刀5个月,砍柴4个月。</p> <p>要是在国内,9个月的 deadline 五个月了还没出东西,呃,是还没开始出东西,就算老板不说啥,自己也会不好意思,对吧?</p> <p>为什么这么做呢?扎实,结实,夯实。</p> <p>这样出来的东西是能够协调地跟游戏里的其他部分融合的。不然呢?可破坏的场景跟技能表现冲突了~ 下雨的特效跟英雄的光环冲突了~ 做英雄花了3个月,然后各种堵窟窿花了10个月。</p> <p>对于成熟的设计团队,只有你给他时间和空间去沉淀,才能扎实,结实,夯实,做出好东西。</p> <p>DNA 和 Pre-Production,就是这个分享给我的最大的两点启发。</p> <p><strong>2. (Overwatch) 网络同步的现代的架构设计</strong></p> <p>教科书般的设计,是上面提到的严谨的体现。</p> <p>为什么这么讲呢?跟现在的系统化方案比起来,以前觉得还可以的一些方案顿时显得很土很山寨。</p> <p>拿录像重放系统举例子,对比现代的网络模块和游戏逻辑结合的调试系统</p> <p>单步单帧逻辑重放,可视化的完整状态调试。</p> <p>旁边识货的同学,眼都直了。</p> <p><strong>3. (Frostbite) 图形渲染架构改造</strong></p> <p>两套东西:FrameGraph, 显式显存管理</p> <p>以前手写的渲染流程,缝缝补补加开关;后来 Gamebryo 实现了一套朴素的 RenderFrame 机制 (有逻辑零碎化的问题);现在通过 FrameGraph 完全达到结构化渲染流程的效果。</p> <p>FrameGraph 用临时生成 lambda 表达式的方法,保留了以前的线性逻辑,(好思路) 跟这个相比,几年前的那套初级机制简直不值一提(在实现功能的基础上追求简单优雅和有效),同样是严谨性的体现。</p> <p>显式显存管理,分块,按照生命期的长短去尽量重复利用块来降低峰值的占用</p> <p>对于渲染系统,我们可以在新的图形 API (DX12 / Vulkan) 上很自然地引申出 <strong>前端</strong> 和 <strong>后端</strong> 概念</p> <ul> <li>前端 (并行化)从材质系统到 drawcall 的产生和提交 (传统的应用程序部分)</li> <li>后端 (从降低碎片化,改善显存利用率开始,逐步接管)(传统的显卡驱动部分)</li> </ul> <p>这是 Frostbite 对应的新渲染架构给我方向上的启发。</p> <hr> <ul> <li>本文同时发布于知乎专栏:<a class="link" href="https://zhuanlan.zhihu.com/p/25703934" target="_blank" rel="noopener" >游戏人间</a></li> </ul> 2017.02 Unity GC Cheatsheet https://gulu-dev.com/post/2017-02-18-unity-gc-cheatsheet/ Sat, 18 Feb 2017 19:37:00 +0000 https://gulu-dev.com/post/2017-02-18-unity-gc-cheatsheet/ <p>关于 Unity 的垃圾回收 (GC) 你可能已经看到不少的文章讨论了。</p> <p>下面是一个极简形式的 Cheatsheet,希望能在最小的篇幅内尽可能全面地列出关于 GC 你需要注意的事项。</p> <hr> <h2 id="unity-gc-cheatsheet">Unity GC Cheatsheet</h2> <hr> <ul> <li>a01. struct Foo 在栈上,但 struct Foo[] 分配在堆上</li> <li>a02. GetType() 会产生 GC Alloc (每个调用 20 Bytes)</li> <li><a class="link" href="https://www.zhihu.com/question/26779558/answer/34015434" target="_blank" rel="noopener" >a03. delegate 的创建时 (赋值 = 或以参数传递) 在堆上分配 (如将方法做为参数传入)</a></li> <li>a04. delegate 尽量使用 =,避免无意的 += 导致 InvocationList 的增长</li> <li>a05. 在针对 GC Alloc 做优化时,对象数量 &gt; 引用关系复杂度 &gt; 对象尺寸</li> <li>a06. 当可以使用整数句柄来代替引用时,尽量使用整数句柄 (简化引用关系)</li> <li>a07. 优化内存布局:利用“数组对 GC 而言是单一对象”这一特性</li> <li><a class="link" href="http://stackoverflow.com/questions/1533757/is-int-a-reference-type-or-a-value-type" target="_blank" rel="noopener" >a08. (a01推广) 单个 ValueType 直接在栈上分配,但 ValueType Array 总是分配在堆上</a></li> </ul> <hr> <ul> <li>b01. 避免频繁调用分配内存的 accessors (如 .vertices/.normals/.uvs/.bones)</li> <li>b02. 避免频繁调用 Int.ToString() 及其它类型的衍生</li> <li>b03. 避免在 Update() 内使用 <a class="link" href="http://answers.unity3d.com/questions/1010251/gameobjecttag-without-gc-allocation.html" target="_blank" rel="noopener" >GameObject.Tag</a> 和 <a class="link" href="http://forum.unity3d.com/threads/unityengine-object-name-allocates-for-each-access.237380/" target="_blank" rel="noopener" >GameObject.Name</a></li> <li>b04. 避免在 Update() 内 GetComponent()</li> <li>b05. 避免在 Update() 内 GetComponentInChildren(),可自己实现无 GC 版本</li> <li>b06. 避免在 Update() 内访问 animation 组件</li> <li>b07. 避免在 Update() 内 FindObjectsOfType()</li> <li>b08. 避免在 Update() 里赋值给栈上的数组,会触发堆内的反复分配</li> <li>b09. 避免频繁使用 Mathf.Max 等函数的数组版(多于两个参数都会调到数组版)</li> <li>b10. (b09 推广):所有具有 params 修饰的函数都应避免频繁使用(以避免临时数组的分配)</li> </ul> <hr> <ul> <li>c01. 在不需要时避免使用 GUILayout - OnGUI 时把 useGUILayout 关掉</li> <li>c02. 避免使用 foreach (除非遍历数组,或直接用 VS 预编译好的 dll)(<a class="link" href="https://unity3d.com/cn/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games?playlist=44069" target="_blank" rel="noopener" >Unity 5.5 已修复此问题</a>)</li> <li>c03. 避免使用枚举或 struct 做 Key 进行字典查找 (除非使用定制的 comparer)</li> <li>c04. 避免使用字符串版本的 Invoke 和 StartCoroutine</li> <li>c05. 避免在产品中调用 Debug.Log (生成堆栈字符串)</li> <li>c06. 避免在产品中使用 Linq</li> <li>c07. 在可能的情况下复用成员变量而不是不断分配新对象</li> <li>c08. 初始化 List&lt;&gt; 时指定合理的 Capacity</li> <li>c09. 使用 StringBuilder 而不是使用 “+” 或 String.Concat() 拼接字符串</li> <li>c10. 在使用协程 yield 时尽量复用 WaitXXX 对象 (使用 Yielders.cs) 而不是每次分配</li> <li>c11. 确保 struct 实现了 IEquatable<T></li> <li>c12. 确保 struct 实现了 Equals() 和 GetHashCode()</li> </ul> <h2 id="details--explanations">Details &amp; Explanations</h2> <ul> <li><a class="link" href="unity-gc-cheatsheet.md" >Unity GC Cheatsheet</a> <ul> <li><a class="link" href="unity-gc-cheatsheet-details.md" ><strong>Details</strong></a></li> <li><a class="link" href="unity-gc-cheatsheet-references.md" >References</a></li> </ul> </li> </ul> <h3 id="a05-在针对-gc-alloc-做优化时对象数量--引用关系复杂度--对象尺寸">a05. 在针对 GC Alloc 做优化时,对象数量 &gt; 引用关系复杂度 &gt; 对象尺寸</h3> <p>对 <a class="link" href="https://en.wikipedia.org/wiki/Boehm_garbage_collector" target="_blank" rel="noopener" >Boehm garbage collector</a> 而言,对象数量直接影响单次 GC 的时间开销 每个对象 90 个时钟周期左右 (大量时间是 cache-missing 所致) 算下来每秒 15M 数目的对象,也就是每毫秒标记 15000 个左右</p> <h3 id="a07-优化内存布局利用数组对-gc-而言是单一对象这一特性">a07. 优化内存布局:利用“数组对 GC 而言是单一对象”这一特性</h3> <p>如我们有 List<Foo>,内含 100 个对象。其中,Foo 如下定义</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Foo</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">a</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">float</span> <span class="n">b</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="n">c</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">string</span> <span class="n">str</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>此时内存中共有 101 个 GC 对象 (100 个 Foo + 1 个 List 内部数组) ,且为 2 级的引用关系 假如我们把数据打散成为单独的数组,如下所示:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="kt">int</span><span class="p">[]</span> <span class="n">aArray</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">float</span><span class="p">[]</span> <span class="n">bArray</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">bool</span><span class="p">[]</span> <span class="n">cArray</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span><span class="p">[]</span> <span class="n">strArray</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>此时所有数据不变的情况下,对象数量 (对 GC 而言) 降低到了 4 个 更进一步,我们把所有的 ValueType 聚合起来</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">struct</span> <span class="nc">Foo_S</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">a</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">float</span> <span class="n">b</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="n">c</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>数据结构就成了</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="n">Foo_S</span><span class="p">[]</span> <span class="n">fooArray</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span><span class="p">[]</span> <span class="n">strArray</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>此时所有数据不变的情况下,对象数量 (对 GC 而言) 降低到了 2 个</p> <h3 id="c03-避免使用枚举或-struct-做-key-进行字典查找-除非使用定制的-comparer">c03. 避免使用枚举或 struct 做 Key 进行字典查找 (除非使用定制的 comparer)</h3> <p>当 Key 为用户定义的 struct 而非内建的值类型时,Dictionary 的主要接口会产生 GC Alloc Add / ContainsKey / TryGetValue / &ldquo;[ ]&rdquo; 等接口都需要对传进来的 TKey 调用默认的 EqualityComparer 来判断是否相等 见 .net 代码文件 <a class="link" href="http://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs" target="_blank" rel="noopener" >dictionary.cs</a> 的第 94 行:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">this</span><span class="p">.</span><span class="n">comparer</span> <span class="p">=</span> <span class="n">comparer</span> <span class="p">??</span> <span class="n">EqualityComparer</span><span class="p">&lt;</span><span class="n">TKey</span><span class="p">&gt;.</span><span class="n">Default</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>而 EqualityComparer 的内部调了私有的 CreateComparer() 来创建真实的 Comparer,见 <a class="link" href="http://referencesource.microsoft.com/#mscorlib/system/collections/generic/equalitycomparer.cs" target="_blank" rel="noopener" >EqualityComparer.cs</a>:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">private</span> <span class="k">static</span> <span class="n">EqualityComparer</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">CreateComparer</span><span class="p">()</span> <span class="p">{}</span> </span></span></code></pre></td></tr></table> </div> </div><p>内建类型(int/float 等等)已经实现了良好的 Equality 判断,而用户定义的 struct 则没有。很可惜上面的代码是 .Net 4.6 的最新代码,有理由推断老版本 mono 在对用户定义的 struct 调用上面的 Add / ContainsKey / TryGetValue / &ldquo;[ ]&rdquo; 等接口时产生了内存分配。</p> <p>方案:只需要手动定义一下 Comparer 并实现 Equals() 和 GetHashCode() 即可</p> <h2 id="references">References</h2> <h3 id="官方及第三方参考-en">官方及第三方参考 (En)</h3> <ul> <li>Unity Official (5.5) <a class="link" href="https://unity3d.com/cn/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games?playlist=44069" target="_blank" rel="noopener" >Optimizing garbage collection in Unity games</a></li> <li>2015-07-21 (Megan Hughes) <a class="link" href="http://www.gamasutra.com/blogs/MeganHughes/20150727/249375/Unity_Garbage_Collection_Tips_and_Tricks.php" target="_blank" rel="noopener" >Unity Garbage Collection Tips and Tricks</a></li> <li>2015-07-31 (Rich Geldreich) <a class="link" href="http://www.gamasutra.com/blogs/RichGeldreich/20150731/250071/Lessons_Learned_While_Fixing_Memory_Leaks_in_our_First_Unity_Title.php" target="_blank" rel="noopener" >Lessons Learned While Fixing Memory Leaks in our First Unity Title</a></li> <li>2015-04-30 (Robert01) <a class="link" href="http://www.somasim.com/blog/2015/04/csharp-memory-and-performance-tips-for-unity/" target="_blank" rel="noopener" >C# memory and performance tips for Unity</a></li> <li>2015-08-07 (Robert02) <a class="link" href="http://www.somasim.com/blog/2015/08/c-performance-tips-for-unity-part-2-structs-and-enums/" target="_blank" rel="noopener" >C# performance tips for Unity, part 2: structs and enums</a></li> <li>2016-01-19 <a class="link" href="http://3-50.net/reducing-memory-allocations-to-avoid-garbage-collection/" target="_blank" rel="noopener" >Reducing memory allocations to avoid Garbage Collection</a></li> <li>2013-11-09 (Wendelin Reich) C# Memory Management for Unity Developers <ul> <li><a class="link" href="http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php" target="_blank" rel="noopener" >(part 1 of 3)</a></li> <li><a class="link" href="http://www.gamasutra.com/blogs/WendelinReich/20131119/203842/C_Memory_Management_for_Unity_Developers_part_2_of_3.php" target="_blank" rel="noopener" >(part 2 of 3)</a></li> <li><a class="link" href="http://www.gamasutra.com/blogs/WendelinReich/20131127/203843/C_Memory_Management_for_Unity_Developers_part_3_of_3.php" target="_blank" rel="noopener" >(part 3 of 3)</a></li> <li><a class="link" href="http://www.cnblogs.com/yishansong/p/4341868.html" target="_blank" rel="noopener" >(cn ver.) Unity开发者的C#内存管理(上篇)</a> by 易山松</li> <li><a class="link" href="http://www.cnblogs.com/yishansong/p/4344299.html" target="_blank" rel="noopener" >(cn ver.) Unity开发者的C#内存管理(中篇)</a> by 易山松</li> </ul> </li> <li>(stackexchange) <a class="link" href="http://gamedev.stackexchange.com/questions/25394/how-should-i-account-for-the-gc-when-building-games-with-unity" target="_blank" rel="noopener" >How should I account for the GC when building games with Unity?</a></li> </ul> <p>侑虎 (UWA)</p> <ul> <li><a class="link" href="http://blog.uwa4d.com/archives/optimzation_memory_1.html" target="_blank" rel="noopener" >性能优化,进无止境-内存篇(上)</a></li> <li><a class="link" href="http://blog.uwa4d.com/archives/optimzation_memory_2.html" target="_blank" rel="noopener" >性能优化,进无止境&mdash;内存篇(下)</a></li> <li><a class="link" href="https://mp.weixin.qq.com/s?__biz=MzI3MzA2MzE5Nw==&amp;mid=2668905142&amp;idx=1&amp;sn=6460b138a7b5069aab451f375e6f75b4&amp;chksm=f1c9eec4c6be67d2a26e19f0bcc187fb5efda17a98329f93c99150513e9b3dc05360235d5403&amp;scene=0&amp;key=5ec1d38f9aafb68979814fae51e1d7695715c161e2f1367243310e55ff584d80f45b4a5d6c1cf437ed694dec3908643879e65baee55883e41357659bc9bc7454b0b17ce67f1e71d340569042cfc1ddf9&amp;ascene=7&amp;uin=NDAyOTU1&amp;devicetype=android-23&amp;version=26050330&amp;nettype=ctnet&amp;abtest_cookie=AQABAAgAAQAchh4AAAA%3D&amp;pass_ticket=Ri4rC%2BMbvUO%2BLUo5fsng0llM9wH%2BIdpHuxYRDWNjm2o%3D&amp;wx_header=1" target="_blank" rel="noopener" >Unity内存优化的“填坑回忆录”</a></li> </ul> <p>腾讯质量开放平台 (WeTest)</p> <ul> <li><a class="link" href="http://wetest.qq.com/lab/view/135.html" target="_blank" rel="noopener" >内存是手游的硬伤——Unity游戏Mono内存管理及泄漏</a></li> <li><a class="link" href="http://wetest.qq.com/lab/view/150.html" target="_blank" rel="noopener" >深入浅出再谈Unity内存泄漏</a></li> <li><a class="link" href="http://wetest.qq.com/lab/view/173.html" target="_blank" rel="noopener" >手游内存占用过高?如何快速定位手游内存问题</a></li> </ul> <p>第三方内存工具</p> <ul> <li><a class="link" href="https://www.jetbrains.com/dotmemory/" target="_blank" rel="noopener" >dotMemory - JetBrains</a></li> <li><a class="link" href="http://wetest.qq.com/cloud/index.php/index/TMM" target="_blank" rel="noopener" >TMM - Tencent tMem Monitor</a></li> <li><a class="link" href="http://www.red-gate.com/products/dotnet-development/ants-memory-profiler/" target="_blank" rel="noopener" >ANTS Memory Profiler</a></li> </ul> <p>知乎问答</p> <ul> <li><a class="link" href="https://www.zhihu.com/question/26779558" target="_blank" rel="noopener" >unity在ios平台下内存的优化?</a></li> <li><a class="link" href="https://www.zhihu.com/question/47031041" target="_blank" rel="noopener" >Unity游戏编程中如何避免runtime动态alloc内存?</a></li> </ul> <p>调查和诊断过程</p> <ul> <li><a class="link" href="https://zhuanlan.zhihu.com/p/23324198" target="_blank" rel="noopener" >U3D项目内存优化记</a> by 凯丁</li> <li><a class="link" href="https://blogs.unity3d.com/cn/2015/01/10/curious-case-of-slow-texture-importing-and-xperf/" target="_blank" rel="noopener" >Curious Case of Slow Texture Importing, and xperf</a></li> </ul> <p>其他</p> <ul> <li><a class="link" href="https://onevcat.com/2012/11/memory-in-unity3d/" target="_blank" rel="noopener" >Unity 3D中的内存管理</a> by 王巍 (@onevcat)</li> <li><a class="link" href="http://blog.csdn.net/ywjun0919/article/details/50688161" target="_blank" rel="noopener" >U3D内存优化原则</a> by ywjun</li> <li><a class="link" href="http://blog.csdn.net/ywjun0919/article/details/50687813" target="_blank" rel="noopener" >U3d内存优化(一)之UILabel使用String的问题</a> by ywjun</li> <li><a class="link" href="http://blog.csdn.net/ywjun0919/article/details/50687929" target="_blank" rel="noopener" >U3d内存优化(二)之Dictonary</a> by ywjun</li> <li><a class="link" href="http://www.cnblogs.com/murongxiaopifu/p/4284988.html" target="_blank" rel="noopener" >深入浅出聊优化:从Draw Calls到GC</a> by jiadong chen</li> <li><a class="link" href="http://blog.csdn.net/pizi0475/article/details/50617747" target="_blank" rel="noopener" >unity3d优化总结篇</a> by pizi0475</li> </ul> 2017.02 《王朝的家底》 记录 https://gulu-dev.com/post/2017-02-12-history-the-economic-view/ Sun, 12 Feb 2017 12:11:00 +0000 https://gulu-dev.com/post/2017-02-12-history-the-economic-view/ <img src="proxy.php?url=https://gulu-dev.com/post/2017-02-12-history-the-economic-view/title.jpg" alt="Featured image of post 2017.02 《王朝的家底》 记录" /><p>[按] 此文内容摘自《王朝的家底》第一篇:三千年的粮仓保卫战。标题和其他非原文部分为笔记,文内不再另行区分。</p> <h1 id="王朝的家底第一篇三千年的粮仓保卫战">《王朝的家底》第一篇:三千年的粮仓保卫战</h1> <h2 id="从狗尾巴草说起">从狗尾巴草说起</h2> <p>狗尾巴草是(中国古代一种重要农作物)粟(谷子,小米粥)的祖本植物。狗尾巴草是粟的野生种,也叫“莠”(二者刚长出的幼苗很难区分,所以有“良莠不分”的说法)</p> <p>粟(谷子),黍(黄米)和菽(大豆),读音相近,最重要的共同点是耐旱耐贫瘠,是先秦时期最重要的农作物。他们生长期短,适合北方旱地,所以率先从百草中脱颖而出。</p> <p>夏朝和商朝曾被称为“粟文化”的王朝。在那以后,随着人口增加,需要开垦更多的荒地,来种植产量更大的作物。</p> <hr> <h2 id="小麦秦国崛起文景之治和农民起义">小麦:秦国崛起,文景之治和农民起义</h2> <p>小麦的故乡在西亚,在距今3800年前经由新疆传入我国。由于我国北方是温带季风气候,春季较为干旱,而小麦更适应地中海气候,刚来时“水土不服 ”,有了大规模灌溉,保证水分充足之后,产量才把粟远远地抛在后面。</p> <p>在战国七雄中,秦国地处西方,最早接触小麦,而且渭河冲积形成的关中平原很适合种植小麦,当时的国家力量也发展到能够组织大量人力来兴修水利(郑国渠)。所以后来秦国有实力连年征战,最终一统天下,是有充足的物质基础的。</p> <p>后人评价文景之治,大多归功于战乱平息后政治稳定、皇帝以身作则勤俭持家、减轻农民的赋税徭役等因素。这些解释固然都有道理,但我们应该关注更为主要的一个原因,那就是小麦的广泛种植。</p> <p>(盐碱化)种小麦的地区,不论是关中平原,还是后来华北平原的一些农耕区,农民们不得不连年耕种,最后让土地陷入万劫不复的盐碱化境地。大批农民失去了稳定的粮食收入,揭竿而起就不可避免了。所以我们可以看到,许多次导致王朝覆灭的农民起义,都发源于小麦的种植区,比如西汉末年的绿林赤眉起义爆发于山东莒县,东汉末年的黄巾大起义爆发于河南洛阳,北魏末年的六镇起义爆发于河套地区隋末的农民起义爆发于山东、河北、河南,唐末的黄巢起义爆发于山东,明末的李自成起义爆发于陕西米脂……这些起义的导火索可能各不相同,但背后都有士地盐碱化的黑影。</p> <hr> <h2 id="水稻唐宋盛世经济重心南移">水稻:唐宋盛世,经济重心南移</h2> <p>和小麦起源于狗尾巴草类似,稻种也来自先民们对草类的筛选。不过对于江南地区的先民来说,他们当时主要以打鱼为生,每天享用海鲜大餐,多么惬意!而种水稻是非常辛苦的事情,他们其实并不太在意稻米做为食物的充饥功能,而是喜欢把稻米放入陶罐中发酵,获得美味的酒。当时食物来源丰富,人口又少,嘴馋也许是远古先民们种水稻的最初目的之一。</p> <p>在小麦无力养活中国人民的关键时刻,古代越南人民向我们伸出了援助之手,他们“友情赞助”了一种优良的稻种——占城稻。占城稻原产越南中南部,宋朝时期那里是占婆古国的势力范围。占城稻只需百天就能收割(早熟),适应能力强,耐旱。宋朝时出现的二熟种植法,直接激发了人口大爆炸,特别是南方地区。</p> <p>唐朝鼎盛期人口5000万,而12世纪宋徽宗时期超过了1亿,南方人口超过了北方一倍,自古以来北多南少的局面彻底扭转了。从北宋时期开始,经济中心就从黄河流域过渡到了长江流域。</p> <p>小麦统治粮仓的时期,如果从公元前221年秦始皇建立起大一统的国家开始算起,到北宋王朝建立时的公元960年,中国一共经历了秦、西汉、东汉、三国、西晋、东晋、南北朝、隋、唐、五代十国等十个时代,平均每百年多一点就会改朝换代一次。如果把南北朝和五代十国时期那些短命的小王朝分开计算的话,朝代更迭的时间就更短了。</p> <p>当水稻成为粮仓的主角后,中国一共经历了北宋、南宋、元、明、清五个王朝,如果算到公元1911年止,在九百五十年的时间中,每个朝代的寿命接近200年。正所谓手中有粮,心中不慌,北宋和之后的王朝都因为广种水稻而延年益寿了。</p> <hr> <h2 id="玉米和地瓜来自美洲大陆的礼物康乾盛世">玉米和地瓜:来自美洲大陆的礼物,康乾盛世</h2> <p>美洲大陆的发现,不仅改变了欧洲,同样也给古代中国带来了深刻的影响,来自美洲的一些农作物也改变了中国社会,它们是:玉米、地瓜(番薯)、花生、向日葵、辣椒、烟草。这些农作物中,玉米棒子和地瓜对粮仓的贡献最大。明末清初的时候,不论是黄河流域还是长江流域,能够种小麦和水稻的土地,基本上已经开发完毕了,以当时的亩产,也只能支撑1亿多人口生活。就在这时,美洲的玉米和地瓜经过漫长的传播道路,跨越了半个地球来到了中国。</p> <p>清朝初期人口 1.5 亿,而仅仅一百年后的康乾盛世,轻松实现翻番到 3 亿。文景之治很大程度上归功于小麦种植的推广,而康乾时期是玉米,地瓜和花生的引入,与水稻小麦一起充实了粮仓。</p> <p>假如玉米和地瓜早一百年进入中国,在明朝中期就养活了更多的贫苦农民,也许吃饱了肚子的李自成就不会带领流民起兵,后金铁骑也根本没有机会中原逐鹿。历史不能假设,但从逻辑常识出发,在农业技术没有飞跃性的提高的前提下人口增加了一倍,新的农作物品种是不可忽略的重要因素。</p> 2017.01 Unity MemoryProfiler 的工作机制及可能的改进 https://gulu-dev.com/post/2017-01-25-unity-memoryprofiler/ Wed, 25 Jan 2017 16:48:00 +0000 https://gulu-dev.com/post/2017-01-25-unity-memoryprofiler/ <p>Unity 的开源内存分析工具 <a class="link" href="https://bitbucket.org/Unity-Technologies/memoryprofiler" target="_blank" rel="noopener" >MemoryProfiler</a> 非常有用,可以提供所有由 Unity 分配的 C++ 对象的内存信息,在该工具内被称为 <code>NativeUnityEngineObject</code> (Native-only Mode)。当 C# 脚本经由 il2cpp 编译为 C++ 时,此工具可以提供额外的所有 C# 对象的信息,在该工具内被称为 <code>ManagedObject</code> (Full Mode)。</p> <p>本文简单地描述了该工具的工作机制,并探讨了一下基于该工具的一些可能的改进。</p> <h2 id="工作机制">工作机制</h2> <p>这个工具中所能提供的所有的内存数据均来源于一个 Unity API:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="n">UnityEditor</span><span class="p">.</span><span class="n">MemoryProfiler</span><span class="p">.</span><span class="n">MemorySnapshot</span><span class="p">.</span><span class="n">RequestNewSnapshot</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><p>通过调用这个函数,我们可以向一个运行着的 Unity 程序请求一个新的内存快照。如果是运行于编辑器内的程序,该请求同步地返回上面说到的 Native-only Mode 数据;如果是运行于 iOS 上的基于 il2cpp 的应用,该请求异步地返回上面说到的 Full Mode 数据。</p> <p>刚收到的快照存在于下面这个紧凑数据对象里:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">PackedMemorySnapshot</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">Connection</span><span class="p">[]</span> <span class="n">connections</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">PackedGCHandle</span><span class="p">[]</span> <span class="n">gcHandles</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">MemorySection</span><span class="p">[]</span> <span class="n">managedHeapSections</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">PackedNativeUnityEngineObject</span><span class="p">[]</span> <span class="n">nativeObjects</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">PackedNativeType</span><span class="p">[]</span> <span class="n">nativeTypes</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">TypeDescription</span><span class="p">[]</span> <span class="n">typeDescriptions</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">VirtualMachineInformation</span> <span class="n">virtualMachineInformation</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>收到这个紧凑数据对象后,MemoryProfiler 做了一些展开的工作,得到下面这个展开后的对象,内含完整的信息和交叉的引用:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">CrawledMemorySnapshot</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">NativeUnityEngineObject</span><span class="p">[]</span> <span class="n">nativeObjects</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">GCHandle</span><span class="p">[]</span> <span class="n">gcHandles</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">ManagedObject</span><span class="p">[]</span> <span class="n">managedObjects</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">StaticFields</span><span class="p">[]</span> <span class="n">staticFields</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="c1">//contains concatenation of nativeObjects, gchandles, managedobjects and staticfields</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">ThingInMemory</span><span class="p">[]</span> <span class="n">allObjects</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">MemorySection</span><span class="p">[]</span> <span class="n">managedHeap</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">TypeDescription</span><span class="p">[]</span> <span class="n">typeDescriptions</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">PackedNativeType</span><span class="p">[]</span> <span class="n">nativeTypes</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">VirtualMachineInformation</span> <span class="n">virtualMachineInformation</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个过程中,最重要的是:<strong>所有的内存对象被展开到 ThingInMemory[] allObjects 这个多态数组里</strong>。有了所有的对象及它们间的引用关系,我们就可以做进一步的分类,调查和分析了。</p> <h2 id="实例类型">实例类型</h2> <p>在上面的展开后的数据对象里,前四项值得分别说明一下:</p> <ul> <li><code>NativeUnityEngineObject[] nativeObjects</code> 这是前面提到过的所有的 C++ 对象。我们无法看到这些对象的实际内容,但是可以看到下面这些信息: <ul> <li>这是一个典型的 C++ 对象: <ul> <li><img src="https://gulu-dev.com/2017-01-25-unity-memoryprofiler/1_native.png" loading="lazy" alt="1_native" ></li> </ul> </li> <li>在上图中,最有价值的信息有 <ul> <li>instanceID - 该对象的实例 ID,Unity 保证在一次运行期间新创建的对象不会与已销毁的对象复用 ID</li> <li>References - 该对象引用的所有对象列表</li> <li>Referenced by - 引用该对象的所有对象列表</li> </ul> </li> </ul> </li> <li><code>ManagedObject[] managedObjects</code> 这是所有的 C# 对象。 <ul> <li>由于该快照包含了对应的 managed heap 的信息,我们可以获得任意 C# 对象的数据细节。</li> <li>这是一个典型的 C# 对象: <ul> <li><img src="https://gulu-dev.com/2017-01-25-unity-memoryprofiler/2_managed.png" loading="lazy" alt="2_managed" ></li> </ul> </li> <li>在上图中可以看到这个对象的每一个字段的详细内容。如果某个字段是对另一个对象的引用,可以直接点击跳转过去。</li> </ul> </li> <li><code>GCHandle[] gcHandles</code> 用于 C#/C++ 对象的交叉生命期管理 <ul> <li>“If the native code will take ownership of that object, we need to tell the garbage collector that the native code is now a root in its object graph. This works by using a special managed object called a GCHandle.” 详见 <a class="link" href="https://blogs.unity3d.com/2015/07/09/il2cpp-internals-garbage-collector-integration/" target="_blank" rel="noopener" >IL2CPP Internals – Garbage collector integration – Unity Blog</a></li> <li>简单地说,如果一个 C# 对象被一个 C++ 对象持有的话,一个 GCHandle 就会被创建出来通知 GC 这种外部引用的情形存在</li> <li>通过任意一个 GCHandle 的 References/Referenced by 我们可以找到位于这个外部引用两端的 C#/C++ 对象</li> </ul> </li> <li><code>StaticFields[] staticFields</code> 则是所有的静态变量 (C#) <ul> <li>这里你可以找到程序内现存的所有静态变量,是非常有用的功能。</li> <li>这是一个典型的静态变量: <ul> <li><img src="https://gulu-dev.com/2017-01-25-unity-memoryprofiler/3_static.png" loading="lazy" alt="3_static" ></li> </ul> </li> <li>分析这些静态变量的数据可以发现,很多时候内存增长都是这种难以意识到的隐性的静态容器的尺寸增长。</li> </ul> </li> </ul> <p>除了这些实例对象,还有 C# 堆的数据和其他一些类型信息,这里就不多说了。</p> <h2 id="可能的改进">可能的改进</h2> <p>以下是一些针对 MemoryProfiler 的一些可以改进之处。</p> <ul> <li>层次化和结构化的数据展示改进 <ul> <li>由于 MemoryProfiler 通过 Treemap 的形式展现数据,在操作时很容易因为数据量太大而难以定位到单个的对象。一个很容易得出的改进是使用更结构化的方式来归类和浏览不同类型的对象。实践中可以使用自制控件 <a class="link" href="https://github.com/PerfAssist/PA_Common/blob/master/Docs/TableView.md" target="_blank" rel="noopener" >TableView</a> 来构造一个双层表格,分别用于类型和对象的展示。具体的使用例子可以参考文章 <a class="link" href="http://gulu-dev.com/post/perf_assist/2016-11-22-unity-string-intern" target="_blank" rel="noopener" >Unity 游戏的 string interning 优化</a></li> <li>这种表格的一个优势是可以自定义字段并显示对应的汇总统计信息</li> </ul> </li> </ul> <p>除了展示方式的改进之外,针对类型或实例的全文搜索,快照间的对比,分配时的细节诊断增强,都是非常有价值的潜在改进。在 <a class="link" href="https://github.com/PerfAssist/PA_ResourceTracker" target="_blank" rel="noopener" >ResourceTracker</a> 中,可以看到我们基于开源的 Unity MemoryProfiler 做出的部分改进工作。</p> <p><img src="https://gulu-dev.com/2017-01-25-unity-memoryprofiler/mem_profiler.png" loading="lazy" alt="mem_profiler" ></p> <p>[注]</p> <ul> <li>本文首先发布于公众号<a class="link" href="http://mp.weixin.qq.com/s/0Vd-7byZQSrYbF0bJ-VqEQ" target="_blank" rel="noopener" >西山居技术</a>。</li> <li>本文同时发布在 <a class="link" href="https://zhuanlan.zhihu.com/p/25187141" target="_blank" rel="noopener" >知乎专栏链接</a></li> </ul> 2017.01 游戏引擎技术点滴 https://gulu-dev.com/post/2017-01-15-game-engine-talk/ Sun, 15 Jan 2017 00:00:00 +0000 https://gulu-dev.com/post/2017-01-15-game-engine-talk/ <img src="proxy.php?url=https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-01.jpg" alt="Featured image of post 2017.01 游戏引擎技术点滴" /><p>[按] 在上个月 [2016-12-17] 的 2016 金山技术开放日上,俺做了一个游戏引擎相关的分享。结束之后,俺断断续续地在零碎时间里把分享的内容整理了一下,为每张幻灯片配上了简单的文字注解,就形成了这篇小结。</p> <p>参与了现场交流的同学可能会发现,这篇文字版跟现场版相比,虽然幻灯片保持原状,但文字注解部分略有出入 (增加的内容主要是第二部分“评估,运用和改造”,这一部分在现场时因为时间关系说得非常简略),还请见谅。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-01.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-01_hube990a1b57bdb5bb8b339093d7fbe7dc_48037_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-01_hube990a1b57bdb5bb8b339093d7fbe7dc_48037_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-01" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <h2 id="内容提纲">内容提纲</h2> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-02.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-02_hube990a1b57bdb5bb8b339093d7fbe7dc_76524_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-02_hube990a1b57bdb5bb8b339093d7fbe7dc_76524_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-02" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <ul> <li>游戏引擎的<strong>十年变迁 (2006-2016)</strong> (对过去十年的游戏引擎发展的简要小结)</li> <li>游戏引擎的<strong>评估,运用和改造</strong> (对一次技术迭代周期内的主要实践的归纳)</li> <li>游戏引擎的<strong>下一个十年</strong> (一些很零碎的对未来十年可能值得深入的技术的思考和启发点)</li> </ul> <h2 id="第一部分游戏引擎的十年变迁-2006-2016">第一部分:游戏引擎的<strong>十年变迁 (2006-2016)</strong></h2> <p>在这次分享中,我着重对比了个人相对有所了解的五款商业引擎,分别是 Unity、Unreal、CryEngine、Gamebryo 和 Torque。而下面划掉的开源引擎 OGRE/Irrlicht/cocos2d-x 性质不同,放在一起对比并不公平,所以排除在这一回讨论的范围以外。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-03.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-03_hube990a1b57bdb5bb8b339093d7fbe7dc_69493_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-03_hube990a1b57bdb5bb8b339093d7fbe7dc_69493_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-03" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>前几个 (Unity、Unreal、CryEngine) 比较知名,就不多做介绍了。这里简单介绍一下 Gamebryo 和 Torque。</p> <p>Gamebryo 是当年 (2007-2010) 国内非常流行的商业引擎之一,业内不少大厂都曾拿这个引擎做过各类项目,也培养出一批熟悉 Gamebryo 的引擎程序员 (包括我在内——虽然我是经由 Unreal 2 启蒙,但只有在 Gamebryo 上的工作才让我看到了自己曾尝试着去实现的理想中引擎的影子)。 Gamebryo 的最强之处在于其设计上的普适性——与 Unreal 和 CryEngine 内含的 FPS 基因不同,Gamebryo 对于 MMO、赛车、休闲、动作等国内常见的游戏类型均能很好地适配。从基于 Gamebryo 的两大代表作《文明4》和《辐射3》在游戏类型上的跨度,就可以看出这种不同寻常的通用性。它的整体架构简洁有力,模块边界符合直觉,模块之间耦合性低,模块实现内聚性强。这些特性使得它的学习曲线相对平缓,新人容易培训和培养,因而也是我个人偏爱的一款引擎。Gamebryo 的弱点在于自身工具链不足,且与第三方的集成度偏低,以及由于前两者导致的引擎提升潜力受限。</p> <p>而 Torque 则是前些年的低成本商业引擎的代表,其特色在于<strong>简单易用的编辑器</strong>和<strong>独具特色的脚本</strong>,基本上可以看作是 2010 年以前的 “史前版 Unity”,前述两大特色也被后来者 Unity 继承并发扬光大。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-04.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-04_hube990a1b57bdb5bb8b339093d7fbe7dc_77585_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-04_hube990a1b57bdb5bb8b339093d7fbe7dc_77585_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-04" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>在这张基本时间线的图上,我们可以看到在过去十年里这五款引擎大致的发展轨迹。其中可以看到,贯穿始末的是 Unreal 和 CryEngine,而另外三款引擎则流行于特定的历史时期。而即使是 Unreal 和 CryEngine,在漫长的发展过程中也自有其高峰和低谷。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-05.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-05_hube990a1b57bdb5bb8b339093d7fbe7dc_108407_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-05_hube990a1b57bdb5bb8b339093d7fbe7dc_108407_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-05" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>有了它们各自的时间线打底,我把这五个引擎各自比较有影响力的版本标注了上来,这样在以五年为跨度的单位 (垂直虚线) 上,每个引擎曾做过的大动作就一目了然了。有过相应项目经历的程序员,对这些曾经活跃一时的版本一定不会陌生。这里我们先不针对单独的版本一一细说,只是对每个引擎的大致活动情况了解一个大概。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-06.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-06_hube990a1b57bdb5bb8b339093d7fbe7dc_111571_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-06_hube990a1b57bdb5bb8b339093d7fbe7dc_111571_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-06" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>细心的同学也许已经注意到了,看起来很凑巧的是,这些引擎的大版本或多或少地聚集在我标出的以五年跨度为单位的竖直虚线附近。而这其中最为显著的就是 Unreal。这仅仅是一个巧合么,还是说,跟其他的时期相比,这些特定的年份有什么不同?</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-07.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-07_hube990a1b57bdb5bb8b339093d7fbe7dc_77995_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-07_hube990a1b57bdb5bb8b339093d7fbe7dc_77995_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-07" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>站在 2016 往回看,我们意识到——在这十年中,这是仅有的两次有着整体产业级影响力的根本性的变迁和进化。如果说是两次技术革命 (Revolution) 可能不甚恰当,但要说这是两次影响范围甚广,使得游戏产业的深度和方向产生了根本性的延展的两次行业范围的演化 (Evolution),似乎并不为过。</p> <p>其中第一次演化,是起源于 2002 年并于四年后 (约 2006 年前后) 渗透到整个行业的,被广泛应用于游戏引擎中的<strong>可编程图形流水线</strong> (Programmable Graphics Pipeline);而第二次演化,则是起源于 2007 年的初代 iPhone 并在四年后 (约 2011 年前后) 渗透到整个行业的,改变了整个游戏行业生态的<strong>移动平台游戏开发</strong> (Mobile Development)。</p> <p>可编程流水线刚刚被引入到实时图形渲染的领域中时,是 3D 图形处理器发展最迅速的时期,从 Geforce 3 开始,nVidia 在硬件方面每半年就有一次较大的更新,同时期的各类图形技术一时间层出不穷,可谓是画面渲染质量提高最快的黄金时代。在2006年时,我任职于育碧上海,有幸参与了 <a class="link" href="https://zh.wikipedia.org/wiki/%E7%BB%86%E8%83%9E%E5%88%86%E8%A3%82%EF%BC%9A%E5%8F%8C%E9%87%8D%E9%97%B4%E8%B0%8D" target="_blank" rel="noopener" >“细胞分裂:双面间谍”</a> 的开发,把<a class="link" href="http://www.gamersky.com/news/200610/44589.shtml" target="_blank" rel="noopener" >当时的游戏截图</a> (注意最后一张是游戏中上海关卡的东方明珠夜景图) 拿到现在来看都并不过时。得益于复杂度迅速提高的材质和各种开脑洞的特效实现的不断涌现,整个行业在图形和渲染表现方面的探索非常迅速和深入,通过真实感图形渲染出来的游戏画面达到前所未有的逼真程度。</p> <p>而移动平台的游戏开发则是另一次重要的变迁。在新的平台上,随着大量非传统核心类玩家的涌入,人们有着与传统 PC/Console 迥异的关注点,从单一的超高画面和沉浸感追求拓展为更多元的交互和体验。可以说直到如今,我们仍处于这次演化的余波之中。</p> <p>我们注意到,这两次演化的共同点在于:它们皆非短时间内剧烈的革命,而都是跨度若干年,并于图中所标示的年份上遍及整个行业的产业级演化。而不同之处是:前一次的演化,在图形领域内前所未有地挖掘了电子游戏作为可视化交互艺术可能触及的“<strong>深度</strong>”,而后一次则通过全新的平台和交互语言大大拓宽了行业所能触及的“<strong>广度</strong>”。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-08.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-08_hube990a1b57bdb5bb8b339093d7fbe7dc_88424_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-08_hube990a1b57bdb5bb8b339093d7fbe7dc_88424_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-08" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>回到时间线中,我们可以发现,Unreal 和 Unity 对两次行业的演化有着强烈的预期和精准的判断——在这些决定命运的转折点上,他们要么在巨变来临前就做好了充分的准备,在行业变迁中始终屹立不倒,要么凭借着顺应趋势的特性集和服务乘风而起。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-09.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-09_hube990a1b57bdb5bb8b339093d7fbe7dc_121515_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-09_hube990a1b57bdb5bb8b339093d7fbe7dc_121515_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-09" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>现在我们重点来看一看 Gamebryo 、CryEngine 和 Turque,它们是在第一次行业演化中崛起的佼佼者,在各自的量级上均是罕有敌手。Gamebryo 的标准材质 (NiStandardMaterial) 对应着可编程流水线在现代商业引擎中最简易和灵活的实现,是那个年代 (2006) 最容易上手的商业引擎之一;CryEngine 的图形表现在孤岛危机时代是最强的引擎之一 (这里的两个“之一”二字仅仅是意思一下);而 Torque 更是为小型独立开发者提供了极易上手的编辑器和非常丰富的着色器开发套件 (基本上可以认为他们自己弄了个内置的低配版 Asset Store)。所有这些让它们在第一次行业演化中脱颖而出,在<a class="link" href="http://devmaster.net/" target="_blank" rel="noopener" >devmaster.net</a> (这是那个年代的一个专注游戏引擎和中间件目录的开发网站) 上成为提及率最高的几个商业引擎。</p> <p>然而时过境迁,在推出了若干有影响力的版本之后,它们不约而同地在 2011 年前后平静了下来。很明显的是,它们并没有意识到时代的趋势,在移动平台崛起时,它们没有做出任何反应,反而把精力用在了事后看起来无关紧要的改进上。</p> <p>Gamebryo 的用户希望引擎能提供更好的编辑器和工具链,而 Gamebryo 前后尽力做出的两版编辑器虽然看起来很不错但并未达到工业级的成熟度 (很像后期的 cocos2d-x) ——在拥有成功项目的支撑和回馈之前就陷入了兼容性的泥潭。而 CryEngine 似乎落入了“为了强大而强大”的漩涡,并未意识到在图形技术日趋成熟的时代,由于边际效应递减,图形上的优势越来越难以形成差异化。与老对手 Unreal 为 iOS 专门打造的瘦身版 UDK 截然相反的是,CryTek 精心打造的“新版” (Rebranded) CryEngine 甚至似乎刻意在逃避移动平台——它的主要特性是支持 Linux 和下一代主机 (PS4 / XBox One / Wii U)。</p> <p>Torque 则是这三者中最为惋惜的。按照现在的眼光看,它是商业引擎中最便宜的 (~$200),它的目标群体是小型独立开发者和团体,它的编辑器像 Unity Editor 那样亲切好用,它的脚本 TorqueScript 神似 C#,它甚至有一个 <a class="link" href="http://www.garagegames.com/products/torque-3d/add-ons" target="_blank" rel="noopener" >Torque 3D Store</a> (可以看做是 Asset Store 的前身,现在仍然可以访问)。在 StackOverflow 上,你甚至能看到 (2009 年) 这样一个有趣的比较:“<a class="link" href="http://stackoverflow.com/questions/1780690/unity-vs-torque-game-engines-and-ide-environment" target="_blank" rel="noopener" >Unity vs Torque game engines and IDE environment</a>” 但是,由于对移动平台的无视,Torque 的用户源源不断地流向了 UDK 和 Unity。</p> <p>Gamebryo 、CryEngine 和 Turque是三款定位迥异的游戏引擎,却有着极为相似的起落周期——在第一次演化中提供了各自领域最有价值的服务而崛起,和对第二次演化的无视甚至逃避导致的衰落。这三款引擎所有的有影响力的版本均在我们框定的第一次演化和第二次演化之间。在移动平台普及之后,这三款引擎再也没有推出一个有影响力的版本。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-10.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-10_hube990a1b57bdb5bb8b339093d7fbe7dc_104212_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-10_hube990a1b57bdb5bb8b339093d7fbe7dc_104212_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-10" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>我们拉近视角,近距离观察 Unreal 这个贯穿十年,历经波折却仍然稳步前行的游戏引擎,看看它的步调和动作是如何与时代趋势相匹配的。在图中我们不难看出,Unreal 总是能够捕捉并提前准备好对应的发布——细究每一条细节,Epic Games 在十年里完整地向我们诠释了“<strong>与时俱进</strong>”的真义——在行业需要深度的时候提供足够的深度,在行业需要广度的时候提供足够的广度。(呃,一不留神成了 Epic 吹了~~)</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-11.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-11_hube990a1b57bdb5bb8b339093d7fbe7dc_89174_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-11_hube990a1b57bdb5bb8b339093d7fbe7dc_89174_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-11" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是第一部分的结论——**不是最强的,也不是最新最酷的,更不是最贵的,而是最适应变化的,活了下来。**这个结论是由达尔文的进化论金句略作修改而来,原文见下面的方框。</p> <p>(第一部分完)</p> <h2 id="第二部分一次技术迭代周期">第二部分:一次技术迭代周期</h2> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-12.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-12_hube990a1b57bdb5bb8b339093d7fbe7dc_68047_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-12_hube990a1b57bdb5bb8b339093d7fbe7dc_68047_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-12" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>如果想要各种姿势自顶向下自底向上巨细无遗地了解游戏引擎的方方面面,可以看 Milo 老师的<a class="link" href="https://zhuanlan.zhihu.com/p/24207171" target="_blank" rel="noopener" >游戏程序员的学习之路</a>,这里就不多说了。我们抓一下挈领,把<strong>游戏引擎的评估</strong>放在最重要的位置,试着解决一下关于 “How” 的几个问题——“How to evaluate” (评估)、“How to use” (运用)、“How to extend” (改造)。</p> <h3 id="游戏引擎的评估">游戏引擎的评估</h3> <p>此前我曾写过一篇文章:<a class="link" href="http://gulu-dev.com/post/2014-07-28-tech-evaluation" target="_blank" rel="noopener" >2014-07-28 如何判断一个技术(中间件/库/工具)的靠谱程度?</a> 这篇文章里我集中讨论了评估中间件需要注意的一些情况,在文末我写道,</p> <blockquote> <p>“对中小规模的技术而言,上面的“望,闻,问,切”已经足以应付了。而对大型代码库/框架/引擎而言,又有一套不大一样的评估标准,另有曲径可探,咱们择日另行讨论,此处暂且按下不表。”</p> </blockquote> <p>当时给自己挖了个不大不小的坑。两年多过去,现在这个坑终于可以填上了。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-13.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-13_hube990a1b57bdb5bb8b339093d7fbe7dc_74129_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-13_hube990a1b57bdb5bb8b339093d7fbe7dc_74129_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-13" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>关于引擎评估,首当其冲的是三个简短的问题,我们一个一个来看。</p> <p>首先,“<strong>是否经受过同类产品的考验</strong>”是一个决定性的因素,这不仅仅意味着能否按时交付,技术风险高低——更多时候,这是你的团队不会因为计划外因素而意外搁浅的重要保障。我们知道,捡到一个存折带来的喜悦远远低于丢失一个等额的存折带来的痛苦,同样的,在幻想你的游戏大卖之前,先尽一切可能确保你的项目不会因为无法控制的因素夭折显然更有意义。没有经过考验的引擎就像是<strong>一杆没上过战场真刀真枪考验过的枪</strong>,在它有效地为你杀敌之前,先祈祷它不会伤到自己吧。这也是市场上看到的山寨产品 (对国产游戏而言) 和续作 (对 3A 游戏而言) 这么多的直接原因。</p> <p>其次,“<strong>好招人吗</strong>”。这个问题表面上看是一个团队管理和人员招聘问题,而实际上却是一个学习曲线和培养成本的问题。在 Unreal 推出 UDK 之前,很多用 Unreal 引擎的小团队在快速出了原型之后都难以为继,这是因为那时的 Unreal 技术人员的培养成本很高,培养时间很长,在人员快速扩充时期,小团队很难消化新手和准新手给团队带来的负担。这就造成了巨大的反差:两三个高手可以拿着 Unreal 在两个月内迅速出一个华丽的原型,在期望值迅速提高之后,弄来一大票人吭哧了一年多,在项目节点上老板一看,哎哟我去,这可不还是那个原型吗~~等等,好像还不如刚开始那时候稳定了~</p> <p>最后,“<strong>是否有代码</strong>”。这个问题在<a class="link" href="http://gulu-dev.com/post/2014-07-28-tech-evaluation" target="_blank" rel="noopener" >此前文章</a>里已有表述,这里引用一下:</p> <blockquote> <p>最后说一下这里面一条俺认为比较重要的,也是当年带队的MMO项目里,被我列为头条编程规范的原则:**绝对,绝对,绝对不要使用没有100%提供源码的第三方技术。**这是一条红线,不管这个技术有多强大,都绝对木有例外。程序猿们或多或少都有感触,在编程的世界里,CPU时序的不确定,存储IO的阻塞,其他进程对CPU/内存资源占用造成的扰动,后台进程如杀毒软件偶尔的锁定文件访问,公网路由的拥塞,都为运行着的程序施加了太多不可预知,不可控制的因素。而在这些不可控制的因素里面,允许在自己进程的地址空间内运行一些无法得知其本来面目的代码,是其中最危险也是最容易失控的那一类。反面例子太多,俺就不举了,也免得触物伤怀,影响心境。</p> </blockquote> <blockquote> <p>&hellip;&hellip;</p> </blockquote> <blockquote> <p>关于“三个绝对”的问题,俺专门补充说明一下,</p> <ul> <li>如果所谓的“软件大厂”是第一方,那通常咱们也没啥选择。就比如要在 Windows 上开发 3D 游戏,用闭源的 DirectX 也是理所当然。正如文中所说,所谓“三个绝对”是针对那些几乎总是有得选择的第三方库而言的。</li> <li>使用软件大厂的闭源技术,也会带来不小的潜在隐患。大公司通常是不太搭理小团队的,如果你掉进的坑恰好在朋友圈里和网上找不到类似的案例或方案,那尝试跟所谓“软件大厂”交流或反馈一般都是做无用功。最终要么花更多的时间吭哧吭哧workaround,要么去掉对应模块了事。</li> <li>作为程序员,当遇到问题时,你希望的是 a) 通过一路在源码中前后追溯,在解决问题之余,弄清楚前因后果,实实在在地增加自己相关领域的经验和认识呢,还是 b) 对着文档反复猜测和校验自己哪个参数有没有误传?</li> </ul> </blockquote> <blockquote> <p>其实到了关键时刻,有代码在手上,就是一颗定心丸,正所谓“<strong>源码之前,了无疑惑</strong>”。当遇上奇怪的症状时,什么文档都比不上正在运行着的唾手可得的鲜活的代码。</p> </blockquote> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-14.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-14_hube990a1b57bdb5bb8b339093d7fbe7dc_127592_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-14_hube990a1b57bdb5bb8b339093d7fbe7dc_127592_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-14" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是针对引擎的两种主要的开发模式的选择,明面上的优势和劣势已经标注在图中了。简单地说,如果“<strong>求快</strong>” (往往是需求方更强势) 占了上风,往往是“钻进去改”的框架式更合适;而如果“<strong>求稳</strong>” (往往是实现方更强势) 的占了上风,则往往会以相对严谨的工程化思维来设计架构和实现。有的团队自打一开始就压根就没考虑过这问题,管它三七二十一先改了再说。一上来堆系统堆得很爽,进度喜人,到了后期处处是改到一半改不动的烂摊子,这种时候再加班加点加人手,硬啃硬怼硬编码,拼命拿战术上的勤奋去掩盖战略上的懒惰。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-15.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-15_hube990a1b57bdb5bb8b339093d7fbe7dc_91592_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-15_hube990a1b57bdb5bb8b339093d7fbe7dc_91592_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-15" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来就到了具体内容的考察了。这一页上列出的三个因素是需要考虑的一级要素。为什么说 (从引擎角度看) “这三个因素直接决定了项目按期交付的可能性”呢?这是因为,对于给定的游戏类型和设计,这些因素直接影响到一个项目实际工程量的大小。</p> <ul> <li><strong>特性集</strong> (feature set) 是引擎“<strong>能帮你解决的问题的集合</strong>”。正如一个 CPU 的指令集那样,引擎的原生实现不仅能帮你省去自己实现这些特性的时间,而且往往是在特定环境下 (给定的问题域内) ,在这个引擎上所能实作出的最佳实践 (Best Practice)。有两个因素会把这种价值给迅速放大:第一,普通游戏开发团队的平均技术水准,通常是低于他们所使用的引擎的研发团队的;第二,普通游戏开发团队对引擎的熟悉程度和运用能力,在一定程度上同样也低于对应的引擎研发团队。所以<strong>已有的特性集与目标项目的匹配情况</strong>,是我们格外关注的焦点。</li> <li><strong>工具链</strong> (tool-chain) 是引擎提供的“<strong>团队工作流程的内部骨架</strong>”。缺乏工具链或工具链不成熟的引擎是双刃剑,需要<strong>显著高于平均水准</strong>的团队才能有效驾驭。Gamebryo 和 OGRE 这两款引擎设计优良,但在工具链上很弱,是两个很好的例子。虽然很多团队淹没在工具制作的泥潭里,但这种不成熟也恰恰给了那些强力团队一个难得的机会,围绕着特定的业务模型构建出<strong>以业务为导向的工具链</strong>,形成了真正的团队级的核心竞争力,而这种核心竞争力是那些有着成熟工具链的引擎的团队极度难以形成的。总得来说,能力强,不怕延期,敢于投入的团队,才有机会扭转,获得业务导向的工具链带来的红利。</li> <li><strong>迭代时间</strong> (iteration time) 是日常开发中无可争议的 <strong>No. 1 Time Killer</strong>,直接影响你的工程效率。我在 Unreal 3 上工作时,有大量的场合 (如 Console Build) 是需要静态链接的,而 UE3 的链接奇慢无比,即使在中高配的电脑上也需要不下 10 分钟。而编译速度也很让人抓狂,即使有 Incredibuild 的分摊,一旦不幸改到头文件也是相当感人。运气不好的时候,一个小时改个两三次就一闪而过了 (下面这个 xkcd 真的一点也不夸张)。</li> </ul> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/compiling.png" width="413" height="360" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/compiling_hu0b189aeb3acf5caae5cf76e605151773_28315_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/compiling_hu0b189aeb3acf5caae5cf76e605151773_28315_1024x0_resize_box_3.png 1024w" loading="lazy" alt="compiling" class="gallery-image" data-flex-grow="114" data-flex-basis="275px" ></p> <p>由于在<strong>头文件的包含传染性</strong>和<strong>预编译的物理依赖关系</strong>上有欠考虑,不当依赖导致修改时的重编传染性极强,进而导致我们深入研究出一系列“避免动到头文件就能搞定需要的功能”的偏门神功,实在是说多了都是泪。好吧,反正这里已经黑了一把 UE3,干脆再来一个,负负得正吧。UnrealScript 是 Epic 专为虚幻引擎实现的脚本语言,无奈这货跟 C++ 的关系实在太紧密了 (如只要在涉及 native 的情况下改动变量就得重编 C++),紧密到运行时的动态能力已经损失殆尽,几乎已经没法拿来当脚本用了 (比如像 Lua 或 Python 那样轻松地在运行时 make change / run / reload )。好在 UE4 里已经把它去掉了,这里就不多说了。</p> <p>这三个方面都会直接影响到一个项目的工程时间开销,是一级的重点考察因素。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-16.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-16_hube990a1b57bdb5bb8b339093d7fbe7dc_90091_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-16_hube990a1b57bdb5bb8b339093d7fbe7dc_90091_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-16" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来是次一级的考察因素。这一级的考察因素侧重于引擎的工程质量,毕竟说到底,游戏引擎是一个软件项目——功能再花哨,如果根基不牢,那么做出来的游戏多半也是摇摇欲坠。团队规模越大,受工程质量高低的影响也会越大,严重时会直接影响到整个团队的节奏和士气,进而成为影响交付的难以克服的风险。</p> <p>我们先来看耐受力 (exception tolerance) 。什么是耐受力?简单说就是对非正常流程的处理能力。能够使用系统性的策略,而不是事到临头草草地 assert 来处理非法输入;能够结构性地把自己可以识别和处置的错误从无数的未定义行为中区分出来。说白了,耐受力意味着“<strong>工程上的安全感</strong>”。</p> <p>举个例子,同样是使用未初始化变量,在C/C++里你可能啥事儿没有,也可能直接宕机,也可能在极为偶然的情形下宕机,也可能看起来没啥事儿而过很长时间以后宕机。而在 Lua 里却不会有问题,并不是后者比前者高明,而仅仅是因为它对这一类问题有良好的定义。</p> <p>再比如说,一个编辑器,指定的操作稍微没有按照事先定义的流程来,立刻 assert ,非常敏感,一有风吹草动就 halt。很多程序员喜欢辩解说“尽早崩溃”是最佳实践——在遇到问题的第一时刻报错,报得越早越好,毕竟越接近问题发生的第一现场越方便他们调试。这个说法本身当然是没问题的——如果你用个默认值糊弄一下,或者是写某种容错逻辑来忍耐了破坏假定的行为,那么错误很有可能会蔓延到之后更加远离出问题的地方才爆发,那时丢失了源头和上下文,查错的成本会变得非常高。</p> <p>以下详细说明部分节选自俺的一篇未发布的文章,详细说明了一下这个问题。</p> <blockquote> <p>如果只考虑程序员,不考虑团队中同样依赖每日版本来工作的策划,美术和测试的话,这个思路是没有问题的。然而跟程序员不同,当发生崩溃时,团队内的其他成员能做的非常有限——以最快速度通知程序员,<strong>版本挂了</strong> (The build is broken!!!)。如果坏的地方正好是他们工作的部分,那么他们只好停下来,等待修好才能继续工作。否则要么一直备有一个可靠的老版本 ,要么手动回滚。</p> </blockquote> <blockquote> <p>现在请摘下程序员的帽子,假设自己是一个负责“测试多人副本,任务和活动”等业务逻辑相关的测试人员,每测一次都要花不少时间进入测试情境,偶尔甚至需要多人一起协同测试。那么一个跟你的工作毫不相关的底层崩溃,所带来的影响就会被迅速放大,很可能得完版本后的几个小时就在反复尝试和等待之中被消耗掉。</p> </blockquote> <blockquote> <p>这是一种惊人的浪费。</p> </blockquote> <blockquote> <p>给程序员巨大便利的“尽早崩溃”,对非程序员来说,意味着日常开发中的每一天,都要冒着被“不可控的因素”延误工作的风险。有人说,正确的姿势难道不是让程序员有更好的自律,在提交前做尽可能充分的测试,确保不要搞破坏吗?是的。可是即使是经验丰富的程序员也不能拍胸脯保证自己 bug-free, 更不能保证由若干人提交的若干“不相干”的改动集成到一起就能无缝地良好工作。让非程序员去承担这种因为版本不成熟导致的效率折扣,是<strong>既不公平也不高效</strong>的。</p> </blockquote> <blockquote> <p>说到“巨大便利”,不得不补充一个前缀——“本机上的”,也就是说,只有崩溃恰好发生在制造这个问题的程序员的机器上 (或可以方便地即时远程调试) 时,这种巨大的便利性才得到体现。考虑到发生在非程序员环境下的崩溃,不少情况下是由于环境配置错误等杂音所致,“有经验的”程序员往往不会浪费他们“宝贵的开发时间”,第一时间赶往现场开始分析和调试(打断自己的工作跑去协助调试,满头大汗弄了半天,发现是环境配置的问题/版本问题/别人代码导致的问题,足以唤醒一个温顺的程序猿内心的洪荒之力了)。更多的,他们会在 IM 上回个消息:“嗯,这个功能我提交前测试是正常的——你的环境干净吗?需要的数据都干净地重新生成了吗?第三方库的二进制文件更新了吗?你们几个人测试的版本一致吗?要不你 Cleanup / 重启 / 重新保存 / 重新建个账号试试?”,试图通过尽可能小的时间开销来帮助诊断和解决问题。长远来看,这些试图节省调试时间的沟通,会让“尽早崩溃”所带来的巨大便利慢慢地挥发殆尽。</p> </blockquote> <blockquote> <p>一个不那么容易觉察却更为严重的系统性问题是,总是采用“尽早崩溃”的实践的团队所产出的代码库,随着系统内不同模块之间的交互(以及随之而来的各种假定)越来越多,往往倾向于通过更多的断言来让系统变得越来越敏感和脆弱。因为,<strong>认真细致地考虑模块间的依赖时序,并系统性地从结构上解决过深的模块间耦合</strong>,总是比一个简单的断言要复杂得多。</p> </blockquote> <blockquote> <p>“尽早崩溃”的主张是如此的简洁有力,以至于我们在那些应当通过改良结构,去除耦合来解决问题的时刻,往往简单地选择了使用断言来做一个时序上的约定。这种显式的指定会把系统的坏气味转化成太多的不必要依赖。的确,问题从表面上看起来变得更简单了——谁破坏了断言,导致了崩溃,谁就修呗——实质上,修来修去,把一个本质上可以剥离的简单交互,变成了严重依赖各种时序和条件的“靠巧合工作”的杂耍系统。</p> </blockquote> <blockquote> <p>你看,“尽早崩溃”的简单性和便利性,在一些情况下反而成了一个让代码质量退化,鼓励系统熵不断增加的问题机制。那么问题来了,在满足了“必要的时候程序应当尽早崩溃”的基础上,还有什么可以选择的实践吗?</p> </blockquote> <blockquote> <p>(以下略)&hellip;</p> </blockquote> <p>到这里我应该已经基本说清楚什么是耐受力了。在图中我提到的三点分别是特殊需求,坏数据,破坏性的改动。这些都很直白,通过验证一个引擎在这些方面的表现,我们很容易对它的耐受力作出判断。</p> <p><strong>可见性</strong>也是重要的考虑因素。如果引擎能充分揭示自己的业务流程 (如何运作),生成的各类数据 (如何存储),关键模块的性能开销 (如何优化),那对各类基于引擎的二次开发才能更有信心,才能够最大限度地避免依赖了错误的假定。当出现问题时,也更容易查找和比对。而如何在维护最大的可见性的同时保持良好的封装和较低的耦合,同样是一个很大的话题,这里就不再细说了。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-17.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-17_hube990a1b57bdb5bb8b339093d7fbe7dc_103329_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-17_hube990a1b57bdb5bb8b339093d7fbe7dc_103329_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-17" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>接下来的页面上这些内容是更次要一些的因素,它们大多都很直白,这里我们挑着简单说一下。</p> <ul> <li>(<strong>teamscale-friendly</strong>) 不少引擎的工具链适应不了团队级的开发,尤其是大规模团队的开发。这类问题会在团队规模迅速增长,对引擎的平均理解程度迅速降低时暴露出来。</li> <li>(<strong>3rd-party-friendly</strong>) 商业引擎通常会有不少与第三方中间件的交互。考察这种交互的一个简单方法是观察那些“三不管”的薄弱地带。此前工作在 Unreal 上时,我曾短期地维护过一个模块,那个模块的业务逻辑依赖于 Unreal / Scaleform / Bink 这三方的适配。因为它们各自为政,在两两集成时,本来就是形式胜于实质,三方交互时就更为薄弱了。当大批量数据需要高频地在不同的中间件之间传递时,潜在的问题就会很容易集中爆发。</li> <li>(<strong>industry standard-friendly</strong>) 使用行业标准这一条就不多说了,我曾在一个游戏项目里见过七八个有着不同的 internal representation 的字符串类 (还不包括 std::string) 。光是字符串转换之间的各种细节,潜规则,优化手段,都够得上出本书了~ 想想你的工程师把他们宝贵的脑力消耗在这些事情上,实在是惊人的浪费~</li> </ul> <h3 id="游戏引擎的运用和改造">游戏引擎的运用和改造</h3> <p>说完了引擎的评估,接下来的运用和改造是很大的题目,对不同的项目类型也不尽相同。到这里不知不觉已有上万字了,为免冗长,我们提炼出一些相对通用的考虑和实践,就不考虑在单方面深入讨论了。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-18.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-18_hube990a1b57bdb5bb8b339093d7fbe7dc_127686_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-18_hube990a1b57bdb5bb8b339093d7fbe7dc_127686_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-18" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>在一个项目内涉及到引擎相关的部分,首当其冲的就是工作流的管理。项目是由人构成的,一个进行中的项目包含了许多显式或隐式的流程、约定和步骤,这些交互随着开发的进展不断动态变化。正如左下图那样,每个团队成员作为其中的一环或多环,有机地交互并推动项目的进展。要想让这个系统持续地有效运转,很重要的一点就是通过不断的观察、定位、梳理,来改进系统中响应速度跟不上整体节奏的环节。</p> <p>在做阶段性回顾时,我们容易把目光聚焦在“什么做了,什么还没做”上,却容易忽略对影响响应速度和导致效率损失的因素的及时处置。如果能够持续不断地观察和优化这些敏感点,我们就能发现,抛开每个成员技术方向和能力的差别不谈,绝大多数响应问题是由于 (过多的) 依赖导致的。这些依赖既有内部的,也有外部的;既有业务逻辑需求驱动的逻辑依赖,也有物理性的资源和数据依赖——当然最多的还是由于“<strong>针对工作流的分析和梳理严重不足</strong>” (俗话说的做到哪儿算哪儿) 导致的项目成员之间的无谓依赖。</p> <p>有种常见的说法是 Daily Build 就是项目的心跳,保证每日构建的安全、自动和鲁棒是最重要的。然而我认为这只是工程意义上的心跳,一个游戏项目的真正心跳在于“<strong>持续的可感知的进展</strong>” (Continuous sensable progress)。一个游戏项目内,任何一个可感知的点,不管是策划针对某个角色某个技能的构思或数值调整,还是美术对某个场景内某个特定氛围的塑造和创作,还是服务器程序对于一个特定功能 (如快速组队匹配) 的效率上的优化,都会通过或长或短的工作流程,最后在某个 Build 内以游戏内的一个可感知的点体现出来。我深切地感到,这样的<strong>基于实际体验点的持续而有节奏的交付</strong>,才是一个健康的游戏项目的真实心跳。而这个真实心跳是否能良好运转,跟工作流的响应效率和依赖处置是直接挂钩的。</p> <p>关于 hackers &amp; scientists 的区分对待,也是值得讨论的一点。“黑客”式的工程师会准确而锋利地切中要害,他们敏感,敏锐,敏捷,有惊人的直觉和洞察力,对大部分问题都能在很短时间内直截了当地给出“行还是不行”的答案,相应地,他们多数时候“事了拂衣去”,不那么在意严谨和完备,不愿意陷入琐碎的工程细节,也对编写日志,测试用例等等一切“官僚主义的形式材料”满不在乎。而与此相对的是,“科学家”式的工程师们,普遍周全,周密,细心和细致,能不厌其烦地追究每一个细节并给出妥善的应对方案,他们无比在意工程的完整性和完备性,产出的代码精确、详实而可靠,充分而周祥地考虑了各种可能出现的隐患和边界条件。</p> <p>很少有程序员能同时具备黑客和科学家的素质,所以这就要求我们能够感知并理解每个个体的行为方式,不断做针对性的调整和细化,从而让他们的积极特性在项目中能得到最大程度的放大。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-19.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-19_hube990a1b57bdb5bb8b339093d7fbe7dc_116521_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-19_hube990a1b57bdb5bb8b339093d7fbe7dc_116521_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-19" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这里是关于同步节奏的管理,图上已有表述,不再多说。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-20.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-20_hube990a1b57bdb5bb8b339093d7fbe7dc_165748_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-20_hube990a1b57bdb5bb8b339093d7fbe7dc_165748_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-20" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>针对引擎局限性举的 Gamebryo 例子。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-21.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-21_hube990a1b57bdb5bb8b339093d7fbe7dc_73920_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-21_hube990a1b57bdb5bb8b339093d7fbe7dc_73920_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-21" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这是魔兽世界里的兔子,详细的材料可见这里:<a class="link" href="https://www.zhihu.com/question/33634376/answer/125936478" target="_blank" rel="noopener" >有哪些看起来很高端的技术其实原理很暴力很初级?</a>。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-22.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-22_hube990a1b57bdb5bb8b339093d7fbe7dc_70666_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-22_hube990a1b57bdb5bb8b339093d7fbe7dc_70666_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-22" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>引擎不支持某个特性,并非总是坏事,也许正是你的游戏体现出差异性的好时机。有段时间,使用 UE3 开发的游戏扎堆出现,其中那些质量低下的作品,几乎总是让你一眼就能看出是在 UE3 上随便堆砌了些美术素材就放出来的昧心之作。而那些真正下功夫的 3A 之作,却总是能跨越技术的藩篱,(至少是在某一方面) 塑造出超越引擎能力的独特的体验。举个例子,如果 Android 本身在国内体验足够好的话,当时小米 MIUI 的独特本地化体验在 Android 阵营里也就没法那么出挑了。</p> <p>什么是伪命题呢?就是那些本质上不存在,却因为某种局限性,稳定性或性能问题而浮现出来的需求。作为程序员,我们经常在提炼 xx 需求的时候发现,只要能把 yy 和 zz 弄好, xx 的问题自然而然就消解了。但机会窗口并非总是存在,也许一下没想通,没理会 yy 和 zz 的潜在问题,手一抖把 xx 做了,之后叠床架屋地做了 n 层,然后再想回来改就已经改不动了。举例的话,不少游戏的热更都是如此,就不具体说了。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-23.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-23_hube990a1b57bdb5bb8b339093d7fbe7dc_66482_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-23_hube990a1b57bdb5bb8b339093d7fbe7dc_66482_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-23" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>关于技术负债,只要能意识到这很大程度上并非负面的因素,不要有太重的心理负担就好。在处置这些欠下的负债时,要有勇气不断地断舍离,随时扔下负重,轻装上阵。不要舍不得删代码。</p> <p>关于<strong>删代码</strong>有一篇非常有意思的文章,非常推荐阅读:</p> <ul> <li><a class="link" href="http://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to" target="_blank" rel="noopener" >(需翻墙) <strong>Write code that is easy to delete, not easy to extend</strong></a></li> </ul> <p>如果上面的链接无法访问的话可以看下面这个 Internet Archive Wayback Machine 版本的:</p> <ul> <li><a class="link" href="http://web.archive.org/web/20161215123811/http://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to" target="_blank" rel="noopener" >(需翻墙) Write code that is easy to delete, not easy to extend (Internet Archive)</a></li> </ul> <p>这篇文章在<a class="link" href="https://news.ycombinator.com/item?id=11093733" target="_blank" rel="noopener" > Hacker News 上的评论</a>也很有趣,一并推荐。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-24.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-24_hube990a1b57bdb5bb8b339093d7fbe7dc_95478_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-24_hube990a1b57bdb5bb8b339093d7fbe7dc_95478_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-24" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>我曾不止一次地听到有项目组在一个进行中的项目里热切地讨论换引擎,当问到我时我常常会尴尬地笑笑。比较而言,这种动议的出发点往往是产品角度,一般出自项目经理或产品负责人,很少由工程师提出,故而讨论的结果不会影响到实际的执行,所以除了笑笑也贡献不了啥有意义的想法。实际情况是,更换引擎是一个难度指数显著较高的操作,一般的团队不一定能克服得了这个困难,而这种风险往往会被 (有意或无意地) 低估。哦,对了,我还发现,那些曾经投入地讨论换引擎的,往往是折腾完了之后最早开始怀念在老的引擎上的好日子的同学。当然了,他们会拿出一百个理由告诉你这么干是值得的,嗯,好吧,毕竟生命在于折腾嘛。</p> <p>(第二部分完)</p> <h2 id="第三部分游戏引擎的下一个十年">第三部分:游戏引擎的<strong>下一个十年</strong></h2> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-25.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-25_hube990a1b57bdb5bb8b339093d7fbe7dc_62158_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-25_hube990a1b57bdb5bb8b339093d7fbe7dc_62158_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-25" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这一部分主要是我个人对游戏引擎方面的一些非常零碎的和个人化的整理和推断,以幻灯片的内容为主,文字材料从略,见谅。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-26.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-26_hube990a1b57bdb5bb8b339093d7fbe7dc_66666_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-26_hube990a1b57bdb5bb8b339093d7fbe7dc_66666_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-26" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>首先是下一代无线的标准——5G。我们可以看到,相较 4G,5G 的带宽和延时指标均提高到原来的 20 倍。用户面延时降低到了难以置信的 0.5ms。不仅仅是视频和直播类应用会从带宽和延时上受益,不少游戏引擎内的同步模块,拿这把尺子一衡量,立刻可以看出巨多的值得改进之处,就好像现代的图形引擎里,九十年代大当其道的 BSP 已经不见踪影了那样。传统引擎里的很多预测,补偿(<a class="link" href="http://gulu-dev.com/post/2016-07-24-id-network-model-evolution" target="_blank" rel="noopener" >这里</a>和<a class="link" href="http://gulu-dev.com/post/2014-03-15-dynamic-prediction-and-latency-compensation" target="_blank" rel="noopener" >这里</a>),都可以用更简明的方式去处理。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-27.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-27_hube990a1b57bdb5bb8b339093d7fbe7dc_151000_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-27_hube990a1b57bdb5bb8b339093d7fbe7dc_151000_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-27" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>在上图 (出处在这里:<a class="link" href="https://gist.github.com/jboner/2841832" target="_blank" rel="noopener" >Latency Numbers Every Programmer Should Know</a>) 我们可以看到,传统的磁盘寻道约耗时 10ms (<a class="link" href="http://baike.baidu.com/link?url=eWpj_1GfHNw9O3WnFE7n0x9D2J_AHw94ESdXmyxRKNXNIskHaAqfDUK3k1ZaKMTDbZOFV2DMGdoTQWwEY9J0X_" target="_blank" rel="noopener" >百度百科上这个数据为 7.5~ 14ms</a>),图上的问题值得思考:当你的网络连接速度远快于 (10x) 本机的磁盘寻道时,你的引擎将应该如何来设计和实现你的资源管理及网络同步机制?不夸张地说,这将是一个牵一发而动全身的问题。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-28.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-28_hube990a1b57bdb5bb8b339093d7fbe7dc_168032_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-28_hube990a1b57bdb5bb8b339093d7fbe7dc_168032_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-28" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>在 2016 年,我们见证了首代消费级的 VR 产品。由于技术尚不成熟,在视觉呈现上有明显的纱门效应。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-29.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-29_hube990a1b57bdb5bb8b339093d7fbe7dc_104962_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-29_hube990a1b57bdb5bb8b339093d7fbe7dc_104962_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-29" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>由于巨大的显示带宽需求,眼下的 VR 设备线材繁多,玩家行动受到颇多限制。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-30.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-30_hube990a1b57bdb5bb8b339093d7fbe7dc_138738_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-30_hube990a1b57bdb5bb8b339093d7fbe7dc_138738_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-30" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>nVidia 的 MRS 技术,把四周的拉伸区域使用较低的分辨率渲染,用于提高渲染效率和降低带宽需求。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-31.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-31_hube990a1b57bdb5bb8b339093d7fbe7dc_79905_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-31_hube990a1b57bdb5bb8b339093d7fbe7dc_79905_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-31" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p><a class="link" href="http://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash" target="_blank" rel="noopener" >Abrash 在 Connect 3 上提到</a>的凹式渲染,用于动态地进一步降低需要传输和渲染的像素量。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-32.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-32_hube990a1b57bdb5bb8b339093d7fbe7dc_115955_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-32_hube990a1b57bdb5bb8b339093d7fbe7dc_115955_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-32" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>经由类似上面的技术,我们将有机会在不久后见到更细致的画面的同时,摆脱线缆的束缚。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-33.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-33_hube990a1b57bdb5bb8b339093d7fbe7dc_132359_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-33_hube990a1b57bdb5bb8b339093d7fbe7dc_132359_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-33" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>而真正的临场感 (Presence) 由于<a class="link" href="http://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash#toc_11" target="_blank" rel="noopener" >各方面的限制 (见链接中结语的 “right problems” 一节)</a>,还需要更久后才能实现。</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-34.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-34_hube990a1b57bdb5bb8b339093d7fbe7dc_61701_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-34_hube990a1b57bdb5bb8b339093d7fbe7dc_61701_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-34" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>看到这些值得深入探寻一番的技术点之后,我们回头看了一眼第一部分的结论——物竞天择,适者生存。还记得大明湖畔的 CryEngine、Gamebryo 和 Torque 吗?</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-35.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-35_hube990a1b57bdb5bb8b339093d7fbe7dc_114439_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-35_hube990a1b57bdb5bb8b339093d7fbe7dc_114439_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-35" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>(这一页是充话费送的)</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-36.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-36_hube990a1b57bdb5bb8b339093d7fbe7dc_83769_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-36_hube990a1b57bdb5bb8b339093d7fbe7dc_83769_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-36" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>(嗯,这一页也是充话费送的)</p> <p><img src="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-37.jpg" width="1280" height="720" srcset="https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-37_hube990a1b57bdb5bb8b339093d7fbe7dc_74910_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2017-01-15-game-engine-talk/images/game-engine-tech-37_hube990a1b57bdb5bb8b339093d7fbe7dc_74910_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game-engine-tech-37" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>居然在结束的页面上第三次提起达尔文,这得是个有多迷恋进化论的人啊~~~</p> <hr> <p>[注]</p> <ul> <li>本文首先发布于公众号<a class="link" href="http://mp.weixin.qq.com/s/a5hy2Ie_v0KNNjdMESFAUg" target="_blank" rel="noopener" >西山居技术</a>。</li> <li>本文同时发布在 <a class="link" href="https://zhuanlan.zhihu.com/p/24927079" target="_blank" rel="noopener" >知乎专栏链接</a> 可以到那里请我喝 1/3 杯咖啡,交个朋友吧:)</li> <li>本文幻灯片见:<a class="link" href="2016-12-17-game-engine-talk-gulu.pdf" >(2016) 游戏引擎技术点滴 (2016金山技术开放日)</a>。</li> <li><a class="link" href="http://mp.weixin.qq.com/s/8fPJdo-yeQC4Uz9vdyVBIQ" target="_blank" rel="noopener" >腾讯 GAD 游戏开发者平台</a>,<a class="link" href="http://mp.weixin.qq.com/s/IR55o7pxPjcYzfkXkpJRjg" target="_blank" rel="noopener" >游戏葡萄</a>,<a class="link" href="http://mp.weixin.qq.com/s/CskLzklO_cVSUun8R5hKZg" target="_blank" rel="noopener" >GameRes 游资网</a> 是三家我授权过的媒体。其他的未授权转载如存在内容不一致的情况,以 <a class="link" href="http://gulu-dev.com/post/2017-01-15-game-engine-talk" target="_blank" rel="noopener" >gulu-dev.com 上的链接内容</a> 为准。</li> </ul> <p>[补]</p> <ul> <li>本文发布后,意外地收到了很多同学的反馈和鼓励,其中不乏有洞察力的思考和分享,这正是提笔交流的意义所在。至于内容本身不够充实,偏科普向的问题,的确存在,还请多多包涵,毕竟这是由一个介绍性的演讲汇总而来的,在深度和专业性方面会相对弱一些。</li> </ul> 2016.12 Unity 协程运行时的监控和优化 https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/ Tue, 20 Dec 2016 20:20:00 +0000 https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/ <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p1.png" width="1252" height="741" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p1_hu3bea98489960e9c1a1ae84b653cef453_105535_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p1_hu3bea98489960e9c1a1ae84b653cef453_105535_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p1" class="gallery-image" data-flex-grow="168" data-flex-basis="405px" ></p> <p>协程 (Coroutine) 是大部分现代编程环境都提供的一个非常有用的机制。它允许我们把不同时刻发生的行为,在代码中以线性的方式聚合起来。与基于事件与回调的系统相比,以协程方式组织的业务逻辑,可读性相对好一些。</p> <p>Unity 内的协程实现是传统协程的简化——在主线程内每一帧给定的时间点上,引擎通过一定的调度机制来唤醒和执行满足条件的协程,以实际上的分时串行化执行回避了协程之间的通信问题。但由于种种因素,协程的执行情况对程序员而言相对不那么透明,可以通过一些简单的机制来对其进行监控和优化。</p> <h2 id="warm-up-从复用-yield-对象说起">Warm up: 从复用 Yield 对象说起</h2> <p>先从一个最简单而直接的改进开始吧。下面一个在每帧结束时执行的协程的例子:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">void</span> <span class="n">Start</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">StartCoroutine</span><span class="p">(</span><span class="n">OnEndOfFrame</span><span class="p">());</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">IEnumerator</span> <span class="n">OnEndOfFrame</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">yield</span> <span class="k">return</span> <span class="k">null</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">while</span> <span class="p">(</span><span class="k">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">//Debug.LogFormat(&#34;Called on EndOfFrame.&#34;);</span> </span></span><span class="line"><span class="cl"> <span class="k">yield</span> <span class="k">return</span> <span class="k">new</span> <span class="n">WaitForEndOfFrame</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>在 Profiler 内可以看到,上面的代码会导致 <code>WaitForEndOfFrame</code> 对象的每帧分配,给 GC 增加负担。假设游戏内有 10 个活跃协程,运行在 60 fps,那么每秒钟的 GC 增量负担是 <code>10 * 60 * 16 = 9.6 KB/s</code>。</p> <p>我们可以简单地通过复用一个全局的 <code>WaitForEndOfFrame</code> 对象来优化掉这个开销:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">static</span> <span class="n">WaitForEndOfFrame</span> <span class="m">_</span><span class="n">endOfFrame</span> <span class="p">=</span> <span class="k">new</span> <span class="n">WaitForEndOfFrame</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><p>在合适的地方创建一个全局共享的 <code>_endOfFrame</code> 之后,只需要把上面的代码改为</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="k">yield</span> <span class="k">return</span> <span class="m">_</span><span class="n">endOfFrame</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span></code></pre></td></tr></table> </div> </div><p>上面的 9.6 KB/s 的 GC 开销就被完全避免了,而逻辑上与优化前完全没有任何区别。</p> <p>实际上,所有继承自 <code>YieldInstruction</code> 的用于挂起协程的指令类型,都可以使用全局缓存来避免不必要的 GC 负担。常见的有:</p> <ul> <li><code>WaitForSeconds</code></li> <li><code>WaitForFixedUpdate</code></li> <li><code>WaitForEndOfFrame</code></li> </ul> <p>在 <a class="link" href="https://github.com/PerfAssist/PA_Common/blob/master/Scripts/Yielders.cs" target="_blank" rel="noopener" ><code>Yielders.cs</code></a> 这个文件里,集中地创建了上面这些类型的静态对象,使用时可以直接这样:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="k">yield</span> <span class="k">return</span> <span class="n">Yielders</span><span class="p">.</span><span class="n">GetWaitForSeconds</span><span class="p">(</span><span class="m">1.0f</span><span class="p">);</span> <span class="c1">// wait for one second</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span></code></pre></td></tr></table> </div> </div><h2 id="coroutine-的工作原理">Coroutine 的工作原理</h2> <p>观察调用链可知,Unity Coroutine 的调用约定靠返回的 <code>IEnumerator</code> 对象来维系。我们知道 <code>IEnumerator</code> 的核心功能函数是:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="n">MoveNext</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个函数在每次被 Unity 协程调度函数 (通常是协程所在类的 SetupCoroutine()) 唤醒时调用,用于驱动对应的协程由上一次 yield 语句开始执行下面的代码段,直到下一条 yield 语句 (对应返回 true) 或函数退出 (对应返回 false)。</p> <p>下图是一次典型的协程调用:</p> <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/coroutine-execution.png" width="490" height="530" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/coroutine-execution_hubd50c4220fda455bb3a9f8ba3b05e3ba_13497_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/coroutine-execution_hubd50c4220fda455bb3a9f8ba3b05e3ba_13497_1024x0_resize_box_3.png 1024w" loading="lazy" alt="2016-12-20-unity-coroutine-optimizing" class="gallery-image" data-flex-grow="92" data-flex-basis="221px" ></p> <p>图中的绿色实心方块是协程实际的活跃执行时间。可以看出,一个协程的完整生命周期是**“在整个生命周期内对其内部所有代码段的一个遍历并依次执行”**的过程。</p> <h2 id="接管和监控-coroutine-的行为">接管和监控 Coroutine 的行为</h2> <h3 id="问题描述">问题描述</h3> <p>由于以下几点问题的存在,协程的执行情况对开发者而言并不透明,很容易在开发过程中引入性能问题。</p> <ol> <li>协程 (除了首次执行) 不是在用户的函数内触发,而是在单独的 <code>SetupCoroutine()</code> 内被激活并执行</li> <li>协程的每次活跃执行,在代码上以单次 yield 为界限。对于具有复杂分支的业务逻辑,<strong>尤其是“本来在主流程内,后来被协程化”的代码</strong>,很难看出每一段 yield 的潜在执行量</li> <li>实践中,如果同时激活的协程较多,就可能会出现多个高开销的协程挤在同一帧执行导致的卡帧。这一类卡顿难以复现和调查。</li> </ol> <h3 id="中间层-trackedcoroutine">中间层 <code>TrackedCoroutine</code></h3> <p>针对这些情况,我们可以在主流程和协程之间添加一层 Wrapper,来接管和监控实际协程的执行情况。具体地说,可以实现一个纯转发的 IEnumerator,如下的缩减版所示:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span><span class="lnt">32 </span><span class="lnt">33 </span><span class="lnt">34 </span><span class="lnt">35 </span><span class="lnt">36 </span><span class="lnt">37 </span><span class="lnt">38 </span><span class="lnt">39 </span><span class="lnt">40 </span><span class="lnt">41 </span><span class="lnt">42 </span><span class="lnt">43 </span><span class="lnt">44 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">TrackedCoroutine</span> <span class="p">:</span> <span class="n">IEnumerator</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">IEnumerator</span> <span class="m">_</span><span class="n">routine</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">TrackedCoroutine</span><span class="p">(</span><span class="n">IEnumerator</span> <span class="n">routine</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="m">_</span><span class="n">routine</span> <span class="p">=</span> <span class="n">routine</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// 在这里标记协程的创建</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kt">object</span> <span class="n">IEnumerator</span><span class="p">.</span><span class="n">Current</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">get</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="m">_</span><span class="n">routine</span><span class="p">.</span><span class="n">Current</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="kt">bool</span> <span class="n">MoveNext</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 在这里可以:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 1. 标记协程的执行</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 2. 记录协程本次执行的时间</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="n">next</span> <span class="p">=</span> <span class="m">_</span><span class="n">routine</span><span class="p">.</span><span class="n">MoveNext</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">next</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 一次普通的执行</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 协程运行到末尾,已结束</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">next</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">void</span> <span class="n">Reset</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="m">_</span><span class="n">routine</span><span class="p">.</span><span class="n">Reset</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>完整版的代码见 <a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker/blob/master/Assets/PerfAssist/CoroutineTracker/RuntimeCoroutineTracker.cs#L44" target="_blank" rel="noopener" >TrackedCoroutine</a> 类的实现。</p> <p>有了这样一个 <code>TrackedCoroutine</code> 之后,我们就可以把正常的</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="n">abc</span><span class="p">.</span><span class="n">StartCoroutine</span><span class="p">(</span><span class="n">xxx</span><span class="p">());</span> </span></span></code></pre></td></tr></table> </div> </div><p>替换为</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="n">abc</span><span class="p">.</span><span class="n">StartCoroutine</span><span class="p">(</span><span class="k">new</span> <span class="n">TrackedCoroutine</span><span class="p">(</span><span class="n">xxx</span><span class="p">()));</span> </span></span></code></pre></td></tr></table> </div> </div><h3 id="启动函数-invokestart">启动函数 <code>InvokeStart()</code></h3> <p>在 <a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker/blob/master/Assets/PerfAssist/CoroutineTracker/RuntimeCoroutineTracker.cs#L101" target="_blank" rel="noopener" >RuntimeCoroutineTracker</a> 类中,可以看到以下两个接口,针对以 <code>IEnumerator</code>,<code>string</code>,及可选的单参形式等三种形式的协程启动的封装。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">RuntimeCoroutineTracker</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Coroutine</span> <span class="n">InvokeStart</span><span class="p">(</span><span class="n">MonoBehaviour</span> <span class="n">initiator</span><span class="p">,</span> <span class="n">IEnumerator</span> <span class="n">routine</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Coroutine</span> <span class="n">InvokeStart</span><span class="p">(</span><span class="n">MonoBehaviour</span> <span class="n">initiator</span><span class="p">,</span> <span class="kt">string</span> <span class="n">methodName</span><span class="p">,</span> <span class="kt">object</span> <span class="n">arg</span> <span class="p">=</span> <span class="k">null</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>上面的外部调用就可以替换为:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="n">RuntimeCoroutineTracker</span><span class="p">.</span><span class="n">InvokeStart</span><span class="p">(</span><span class="n">abc</span><span class="p">,</span> <span class="n">xxx</span><span class="p">());</span> </span></span></code></pre></td></tr></table> </div> </div><p>至此,藉由一个中间层 <code>TrackedCoroutine</code>,我们得以接管和监控所有协程的单次运行过程。</p> <h3 id="监控-plugins-内的协程">监控 Plugins 内的协程</h3> <p>由于 Plugins 目录单独编译,无法直接调用外部的功能,这里我们为所有的插件提供一个转发机制,用于把插件内启动协程的请求转发到上面的启动函数。</p> <p>首先定义两个委托:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">delegate</span> <span class="n">Coroutine</span> <span class="n">CoroutineStartHandler_IEnumerator</span><span class="p">(</span><span class="n">MonoBehaviour</span> <span class="n">initiator</span><span class="p">,</span> <span class="n">IEnumerator</span> <span class="n">routine</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="k">public</span> <span class="k">delegate</span> <span class="n">Coroutine</span> <span class="n">CoroutineStartHandler_String</span><span class="p">(</span><span class="n">MonoBehaviour</span> <span class="n">initiator</span><span class="p">,</span> <span class="kt">string</span> <span class="n">methodName</span><span class="p">,</span> <span class="kt">object</span> <span class="n">arg</span> <span class="p">=</span> <span class="k">null</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>然后把实际的协程请求转发给这两个委托:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">CoroutinePluginForwarder</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Coroutine</span> <span class="n">InvokeStart</span><span class="p">(</span><span class="n">MonoBehaviour</span> <span class="n">initiator</span><span class="p">,</span> <span class="n">IEnumerator</span> <span class="n">routine</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">InvokeStart_IEnumerator</span><span class="p">(</span><span class="n">initiator</span><span class="p">,</span> <span class="n">routine</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Coroutine</span> <span class="n">InvokeStart</span><span class="p">(</span><span class="n">MonoBehaviour</span> <span class="n">initiator</span><span class="p">,</span> <span class="kt">string</span> <span class="n">methodName</span><span class="p">,</span> <span class="kt">object</span> <span class="n">arg</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">InvokeStart_String</span><span class="p">(</span><span class="n">initiator</span><span class="p">,</span> <span class="n">methodName</span><span class="p">,</span> <span class="n">arg</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>最后在运行时注册两个委托即可:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="n">CoroutinePluginForwarder</span><span class="p">.</span><span class="n">InvokeStart_IEnumerator</span> <span class="p">=</span> <span class="n">RuntimeCoroutineTracker</span><span class="p">.</span><span class="n">InvokeStart</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="n">CoroutinePluginForwarder</span><span class="p">.</span><span class="n">InvokeStart_String</span> <span class="p">=</span> <span class="n">RuntimeCoroutineTracker</span><span class="p">.</span><span class="n">InvokeStart</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>完整的代码实现见 <a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker/blob/master/Assets/Plugins/CoroutineTracker/CoroutinePluginForwarder.cs#L7" target="_blank" rel="noopener" >CoroutinePluginForwarder</a> 类。</p> <h2 id="perfassist-组件---coroutinetracker-on-githubhttpsgithubcomperfassistpa_coroutinetracker">PerfAssist 组件 - CoroutineTracker (<a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker" target="_blank" rel="noopener" >on GitHub</a>)</h2> <p>在上面这些实现的基础上,前段时间我实现了一个编辑器内的工具面板 CoroutineTracker ,用于帮助开发者监控和分析系统内协程的运行情况。</p> <ul> <li><a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker" target="_blank" rel="noopener" >https://github.com/PerfAssist/PA_CoroutineTracker</a></li> </ul> <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p1.png" width="1252" height="741" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p1_hu3bea98489960e9c1a1ae84b653cef453_105535_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p1_hu3bea98489960e9c1a1ae84b653cef453_105535_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p1" class="gallery-image" data-flex-grow="168" data-flex-basis="405px" ></p> <h3 id="功能介绍">功能介绍</h3> <p>左边的四列是程序运行时所有被追踪协程的实时的启动次数,结束次数,执行次数和执行时间。</p> <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p2.png" width="941" height="572" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p2_hue95e2faafcba4d56932a7d59ca586f7c_23407_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p2_hue95e2faafcba4d56932a7d59ca586f7c_23407_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p2" class="gallery-image" data-flex-grow="164" data-flex-basis="394px" ></p> <p>当点击图形上任何一个位置时,选中该时间点(秒为单位),在图形上是绿色竖条。</p> <p>此时右边的数据报表刷新为在这一秒中活动的所有协程的列表,如下图所示:</p> <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p3.png" width="603" height="467" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p3_huc391e13c058e65330667f10b4130c105_46173_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p3_huc391e13c058e65330667f10b4130c105_46173_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p3" class="gallery-image" data-flex-grow="129" data-flex-basis="309px" ></p> <p>注意,该表中的数据依次为:</p> <ul> <li>协程的完整修饰名 (mangled name)</li> <li>在选定时间段内的执行次数 (selected execution count)</li> <li>在选定时间段内的执行时间 (selected execution time)</li> <li>到该选中时间为止时总的执行次数 (summed execution count)</li> <li>到该选中时间为止时总的执行时间 (summed execution time)</li> </ul> <p>可以通过表头对每一列的数据进行排序。</p> <p>当选中列表中某一个协程时,面板的右下角会显示该协程的详细信息,如下图所示:</p> <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p4.png" width="632" height="324" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p4_hu2682d95da700c4f88f524ed37d71b63e_42190_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p4_hu2682d95da700c4f88f524ed37d71b63e_42190_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p4" class="gallery-image" data-flex-grow="195" data-flex-basis="468px" ></p> <p>这里有下面的信息:</p> <ul> <li>该协程的序列 ID (sequence ID)</li> <li>启动时间 (creation time)</li> <li>结束时间 (termination time)</li> <li>启动时堆栈 (creation stacktrace)</li> </ul> <p>向下滚动,可看到该协程的完整执行流程信息,如下图所示:</p> <p><img src="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p5.png" width="655" height="353" srcset="https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p5_hu8d94be2ee2fa7f55e53cdba60508dd97_34468_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-20-unity-coroutine-optimizing/images/p5_hu8d94be2ee2fa7f55e53cdba60508dd97_34468_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p5" class="gallery-image" data-flex-grow="185" data-flex-basis="445px" ></p> <h3 id="常见问题调查">常见问题调查</h3> <p>使用这个工具,我们可以更方便地调查下面的问题:</p> <ul> <li>yield 过于频繁的</li> <li>单次运行时间太久的</li> <li>总时间开销太高的</li> <li>进入死循环,始终未能正确结束掉的</li> <li>递归 yield 产生过深执行层次的</li> </ul> <hr> <p>[注]</p> <ul> <li>本文同时发在<a class="link" href="http://gulu-dev.com/post/perf_assist/2016-12-20-unity-coroutine-optimizing" target="_blank" rel="noopener" >我的 blog</a> 和<a class="link" href="https://zhuanlan.zhihu.com/p/24519241" target="_blank" rel="noopener" >知乎专栏</a></li> <li>本文已授权侑虎科技的<a class="link" href="http://mp.weixin.qq.com/s/y6A4OkcqlCegu7uAD2nOPQ" target="_blank" rel="noopener" >公众号转载</a></li> <li>本文遵循 <a class="link" href="http://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener" >Creative Commons BY-NC-ND 4.0</a> 许可协议。</li> <li><a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker" target="_blank" rel="noopener" >CoroutineTracker</a> 工具是 <a class="link" href="https://github.com/PerfAssist" target="_blank" rel="noopener" >PerfAssist</a> 套件的一部分,后续的改进和更新都会出现在那里。</li> <li>如果在使用时遇到问题,欢迎直接在 <a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker" target="_blank" rel="noopener" >GitHub</a> 上发 Issues 或 Pull Requests 给我,往往能比评论得到更快速的回复。</li> </ul> <hr> <p>[补]</p> <ul> <li> <p>[2017-01-06] 多谢评论中的 @CM 君,此问题已修复。</p> <ul> <li>@CM 君提到,“hi, 我看到Yielders注释写道Dictionary以值类型作Key会产生GC,不是是否有进行过实际的测试。我在Profiler中看过Dictionary的ContainsKey、ContainsValue、[Key]等操作均无GC产生。”</li> <li>这段代码的原出处在<a class="link" href="https://forum.unity3d.com/threads/c-coroutine-waitforseconds-garbage-collection-tip.224878/" target="_blank" rel="noopener" >这里</a>。我在 Unity 5.5.0 下已验证,以 float 作为 Dictionary 的 Key 时,确实不会像注释中描述的那样,产生 GC。最新的 <a class="link" href="https://github.com/PerfAssist/PA_Common/blob/master/Scripts/Yielders.cs" target="_blank" rel="noopener" >Yielders.cs</a> 中已修复此情况。</li> </ul> </li> <li> <p>[2017-01-06] 评论中提到的找不到文件的情况,是因为所缺的问价在子库 <a class="link" href="https://github.com/PerfAssist/PA_Common" target="_blank" rel="noopener" >PA_Common</a> 中,见<a class="link" href="https://github.com/PerfAssist/PA_CoroutineTracker/tree/master/Assets/PerfAssist" target="_blank" rel="noopener" >此页面</a>上对该子库的引用。使用 &ldquo;git submodule &hellip;&rdquo; 更新到本地即可。</p> <p>​</p> </li> </ul> 2016.12 VR 的未来五年 - Michael Abrash 在 Oculus Connect 3 上的回顾与展望 https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/ Sun, 18 Dec 2016 06:24:00 +0000 https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/ <hr> <p>[按] 本文刊发于<a class="link" href="http://geek.csdn.net/news/detail/127530" target="_blank" rel="noopener" >《程序员》2016年12月</a>,转载请与该刊物联系。</p> <hr> <p>年度 VR 盛会 Oculus Connect 3 上,Michael Abrash 和 John Carmack 和往年一样带来了精彩的演讲。和以往一样,<a class="link" href="https://www.youtube.com/watch?v=Mv_eIRv1Vk4" target="_blank" rel="noopener" >Abrash 的发言</a> 以前瞻性的研究和底层技术发展趋势为主,而 <a class="link" href="https://www.youtube.com/watch?v=zlxyjx3bJ28" target="_blank" rel="noopener" >Carmack 的发言</a> 一如既往地更关注在当下的平台上 (主要是移动 VR),通过工程化努力可以把交互和应用做到什么程度。</p> <p>本篇主要包含了与 Michael Abrash 的演讲相关的内容。</p> <h2 id="回顾">回顾</h2> <p>开始之前,我们先简单回顾一下 Abrash 在近三年前 (2014年1月) 的 <a class="link" href="http://www.steamdevdays.com/2014/" target="_blank" rel="noopener" >Steam Dev Days 2014</a> 上曾作出的判断和预测——在名为 <a class="link" href="http://www.youtube.com/watch?v=G-2dQoeqVVo&amp;list=PLckFgM6dUP2hc4iy-IdKFtqR9TeZWMPjm" target="_blank" rel="noopener" >&ldquo;What VR Could, Should, and Almost Certainly Will Be within Two Years&rdquo;</a> 的演讲中,Abrash 的开场白就说到 “Compelling consumer-priced VR hardware is coming, probably within two years” (&ldquo;消费级 VR 硬件将会在两年内出现&rdquo;)。而正是两年后的 2016 年,三大厂商都把自己的产品送到了消费者手中。</p> <p>下面这些是当时的部分辑录,而括号内是实际发生的情况。</p> <ul> <li>&ldquo;We strongly believe that it’s feasible to use the same technology to ship a consumer version within two years.&rdquo; (成品 Oculus Rift 和 HTC Vive 均于 2016 年发售)</li> <li>&ldquo;You see, for latency and bandwidth reasons, presence can only happen with <strong>a head-mounted display connected to a device capable of heavy-duty 3D rendering</strong>, so there’s no way that TV, movies, streaming, or anything that lacks lots of local compute power is up to the task. A corollary is that the <strong>PC – Linux, Windows, and OSX – is going to be the best place for VR</strong>, because that’s where the most FLOPs are.&rdquo; (成品 Oculus Rift 和 HTC Vive 都依托于 PC 平台)</li> <li>&ldquo;Presence starts to work somewhere around an 80 degree field of view, and improves significantly at least out to 110 degrees.&rdquo; (成品 Oculus Rift 和 HTC Vive 的 fov 都是 110°)</li> <li>&ldquo;We’ve found that 1080p seems to be enough for presence. We expect that 1440p, or better yet 2160p would be huge steps up.&rdquo; (成品 Oculus Rift 和 HTC Vive 的分辨率都是 2160 x 1200)</li> <li>&ldquo;we built the fastest low-persistence headmounted display we could; it runs at 95 Hz, and that successfully eliminates visible flicker.&rdquo; (成品 Oculus Rift 和 HTC Vive 的刷新率都是 90Hz)</li> </ul> <p>基本上可以说在 2014 年时,Abrash 就把两年后的第一代 VR 设备的大部分硬件参数给挨个“钦点”了,其判断和预见力可见一斑。</p> <h2 id="愿景">愿景</h2> <p>快速扫过两年前的演讲后,我们把目光收回到 Abrash 在 Oculus Connect 3 上的发言。</p> <p>在开场白中 Abrash 说到,随着几大消费级产品的发布,今年是 VR 行业的重要一年,这一年里行业内发生的诸多重量级事件,从五年前来看简直无法想象。Abrash 搬出了自己曾在 20 年前的 GDC 上引用过的话,“Pretty soon, computers will be fast” 来说明,我们对于趋势的发展是存在认知偏差的,我们会被当时的条件局限,但技术的发展从不会停下脚步,只会加速向前。现在对我们来说稳定在 90 帧是一个难以达到的目标,但以几年后的眼光来看实属平常。</p> <p>事实上,理解这个认知偏差对我们很有用,它给了我们工作的意义。读过 <a class="link" href="https://book.douban.com/subject/1152971/" target="_blank" rel="noopener" >Doom 启示录</a> 和 <a class="link" href="https://github.com/jagregory/abrash-black-book" target="_blank" rel="noopener" >Michael Abrash&rsquo;s Graphics Programming Black Book</a> 的同学,应该对 Abrash 接下来讲的这段故事比较熟悉了。</p> <p>Abrash 说自己毕业的时候压根没想过什么工作的意义。那时候虽然在一个偶然的机会里知道了个人计算机,也觉得比自己所在的行业有趣得多,可当时考虑的更多的是怎么找到一份有趣而又体面的工作,对趋势和发展并没太在意。当他逐渐明白过来时,已过去了太多时间,直到 15 年后跟 John Carmack 相遇。</p> <p>1993 年时,Michael Abrash 是第一代 Windows NT 的图形组经理,当时他被朋友手上泄漏出来的 DOOM 给震撼了,就直接给 John 发了封邮件。他们约着在西雅图一起吃饭时,Abrash 拒绝了卡神一起工作的邀约——他觉得在微软搞搞图形蛮有意思的,而且还有不少股权。</p> <p>然而这不是一个十动然拒的故事,Abrash 无意间读了《雪崩》这个科幻小说之后,突然间觉得那书里提到的大部分 VR 场景实际上是可能实现的 (运用当时的图形技术) 这里 Abrash 用了非常细致的措辞 —— “It could actually work, maybe not right then, or quite the way that Neal Stephenson described it but <strong>close enough at some point</strong> in the foreseeable future”</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-02.jpg" width="1010" height="597" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-02_huef4c9e0e35c65242de268f44951f3916_60452_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-02_huef4c9e0e35c65242de268f44951f3916_60452_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="abrash-02" class="gallery-image" data-flex-grow="169" data-flex-basis="406px" ></p> <p>一年后,John 又找他吃饭聊天时,出乎他意料的是,John 坐下来后开始畅谈自己对未来的愿景,丝毫没提拉他入伙的事情。John 提到怎么让玩家自己去一步步搭建固定的服务器,定制,扩展,互连,以及随之产生出某种意义上的“网络空间” (cyberspace),这是在 1994 年。John 连着说了两个钟头,他描绘的愿景跟雪崩里描绘的 VR 场景在 Abrash 的脑海里不断共振,在那一刹那,未来清晰地在他的眼前浮现出来。头一次,他觉得自己找到了一件比写牛X代码更有意义的事儿。接着他答应了 John 的邀请。</p> <p>&ldquo;It was by far the worst financial decision of my life.&rdquo; (“这是我这辈子做过的最糟糕的财务决定。”)Abrash 微笑着说完这句话以后,全场哄堂大笑,“微软的股票在那之后两年里翻了三倍,然后又翻倍,再次翻倍……”</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-04.png" width="1128" height="647" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-04_hud5f8cbaf2b757809596f1ed91f19b43f_177458_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-04_hud5f8cbaf2b757809596f1ed91f19b43f_177458_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-04" class="gallery-image" data-flex-grow="174" data-flex-basis="418px" ></p> <p>注意,事实证明,John 说到的愿景并不是一厢情愿的猜测,他一步一步地使之成为现实。下面的这幅图是 id Tech 在 <a class="link" href="https://en.wikipedia.org/wiki/Id_Tech" target="_blank" rel="noopener" >Wikipedia 页面</a> 上的配图,图中是 Quake 的各种衍生,某种意义上可以说 Doom/Quake 从根基上影响了其后的大多数现代 3D 引擎,包括目前应用最为广泛的商业引擎 Unreal。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/Quake_-_family_tree_2.svg.png" width="471" height="599" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/Quake_-_family_tree_2.svg_hua89cd9d01262c4c563503d467f4906a2_68831_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/Quake_-_family_tree_2.svg_hua89cd9d01262c4c563503d467f4906a2_68831_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Quake_-_family_tree_2.svg" class="gallery-image" data-flex-grow="78" data-flex-basis="188px" ></p> <p>接着 Abrash 直接把时间线拖到了去年的 Connect 2 (见下图,具体内容见此前做过的记录:<a class="link" href="http://gulu-dev.com/post/2015-10-18-oculus-connect-2-michael-abrash-keynote" target="_blank" rel="noopener" >Oculus Connect 2 首席科学家 Michael Abrash 发言实录</a> )。他动情地说到,是一致的愿景让我们愿意把时间和精力投入到 VR 上来,明白自己正在一步步推动这个愿景的实现让这些努力都更具意义。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-05.jpg" width="1162" height="652" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-05_hu2ef58073e89bafdb9bcff2e9f4dda9f3_102174_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-05_hu2ef58073e89bafdb9bcff2e9f4dda9f3_102174_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="abrash-05" class="gallery-image" data-flex-grow="178" data-flex-basis="427px" ></p> <h2 id="预测">预测</h2> <p>三年前的预言大部分成为了现实,这次 Abrash 试着预测他眼中关于 VR 底层平台基础设施的今后五年的发展趋势和演化方向。这些预测都是针对高端 PC 平台的,毕竟相比移动 VR,高性能的 PC 在电力和运算能力上有着巨大的优势。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-06.png" width="1037" height="568" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-06_hu71933c9c10346740bf8862c2c7f703f5_184036_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-06_hu71933c9c10346740bf8862c2c7f703f5_184036_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-06" class="gallery-image" data-flex-grow="182" data-flex-basis="438px" ></p> <p>主要包括七个方面:</p> <ol> <li>光学和显示 (Optics and displays)</li> <li>图形 (Graphics)</li> <li>眼部追踪 (Eye tracking)</li> <li>音效 (Audio)</li> <li>交互 (Interaction)</li> <li>人体工学 (Ergonomics)</li> <li>计算机视觉 (Computer Vision)</li> </ol> <h3 id="光学和显示-optics-and-displays">光学和显示 (Optics and displays)</h3> <p>关于光学显示这部分,Abrash 对比了当下的设备参数和人眼的区别。可以看到,与人眼相比,目前的技术在各个指标上均相去甚远。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-07.png" width="1159" height="599" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-07_hu7d2a565bca45968165a0bfc7c385ba8a_193293_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-07_hu7d2a565bca45968165a0bfc7c385ba8a_193293_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-07" class="gallery-image" data-flex-grow="193" data-flex-basis="464px" ></p> <p>紧接着给出了他的五年预测。可以看到,除了像素密度和 FOV 的增大以外,<a class="link" href="https://en.wikipedia.org/wiki/Depth_of_focus" target="_blank" rel="noopener" >可变焦深 (Variable depth of focus)</a> 的实现值得注意。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-08.png" width="1120" height="593" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-08_hue875957279e3094d1d1bfebd19a5497c_196289_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-08_hue875957279e3094d1d1bfebd19a5497c_196289_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-08" class="gallery-image" data-flex-grow="188" data-flex-basis="453px" ></p> <p>随着工艺的提高,可以肯定的是整体的像素数量会稳步增长。那么在给定的分辨率下,是更大的可视范围 (FOV) 重要,还是更细密的像素密度重要呢?这个问题取决于可视范围的增长是否能带来更好的体验,而 Abrash 在这里的答复是肯定的,他认为五年内可视角度将从 90 度增长到 140 度,而像素密度也将从目前的 30 像素/度翻倍至 120 像素/度。由于目前的光学技术在 FOV 超过 100 度时会失真,需要引入新的技术来实现 140 度的目标。</p> <p>关于最后一项可变焦深,目前设备的固定焦深是在人眼前两米左右,如果可变的话能显著地提高真实度。“Anything that makes virtual viewing more like the real world will increase comfort and the ability to stay in VR for long periods.” (任何能让虚拟现实变得更真实的技术都能显著地改善舒适度并延长人们停留在 VR 里的时间) 这里 Abrash 提到了全息显示/光场显示/多焦点显示/可变焦显示等技术方案,但目前对头戴式设备来说,这些技术暂时都还不够成熟,需要进一步的研究。Abrash 认为以五年的区间来看,这个问题是有望解决的。</p> <h3 id="图形-graphics">图形 (Graphics)</h3> <p>4K*4K 的分辨率意味着需要以 90 帧的速度为每只眼睛渲染 16M 的像素。然而这些像素里的大部分实际上是浪费的。视网膜的中央凹 (fovea) 的直径占据整个视网膜直径的十分之一不到,但却是人眼最高的分辨率的区域,在这个区域之外分辨率急剧降低(见下图)。也就是说,人眼焦点之外的次要区域,绝大部分是低分辨率的较模糊影像。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-09.png" width="1064" height="522" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-09_hueee75018a9bac06382d1f1b0a7eeb449_222424_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-09_hueee75018a9bac06382d1f1b0a7eeb449_222424_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-09" class="gallery-image" data-flex-grow="203" data-flex-basis="489px" ></p> <p>解决之道在于 foveated rendering (中文似可称为“凹式渲染”)。 就是如下图那样,仅以较高的分辨率渲染视线焦点区域。其他地方可适当降低分辨率,这样也通过减少像素数量来顺便降低了GPU负担。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-10.jpg" width="1067" height="629" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-10_hu967aaf47eab32e47135c58cae21983a4_65790_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-10_hu967aaf47eab32e47135c58cae21983a4_65790_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="abrash-10" class="gallery-image" data-flex-grow="169" data-flex-basis="407px" ></p> <p>注意,这跟 nVidia <a class="link" href="https://github.com/NvPhysX/UnrealEngine/tree/MultiRes-4.13" target="_blank" rel="noopener" >集成到 Unreal 4.1x 里的 Multi-Res Shading</a> 是不同的。MRS 利用的是光学矫正后分辨率不均匀的原理,在四周拉伸的区域使用较低分辨率,中央区域保持原分辨率。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/mrs01.jpg" width="1200" height="675" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/mrs01_hu06fab2b1b007d99c2a6130764f1e046a_98280_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/mrs01_hu06fab2b1b007d99c2a6130764f1e046a_98280_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="mrs01" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>MRS 的优势是,在 nVidia 的 Maxwell 级以上的 GPU 是硬件支持一次性渲染多个缩放的视口的 (multiple scaled viewports in a single pass),而缺点是静态分辨率,无法随着视线转移做动态的改变。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/mrs02.jpg" width="490" height="329" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/mrs02_hu8d037c4d8e7c8f65b8101af6b3f15ba7_37727_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/mrs02_hu8d037c4d8e7c8f65b8101af6b3f15ba7_37727_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="mrs02" class="gallery-image" data-flex-grow="148" data-flex-basis="357px" ></p> <p>另一方面,凹式渲染本身也存在着一些需要解决的问题:其中,一部分工作来自于对传统渲染流程的改造,甚至可能需要完全的重新设计;另一部分则来自于对完美眼部追踪的需求,这很好理解,只有完全精确地知道视线的位置,才能动态地处理对应的区域。关于眼部追踪的讨论见下一节。</p> <h3 id="眼部追踪-eye-tracking">眼部追踪 (Eye tracking)</h3> <p>在两年前的第一届 Oculus Connect 上,Abrash 已经强调了眼部追踪在将来的 VR 中的重要角色,但目前这项技术的发展比当时的预计更加困难。可能你会觉得,在一个如此有限的范围内追踪单个突出的目标能有多难呢,一开始 Oculus Research 也低估了这项任务的难度,觉得只是工作量的问题。后来发现对于凹式渲染这类对<strong>实时性和精确性</strong>要求极高的需求来说,目前的眼部追踪还差得很远。因为一旦任何一帧没有及时响应或存在计算偏差的话,都会造成很糟糕的渲染结果和用户体验。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-11.png" width="1212" height="649" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-11_hu3f39d9735212b3c4254794a6f22295a8_406004_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-11_hu3f39d9735212b3c4254794a6f22295a8_406004_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-11" class="gallery-image" data-flex-grow="186" data-flex-basis="448px" ></p> <p>可靠的眼部跟踪需要考虑很多因素,其中最主要的是完整的眼部运动范围和所有的人种差异性。人的瞳孔变化幅度会很大,尺寸和形状都会随时改变,而且经常会两边不一致,同时也需要考虑眼睑,眼球突出幅度,激光矫正手术的影响和干扰,眼球本身在运动时的形变,以及在设备的狭小空间内追踪完整眼部运动的约束。</p> <p>眼下有不少潜在的突破,都依赖于高度精确的眼部追踪实现。用 Abrash 的话来说,“Eye tracking is so central to the future of VR”。虽然他认为这是五年内可以解决的问题,但他同时也承认这是他所有预测中最大的单一风险 (&ldquo;Greatest single risk factor for my predictions&rdquo;)。</p> <h3 id="音效-audio">音效 (Audio)</h3> <p>对音效的发展 Abrash 显得比较乐观,五年之内应可做到简单快速地生成个人化的 head-related transfer function (HRTF) 对于声音的反弹,叠加,干涉,以及对声源方向和距离的判断的描述,详见<a class="link" href="http://gulu-dev.com/post/2015-10-18-oculus-connect-2-michael-abrash-keynote" target="_blank" rel="noopener" >去年演讲的对应内容</a>。好的音效模拟可以在参与者没有意识到的情况下改善虚拟空间内的整体体验。与去年强调的观点类似,由于运算量的限制,将只能处理有限的声源和接收者的声音传播。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-12.png" width="1128" height="629" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-12_hub429c72942760614af618fce2f958ed8_344938_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-12_hub429c72942760614af618fce2f958ed8_344938_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-12" class="gallery-image" data-flex-grow="179" data-flex-basis="430px" ></p> <h3 id="交互-interaction">交互 (Interaction)</h3> <p>Abrash 认为 Touch 将会扮演 VR 时代鼠标的角色,在很长时间内都会如此。而双手的追踪,重建和渲染对虚拟环境下的社交也很重要,虚拟人物如果有了精确的手部运动,就会大大增强表现力和感染力。空出的双手也可以用手势来操作简单的界面 (就像通常的科幻电影里那样),比如看电影的时候控制进度,或者在虚拟键盘上打字,等等。这些在五年内也有望被基本实现。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-13.png" width="1173" height="608" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-13_hu4b3846ae006e96f2e5d3231cee461b05_334092_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-13_hu4b3846ae006e96f2e5d3231cee461b05_334092_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-13" class="gallery-image" data-flex-grow="192" data-flex-basis="463px" ></p> <h3 id="人体工学-ergonomics">人体工学 (Ergonomics)</h3> <p>如果能不用头戴任何设备地在全息甲板上走来走去当然是最好的,不过看起来这在五年之内不太会发生。但设备本身将会变得更加小巧和轻便,而其中最大的挑战在于无线化。这样玩家可以在持续访问高性能 PC 的同时,自在地在房间中走动。这里的主要问题是显示带宽的限制。对于 4K*4K 的分辨率,通过凹式渲染来降低对带宽的需求看起来是一条可行的方案。</p> <h3 id="计算机视觉-computer-vision">计算机视觉 (Computer Vision)</h3> <p>除此之外,真实环境的融入也将会非常有价值。有在 VR 中重建的 (部分) 真实世界作为参考时,参与者的行动能够更安全,也能更好地感知和处理真实世界里发生的事件,如有人走进房间,端起水杯喝水,等等。Abrash 称这一类情境为混合现实 (mixed reality),或增强 VR (augumented VR)。五年之内,随着工程问题的逐步解决,VR 与现实之间的界限将会逐步模糊。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-14.jpg" width="1099" height="636" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-14_huab41cc4b143fef0d7af39ab544db0ff4_98393_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-14_huab41cc4b143fef0d7af39ab544db0ff4_98393_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="abrash-14" class="gallery-image" data-flex-grow="172" data-flex-basis="414px" ></p> <p>注意 Augmented VR 和 AR 的不同在于,前者在虚拟空间内重建了真实场景的完整模型,并可以通过简单交互来修改重建出的模型的尺寸,材质,方位等等。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-15.jpg" width="1170" height="650" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-15_hua62bb7d91b6077ad34ae6959e79ce291_119965_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-15_hua62bb7d91b6077ad34ae6959e79ce291_119965_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="abrash-15" class="gallery-image" data-flex-grow="180" data-flex-basis="432px" ></p> <p>然而,人类本身的重建是其中最复杂的部分,虽然现在在各种传感器和摄像机的帮助下,已经有了很好的手部追踪,但面部的细微表情捕捉和重现仍是一个待解决的问题。五年之内,可以期待具有基本社交属性和娱乐属性的虚拟角色 (virtual avatar),但较真人而言,虚拟角色的拟真程度一时还无法相提并论。</p> <h2 id="虚拟工作间">虚拟工作间</h2> <p>Abrash 在最后进一步充实和展望了他去年曾说起的虚拟工作间,这实际上是前面各项技术的汇总应用(尤其是实时的 HRTF 语音和基于计算机视觉的重建)。注意大屏幕上左下角重建出来的拿着真实咖啡杯的左手。</p> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-16.jpg" width="1293" height="668" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-16_hu612b10af30c48ac53b6457ba86465c55_65428_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-16_hu612b10af30c48ac53b6457ba86465c55_65428_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="abrash-16" class="gallery-image" data-flex-grow="193" data-flex-basis="464px" ></p> <h2 id="结语">结语</h2> <p>与前两届 Connect 大会上 Abrash 的发言相比,本次的内容更趋于细致化和具体化。这一点,从 Abrash 的结语也可以看出来:</p> <blockquote> <p>“The way technological revolutions actually happen involves smart people working hard <strong>on the right problems at the right time</strong>.”</p> </blockquote> <p><img src="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-17.png" width="1217" height="626" srcset="https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-17_hu1d705e358039e5003430aaf62beaed68_225349_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash/images/abrash-17_hu1d705e358039e5003430aaf62beaed68_225349_1024x0_resize_box_3.png 1024w" loading="lazy" alt="abrash-17" class="gallery-image" data-flex-grow="194" data-flex-basis="466px" ></p> <p>那么哪些是 right problems ?</p> <p>我们一个一个来看,</p> <p>首先,去年曾完整阐述的感知系统——视觉,听觉,触觉,嗅觉和味觉,再加上 (用于感知速度,加速度,空间位置和控制平衡) 的前庭系统 (vestibular)——里曾一笔带过的视觉 (Vision) 一节,在今年被展开成最重要的三个分类——光学和显示 (Optics and displays)、图形 (Graphics) 和计算机视觉 (Computer Vision) ——并分别定义了明确的目标。</p> <p>其次,完备的眼部追踪作为&quot;最大的单一风险&quot; (&ldquo;Greatest single risk factor for my predictions&rdquo;) 成为横在诸多潜在突破点面前的障碍。这一挑战原本被认为只是工作量的问题,现在已被证实比预期的更为艰巨。而从实践角度出发,不那么完全精确的追踪虽然保证不了完美的体验,但只要交互设计者不要把精确的定位作为交互的核心,就可以一定程度上缓解这个问题。就好像是我们从PC上鼠标的精确定位,慢慢过渡到移动平台上依赖手指的粗略定位,而并没有感受到太过不适那样,交互语言本身也会进化来适应眼部的运动特性。另一方面,所谓的凹式渲染 (foveated rendering) 也可以不用一上来就把自己定位到&quot;与视网膜的密度分布完全匹配&quot; 这样一个目标,在我看来第一阶段只要能做到识别大致的视线方向 (line of sight) 并把明显在该热点区域之外的部分低分辨率化,已经能省很大量的像素渲染量了。热点区域可大可小,可依据当时的眼部追踪精度而定,甚至可以在眼部运动剧烈不易判断时放大,而在静止容易判断时缩小 (就如同 GTA 等游戏里的小地图,当你运动速度快时能看到更大范围的小地图,而速度降下来时小地图也随之拉近)。</p> <p>最后,我们注意到 Abrash 明显注意到去年涌现出来的 AR 和 VR 之间的诸多形态 (即所谓的 Mixed Reality / Augmented VR 等等)。在此前一直被孤立对待的重建 (Reality Reconstruction) 方面的研究,现在看起来优先级提高了,并有了针对社交情境和工作情境这两个重点场景的比较完整的思考。行文至此,我们很自然地发觉,能把 Abrash 和 Carmack 邀请到一起工作,对 Oculus 而言是非常幸运的事——Abrash 偏社交和工作交流,Carmack 偏游戏和娱乐交互;Abrash 强于理论,Carmack 重在实践;Abrash 关注面向未来的基础设施,Carmack 执着于把当下已有的技术做到极致。他们在一起形成了从工作到游戏,从科学理论到工程实践,从现在到未来的完美互补。</p> <p>很多人说 2016 年是 VR 元年,但我们深深地明白,随着消费级产品的发布,漫漫征途才刚刚开始。还记得初代 iPhone 发布时的情景吗?初代 Android 呢?在这场刚刚拉开大幕的华丽演出中,你愿意成为座椅上的观众,还是舞台上的演员?</p> <p>我们屏息以待。</p> <p>[注]</p> <ul> <li>[2016-11-20 00:38] 初稿</li> <li>[2016-11-24 09:49] 修订</li> <li>[2016-12-18 06:26] 发布到 <a class="link" href="http://gulu-dev.com/post/2016-12-18-oculus-connect-3-abrash" target="_blank" rel="noopener" >Blog</a> 和<a class="link" href="https://zhuanlan.zhihu.com/p/24427320?refer=gu-lu" target="_blank" rel="noopener" >知乎专栏</a></li> </ul> 2016.12 无压应对信息过载 - 2016 效率小结 https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/ Thu, 08 Dec 2016 06:08:00 +0000 https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/info-overload-title.png" alt="Featured image of post 2016.12 无压应对信息过载 - 2016 效率小结" /><h2 id="tldr-too-long-didnt-read">tl;dr (&ldquo;Too long; didn’t read.&rdquo;)</h2> <p>信息过载是我们每天都要面对的问题。有趣的是,随着信息鉴别,过滤,处理能力的提高,我们往往不自觉地会延展自己的信息渠道(新 app、新 feed 和新的公众号,等等),接着让自己陷入更大的信息漩涡——明明是效率提高了,吞吐量增大了,为什么好像每个人都更加不堪重负了?</p> <blockquote> <p><strong>It&rsquo;s not information overload. It&rsquo;s filter failure.</strong></p> <ul> <li>Clay Shirky, excerpted from <a class="link" href="https://en.wikipedia.org/wiki/Information_overload" target="_blank" rel="noopener" >Information overload (Wikipedia)</a></li> </ul> </blockquote> <p>在即将过去的 2016 年,我形成了一些小习惯,帮助自己更好地应对信息过载的难题。整理一下,分享给有同样困扰的同学。本文中,我会列出一些自己<strong>觉得好用的小技巧</strong>,然后是作为容器的<strong>信息库的分拣标准</strong>,最后谈一下我是如何用<strong>最低的时间成本</strong>去维护这套体系的。</p> <hr> <blockquote> <p><strong>tl;dr</strong> - (chiefly Internet) &ldquo;Too long; didn’t read.&rdquo; Used to indicate that one did not read a (long) text, or to introduce a short summary of an overly long text.</p> <ul> <li>Excerpted from <a class="link" href="https://en.wiktionary.org/wiki/tl;dr" target="_blank" rel="noopener" >tl;dr (Wikipedia)</a></li> </ul> </blockquote> <h2 id="小技巧列表-5个">小技巧列表 (5个)</h2> <h3 id="技巧110-秒处置">技巧1:10 秒处置</h3> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/10-secs.jpg" width="300" height="400" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/10-secs_hufd0b73b895197c5e2e1531e69c54f671_24830_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/10-secs_hufd0b73b895197c5e2e1531e69c54f671_24830_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="10-secs" class="gallery-image" data-flex-grow="75" data-flex-basis="180px" ></p> <p>当你处于碎片阅读的状态(随便读读)时,即使遇到再有价值的文章(最新的例子是 Milo 老师的<a class="link" href="https://zhuanlan.zhihu.com/p/24207171" target="_blank" rel="noopener" >游戏程序员的学习之路</a>——真不想放这个链接,因为精彩到很多人点进去估计就忘了回来读本文了 Orz..),再有趣的故事,再不能错过的视频,也要能克制一直读完的冲动——除非它短到 30 秒以内就能完成。取而代之的是,为它建一个后续的条目,在合适的时候再决定是不是真的要读下去。</p> <p>这样做的最大好处在于——能让你在最短时间内意识到,<strong>很多不知不觉读完的材料其实是不值得读的</strong>。要学会随时用你的 inner voice 对自己喊:“<strong>Cut</strong>!!”。况且,**“新建一个条目”**这个额外的动作本身是有3秒钟时间成本的,这本身就是一道坎,如果连这额外3秒钟我们都要琢磨一下,那这材料很有可能不值得继续读下去。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/cut.jpg" width="320" height="320" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/cut_hua7592199fbc30c2546f644dbdd928a29_14888_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/cut_hua7592199fbc30c2546f644dbdd928a29_14888_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="cut" class="gallery-image" data-flex-grow="100" data-flex-basis="240px" ></p> <p>既然你是在碎片阅读,那就应该真正做到<strong>以碎片化的方式去阅读</strong>。如果总是由着兴趣敞开了读,那么你的阅读过程最有可能变成这样:</p> <ul> <li>短&ndash;短&ndash;**长&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;**短&ndash;**长&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&ndash;**短&ndash;</li> </ul> <p>而事后看,中间的长文未必总是值得读的,那么时间就在不知不觉中浪费了。</p> <p>像上面那样的短长交错的模式,其实还算是好的,如果你真的遇到一个精彩的长文,更有可能变成这样:</p> <ul> <li>短&ndash;短&ndash;<strong>长&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;&mdash;</strong>(<strong>被打断/未读完</strong>)</li> </ul> <p>这个偶遇的长文将占据你余下的所有碎片时间,而且你几乎肯定不会读完——通常是在外部环境变化时(如车来了)才发现自己兴致勃勃地读到一半——匆匆地收藏一下,就关上手机结束了这次阅读。这样,你实际上花了不少额外时间(假设是正常读完全文所需的一半时间)来做了一个收藏的决定。</p> <p>即使日后仍有幸记得回来打开收藏把它读完,还是要花时间去补回脑海中的上下文,这样你读一篇文章的总时间开销将是你正常阅读的 1.5 倍;而倘若你不再记得回来续读,或者回来时又觉得“这篇文章似乎不如刚读到时那么有趣了”,决定不读了,那前面那一半你就白读了。</p> <p>所以你看,这么做是很低效的——不管你日后读还是不读,<strong>当下的时间</strong>都被浪费掉了。</p> <hr> <p>在碎片阅读时,试着让自己习惯于喊 “Cut”——即使你遇到的材料再精彩,也尽量保持克制——能让你的阅读过程变成下面这样:</p> <ul> <li>短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;短&ndash;</li> </ul> <p>当这样的节奏形成习惯以后,能让你更有效地利用碎片时间——任何时间点上结束都不会有“没读完”的心理负担,这才是真正的“碎片化阅读”。</p> <hr> <p>“10 秒处置” 发生于接触信息的第一时间点,是我个人<strong>在信息处置方面的最佳实践</strong>。如能善加运用,“10 秒处置”这个简单的原则就能成为<strong>个人信息边界</strong>的第一道防线,可以帮你<strong>以最小的时间代价抵御最大量的信息攻击</strong>。所以作为性价比最高的技巧,这条原则被我排在第一个讲,希望能对阅读此文的你有所助益。</p> <p>如果你的时间非常宝贵,读到这里就打算结束阅读本文,那么可以说你已经获得了本文最有价值的一条小技巧,可以安心地关闭页面离开了。</p> <hr> <h3 id="技巧2把-read-it-later-改为-read-it-at-2045">技巧2:把 “read it later” 改为 “read it at 20:45”</h3> <p>当你在 10 秒处置时决定认可一篇文章的价值,打算把它收藏起来以便日后认真阅读时,是直接点一下“收藏”就完事了吗?</p> <p>以前我就是这么做的。但后来我发现,所有那些收藏起来以便日后再读的文章,十有八九都石沉大海了。新的信息前仆后继地潮水般扑面涌来,永无休止地反复堆砌在老旧信息之上。层层碾压之下,老的收藏几乎无法获得被再次认真阅读的机会。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/reading-list.jpg" width="372" height="482" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/reading-list_hu56bd0c17725e657b4bb81e8b26cbd46d_31642_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/reading-list_hu56bd0c17725e657b4bb81e8b26cbd46d_31642_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="reading-list" class="gallery-image" data-flex-grow="77" data-flex-basis="185px" ></p> <p>正如@顾煜老师说到的那样,<a class="link" href="https://zhuanlan.zhihu.com/p/20824083" target="_blank" rel="noopener" >Read it later 变成了 Read it never</a>。</p> <hr> <p>意识到这一点之后,我在 2016 年养成的第二个习惯是——任何时候触发“收藏”或类似的动作(如加入阅读列表),都明确地为这个收藏打一个目标阅读时间的标签(&quot;<code>read-time</code>&quot; tag),这个标签可以是:</p> <ul> <li>明确的目标时间点,如:20:45 today</li> <li>明确的目标时间段,如:this week / this month / this year</li> <li>不明确(不明确本身也是一种明确),如:someday / after xxx is done / pending</li> </ul> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/tags.png" width="627" height="239" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/tags_hub17b8da4f4a3b8bfdc58fc2cd359ba62_12897_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/tags_hub17b8da4f4a3b8bfdc58fc2cd359ba62_12897_1024x0_resize_box_3.png 1024w" loading="lazy" alt="tags" class="gallery-image" data-flex-grow="262" data-flex-basis="629px" ></p> <p>这样做的好处是——你随时可以通过标签查找,知道自己接下来的指定时间段里阅读任务重不重,然后重估和调整自己的阅读量。当你真的有整块的时间静心阅读时,可以<strong>立即开始</strong>阅读这个时间点上的存货,而不是在收藏夹里东翻翻西看看,把时间和心智浪费在反复挑选适合阅读的材料上。</p> <hr> <p>我常用的 Pocket 阅读列表,微信收藏和印象笔记·剪藏,都支持“为新增条目指定任意数目个标签”。当然标签还有许多其它的用途(比如打上阅读这段材料大约会占据的时间,如“10min”, “1h” 等等),这里跟本条目无关,就不多说了。</p> <p>更优秀的阅读列表(比如高级版 Todoist)允许你定义一条提醒,到时间了提醒你读,并直接把文章链接或内容推送到通知栏。如果主动取消或没有及时阅读,它能自动把过期的条目聚合到一起 (overdue),让你知道自己的估算有误,下次就可以估得更准确。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/overdue.png" width="638" height="268" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/overdue_hu3b0cb7934e4c569909cb64d46a3d26f7_13523_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/overdue_hu3b0cb7934e4c569909cb64d46a3d26f7_13523_1024x0_resize_box_3.png 1024w" loading="lazy" alt="overdue" class="gallery-image" data-flex-grow="238" data-flex-basis="571px" ></p> <p>呃,好吧,截完图才发现,此文本来计划两天前(6 Dec)就完成的——一篇谈效率的文章本身延期了——真是给自己跪了——囧rz&hellip;</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/r-u-kidding.jpg" width="220" height="220" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/r-u-kidding_hub729530299518d63c53ebcdeb30e79ea_10444_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/r-u-kidding_hub729530299518d63c53ebcdeb30e79ea_10444_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="r-u-kidding" class="gallery-image" data-flex-grow="100" data-flex-basis="240px" ></p> <p>“定义明确的时间点”这个动作同样有助于你把那些可读可不读的材料拒之门外,比如 “read it someday” 通常意味着“虽然觉得有点用,可还是先一边待着去吧”,将其物理隔离于你的日常待选列表之外——直到因为某个新情况的发生你提高了它的优先级——或(更多情况下)一直得不到宠幸,晚些时候被清理掉。</p> <h3 id="技巧3数目限制以免超量">技巧3:数目限制以免超量</h3> <p>刚开始打标签的时候很容易滥用 <code>today</code>。我自己最开始经常的情况是,一天下来攒了十几二十条标记 <code>today</code>,一晚上连一半都读不完,完全超出了自己的可行阅读量。只能全部 push to tomorrow,结果日复一日地滚雪球,标签就白打了。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/tomorrow.png" width="658" height="314" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/tomorrow_hu332a388092916291720103d2e777b2e8_20348_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/tomorrow_hu332a388092916291720103d2e777b2e8_20348_1024x0_resize_box_3.png 1024w" loading="lazy" alt="tomorrow" class="gallery-image" data-flex-grow="209" data-flex-basis="502px" ></p> <p>看,过期事项只要轻轻点一下(有段时间是我点得最多最勤的按钮了)就可以推给明天了,明日复明日,真是邪恶的功能啊。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">明日复明日,明日何其多。 </span></span><span class="line"><span class="cl">我生待明日,万事成蹉跎。 </span></span><span class="line"><span class="cl">世人若被明日累,春去秋来老将至。 </span></span><span class="line"><span class="cl">朝看水东流,暮看日西坠。 </span></span><span class="line"><span class="cl">百年明日能几何?请君听我明日歌。 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">- 明日歌, 明 ·钱鹤滩 </span></span></code></pre></td></tr></table> </div> </div><p>这当中最重要的是对自己的<strong>阅读强度</strong>,<strong>阅读材料量</strong>和<strong>用于阅读的时间量</strong>有正确的预估。跟健身一样,一开始就做准确的预估是比较难的,不妨把数量定得低一些——先定一个小目标——一天一篇深度阅读,有把握之后再逐渐增加。坚持四周习惯养成之后就不用刻意地算着排了,你的节奏感和潜意识在 “10 秒处置” 时往往就能自动帮你作出条件反射式的决定和安排。</p> <p>我目前通常是3-5篇,每篇平均十分钟左右,也就是一天下来精读的阅读量在半小时在一个小时之间。这样虽然远远比不上那些速读大神,但好处是没啥负担就可以完成,不至于排得太满增加无谓的压力。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/DailyGoals.jpg" width="475" height="249" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/DailyGoals_hu32831705420c36665f3b731abceb6073_29373_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/DailyGoals_hu32831705420c36665f3b731abceb6073_29373_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="DailyGoals" class="gallery-image" data-flex-grow="190" data-flex-basis="457px" ></p> <p>另外,排的时候我也会注意内容的分布,通常不会在某一天全部是同类,而是从浅到深各类的都会有一些,这样比较不会觉得疲劳。如果当天的状态很好可能会多排深入的材料,反之会更娱乐化和消遣一些。毕竟没必要把自己当成机器,不必强求定量的输入。</p> <h3 id="技巧4即刻奖励和迷你成就系统">技巧4:即刻奖励和迷你成就系统</h3> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/welldone.jpg" width="640" height="426" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/welldone_hu1fbd8dd0610b6744e3ab89fec0a6047b_42582_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/welldone_hu1fbd8dd0610b6744e3ab89fec0a6047b_42582_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="welldone" class="gallery-image" data-flex-grow="150" data-flex-basis="360px" ></p> <p><strong>给自己的鼓励和奖励</strong>并不像看起来那么幼稚。</p> <p>在百万年的进化之中,“行动-成功-收获-喜悦-再行动”和“行动-失败-放弃-沮丧-不再行动” 这样的即时反馈机制已经深深地镌刻在我们的基因之中——对于资源极度匮乏的漫长的原始人类时期,每个行动点上的即刻奖励往往就是生存本身,无法形成正向反馈循环的个体在漫长的进化过程中都会被淘汰。</p> <p>如果你和我一样喜欢玩游戏的话一定明白我在说什么,精心设计的游戏总是会制造持续的积极反馈来吸引你的注意力一直玩下去,小到超级玛丽里面不断吃到金币,大到魔兽世界的多人副本体验。而相对的,缺乏积极反馈的游戏过程很容易就被中断掉。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/reward.jpg" width="322" height="146" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/reward_hu43e256e4088ec25c09fc544ae828d7f9_17659_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/reward_hu43e256e4088ec25c09fc544ae828d7f9_17659_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="reward" class="gallery-image" data-flex-grow="220" data-flex-basis="529px" ></p> <p>即使通过培养超强的自律,信仰,使命感等自我驱动机制,使你脱离了任何即刻的反馈也可以持续地完成既定目标,即时奖励仍然有巨大的积极意义。它会向你自己释放认可和满足的信号,具体到接收信息这个任务里,完成既定的阅读目标后的奖励,让你可以随时提醒自己阶段性目标的实现,解除当下的心智负担。</p> <p>定义各种迷你的主题成就,可以赋予零碎的信息接收以完整的意义,在日后回顾时,也能很容易地<strong>找到来时的路</strong>。那些历史上纵横寰宇的伟人,自有无数仰慕者为其树碑立传——而对吾辈凡人而言,这些由自己定义的小小成就,即是自身每一段成长的点滴见证。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/grow.jpg" width="734" height="490" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/grow_hu67f4ca66540b7700a77d95754b9f60e1_101022_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/grow_hu67f4ca66540b7700a77d95754b9f60e1_101022_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="grow" class="gallery-image" data-flex-grow="149" data-flex-basis="359px" ></p> <h3 id="技巧5batch-reading-批量阅读分摊心智展开的开销">技巧5:batch reading (批量阅读)分摊心智展开的开销</h3> <p>在不太熟悉的领域,对于有深度的文章,我们往往需要起步阶段的思维铺垫和心智展开的过程。如果阅读时一味图快,往往只能懂个大概,无法真正地消化吸收,为己所用。在这种时候,把针对同一主题的不同角度的文章放到一起阅读,往往在互相印证,加深理解的同时,避免了多次展开的时间和心智开销。材料越多样,细节越丰富,越有助于我们的潜意识去自动地梳理和提炼。</p> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/parallel.png" width="500" height="223" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/parallel_hu17b5e6f5a38de1ed408e0fd043fa1c6e_13285_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/parallel_hu17b5e6f5a38de1ed408e0fd043fa1c6e_13285_1024x0_resize_box_3.png 1024w" loading="lazy" alt="parallel" class="gallery-image" data-flex-grow="224" data-flex-basis="538px" ></p> <p>从上图可以看出,针对同一主题的批量阅读往往比单一的阅读更有效率,对该主题获得的认识更深入。</p> <p>有时,我们会遇到自己很感兴趣,但是一看就知道当下还读不懂的文章。这就是很好的日后 batch 的对象。我一般会打个 <code>pending for batch</code> 的标签。攒了三五篇同一主题却深浅程度不一的文章之后,一鼓作气读下来,收获会很大——这可比“零打碎敲,读了不少却始终保持在一知半解的程度”要来得有效率多了。</p> <h2 id="我的信息库">我的信息库</h2> <p>日常的 GTD 加上以上的筛选技巧就是我的日常信息流水线,而流水线的产出就是我的信息储存体系,包含下面四个部分:</p> <ul> <li><strong>core value</strong> - 这部分是我能提供的核心价值所在,包括了我最擅长的技能及相关的领域经验和知识积累,信息质量和密度最高,深度优先,主要保存为有完全修订历史的 markdown 文档。</li> <li><strong>references</strong> - 这部分是对我而言是非核心的外部参考和引用,会对吸收的信息做一定的提炼,广度优先,主要保存于 Evernote 类信息采集软件。</li> <li><strong>stories</strong> - 各类演讲或公开课,各种故事,奇闻,都市传说,行业会议,各类政经文史哲材料,等等,以保证原材料的完整性为主要考虑,主要档案格式为 PDF。</li> <li><strong>resources</strong> - 各类资源工具文档的聚合,外部服务,等等,散见于书签,收藏夹及各类 app 里。</li> </ul> <p><img src="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/info_lib.png" width="504" height="370" srcset="https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/info_lib_hu8d557869e4004dcf5904a8b39fe34b54_88605_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-12-08-stress-free-dealing-with-info-overload/images/info_lib_hu8d557869e4004dcf5904a8b39fe34b54_88605_1024x0_resize_box_3.png 1024w" loading="lazy" alt="info_lib" class="gallery-image" data-flex-grow="136" data-flex-basis="326px" ></p> <h2 id="惰性整理法则维护用最低的时间成本">惰性整理法则:维护——用最低的时间成本</h2> <p>惰性整理法则:如果专门花时间整理,容易陷入为了整理而整理的泥潭,时间花很多但性价比低,当时感觉整理得很好,但大多数材料是有多维视角的,整理时为了效率往往只考虑了单一的角度。</p> <p>更好的做法就是<strong>打上时间戳,任其自然按照日期排列,不做任何整理</strong>。当新的材料出现的时候,只有我们很明显地觉察到几份材料的相关度足够强时,再把它们放在一起,让这些材料自发的聚合。这样不仅时间开销极低,也能辅助着把头脑当中的零碎知识点聚点成线,聚线成面,聚沙成塔。更重要的是,我们清楚地知道,每一次清理都能有效提高有序程度,以最小的代价降低系统的熵。</p> <p>适度的混乱加上惰性整理可以让我们做到 clean-on-demand,从反复整理的大块时间浪费的泥潭当中解脱出来。正如增量式垃圾回收每次只处理一小部分那样,惰性整理本质上是一种增量式的渐进整理。</p> <hr> <h2 id="小结">小结</h2> <p>对于看重成长的个体来说,个人效率是一个永恒的话题。摆脱<a class="link" href="https://www.zhihu.com/question/21785190" target="_blank" rel="noopener" >松鼠症</a>,绝非一日之功;消除长期积累的内心的焦虑和不安,更不是掌握几个小技巧就能做到,需要的更多的是根植内心的耐心和勇气。记此小文,聊以自勉。</p> 2016.11 Unity 游戏的 string interning 优化 https://gulu-dev.com/post/2016-11-22-unity-string-interning/ Tue, 22 Nov 2016 13:00:00 +0000 https://gulu-dev.com/post/2016-11-22-unity-string-interning/ <h1 id="unity-游戏的-string-interning-优化">Unity 游戏的 string interning 优化</h1> <h2 id="问题描述">问题描述</h2> <p>在开始之前,先说一下这个问题为什么很容易被忽视吧。</p> <p>通常情况下,我们难以注意到运行着的 Unity 程序内 string 的实例化情况。这些字符串的创建,销毁的时机是否合理,是否存在有重复 (相同内容的字符串),冗余 (存有已不再有意义的垃圾字符),低效 (capacity 远大于 length),以及泄漏 (没有在期望的时机及时销毁) 的情况就更容易被忽视了。由于 string 没法随时像普通的 Unity 对象那样通过调用 <code>Object.GetInstanceID()</code> 来查看实例id,我们不太容易感知字符串对象的实际内存开销。其实要不是偶然在工具里发现了大量的此类情况,俺也没想到看起来颇单纯的 immutable string 里居然隐藏着这么多秘密。</p> <p>一次只说一件事,这次我们只讨论重复字符串的问题。</p> <p>使用自制工具 <a class="link" href="https://github.com/PerfAssist/ResourceTracker" target="_blank" rel="noopener" >ResourceTracker</a>,可以发现 Unity 游戏运行时 mono(il2cpp) 内有大量重复的字符串,如下所示:</p> <p><img src="https://gulu-dev.com/post/2016-11-22-unity-string-interning/before_intern.png" width="784" height="649" srcset="https://gulu-dev.com/post/2016-11-22-unity-string-interning/before_intern_hu7ef2d5e9a80a20fca2fecf56c127cccb_61142_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-22-unity-string-interning/before_intern_hu7ef2d5e9a80a20fca2fecf56c127cccb_61142_1024x0_resize_box_3.png 1024w" loading="lazy" alt="before_intern" class="gallery-image" data-flex-grow="120" data-flex-basis="289px" ></p> <h2 id="手动-intern">手动 Intern()</h2> <p>对 .Net 特性有了解的同学,应该知道 C# 同 Java 一样,提供了一套内建的 string interning 机制,能够在后台维护一个字符串池,从而保证让同样内容的字符串始终复用同一个对象。这么做有两个好处,一个是节省了内存 (重复字符串越多,内存节省量越大),另一个好处是降低了字符串比较的开销 (如果两个字符串引用一致,就不用逐字符比较内容了)</p> <p>但是为什么上面的 Unity 程序内仍然有大量的重复字符串呢?</p> <p>查看他们的地址,发现彼此各不相同,说明的确没有引用到同一块内存区域。由于 C# 语言实现以静态的特性为主,俺推测,也许只有编译期可以捕捉到的字符串 (也就是通常用字面字符串 literal string 来构建时) 才会 interning。</p> <p>做个实验吧:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar</span> <span class="p">=</span> <span class="s">&#34;foobar&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar2</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StringBuilder</span><span class="p">().</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;foo&#34;</span><span class="p">).</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;bar&#34;</span><span class="p">).</span><span class="n">ToString</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">foobar</span> <span class="p">==</span> <span class="n">foobar2</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Object</span><span class="p">.</span><span class="n">ReferenceEquals</span><span class="p">(</span><span class="n">foobar</span><span class="p">,</span> <span class="n">foobar2</span><span class="p">));</span> </span></span></code></pre></td></tr></table> </div> </div><p>运行上面的代码,输出结果分别是 <code>True</code> 和 <code>False</code>。嗯,也就是说,即使运行时内容一样 (<code>==</code> 返回 <code>True</code>),手动在运行时拼出来的字符串也<strong>不会自动复用已有的对象</strong>。查看游戏代码,发现很多重复字符串是通过解析 binary stream 或 text stream 构造出来的,这样就解释得通了。(<a class="link" href="http://stackoverflow.com/questions/8692303/intern-string-literals-misunderstanding" target="_blank" rel="noopener" >String literals get interned automatically</a>)</p> <p>手动 Intern 一下试试吧。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar0</span> <span class="p">=</span> <span class="s">&#34;foobar&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar1</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StringBuilder</span><span class="p">().</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;foo&#34;</span><span class="p">).</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;bar&#34;</span><span class="p">).</span><span class="n">ToString</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar2</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Intern</span><span class="p">(</span><span class="n">foobar1</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar3</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StringBuilder</span><span class="p">().</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;f&#34;</span><span class="p">).</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;oo&#34;</span><span class="p">).</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;b&#34;</span><span class="p">).</span><span class="n">Append</span><span class="p">(</span><span class="s">&#34;ar&#34;</span><span class="p">).</span><span class="n">ToString</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">foobar4</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Intern</span><span class="p">(</span><span class="n">foobar3</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">foobar0</span> <span class="p">==</span> <span class="n">foobar1</span><span class="p">);</span> <span class="c1">// True</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">foobar0</span> <span class="p">==</span> <span class="n">foobar2</span><span class="p">);</span> <span class="c1">// True</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">foobar0</span> <span class="p">==</span> <span class="n">foobar3</span><span class="p">);</span> <span class="c1">// True</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">foobar0</span> <span class="p">==</span> <span class="n">foobar4</span><span class="p">);</span> <span class="c1">// True</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Object</span><span class="p">.</span><span class="n">ReferenceEquals</span><span class="p">(</span><span class="n">foobar0</span><span class="p">,</span> <span class="n">foobar1</span><span class="p">));</span> <span class="c1">// False</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Object</span><span class="p">.</span><span class="n">ReferenceEquals</span><span class="p">(</span><span class="n">foobar0</span><span class="p">,</span> <span class="n">foobar2</span><span class="p">));</span> <span class="c1">// True</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Object</span><span class="p">.</span><span class="n">ReferenceEquals</span><span class="p">(</span><span class="n">foobar0</span><span class="p">,</span> <span class="n">foobar3</span><span class="p">));</span> <span class="c1">// False</span> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">Log</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Object</span><span class="p">.</span><span class="n">ReferenceEquals</span><span class="p">(</span><span class="n">foobar0</span><span class="p">,</span> <span class="n">foobar4</span><span class="p">));</span> <span class="c1">// True</span> </span></span></code></pre></td></tr></table> </div> </div><p>注意,C# 并没有提供“清除已经 Intern 的字符串”的接口。也就是说,如果不由分说地把产生的字符串都扔进去,会造成大量短生命期字符串 (如某个地图上特有的特效名) 在全局池内的堆积。</p> <p>解决这个问题并不难,手写一个可清除的版本就可以了。</p> <h2 id="可清除的-interning---uniquestring">可清除的 Interning - UniqueString</h2> <p>下面的 <code>UniqueString</code> 类除了提供两个与 <code>string.Intern()</code> 和 <code>string.IsInterned()</code> 一致的接口外,还提供了 <code>Clear()</code> 接口用于周期性地释放整个字符串池,可在地图切换等时机调用。这个类通过判断参数来确认,是将字符串放入全局的系统池,还是支持周期性清理的用户池。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">UniqueString</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// &#39;removable = false&#39; means the string would be added to the global string pool</span> </span></span><span class="line"><span class="cl"> <span class="c1">// which would stay in memory in the rest of the whole execution period.</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">string</span> <span class="n">Intern</span><span class="p">(</span><span class="kt">string</span> <span class="n">str</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">removable</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Why return a ref rather than a bool? </span> </span></span><span class="line"><span class="cl"> <span class="c1">// return-val is the ref to the unique interned one, which should be tested against `null`</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">string</span> <span class="n">IsInterned</span><span class="p">(</span><span class="kt">string</span> <span class="n">str</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// should be called on a regular basis</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">Clear</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>通过参数 <code>removable</code> 我们可以指定使用默认 intern 还是 removable-intern。显式地指定后者的字符串将可被随后的 <code>UniqueString.Clear()</code> 清理。</p> <p><code>UniqueString</code> 的实现 (及更新) 在<a class="link" href="https://github.com/PerfAssist/PA_Common/blob/master/Scripts/UniqueString.cs" target="_blank" rel="noopener" >这里</a>。</p> <h2 id="效果">效果</h2> <p>使用上面的机制在关键点加了几行代码简单地优化后,内存中的字符串从 88000 条降低到 34000 条左右 (仍有很多重复存在)。</p> <p><img src="https://gulu-dev.com/post/2016-11-22-unity-string-interning/after_intern.png" width="773" height="413" srcset="https://gulu-dev.com/post/2016-11-22-unity-string-interning/after_intern_hu4f09a5695d2d482f17643dd5bc811947_37761_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-22-unity-string-interning/after_intern_hu4f09a5695d2d482f17643dd5bc811947_37761_1024x0_resize_box_3.png 1024w" loading="lazy" alt="after_intern" class="gallery-image" data-flex-grow="187" data-flex-basis="449px" ></p> <h2 id="小结">小结</h2> <ol> <li>直接写在代码里的常量字符串 (即所谓的 literal string) 会在启动时被系统自动 Intern 到系统字符串池;而通过拼接,解析,转换等方式在运行时动态产生的字符串则不会。</li> <li>避免在 C# 代码里写多行的巨型 literal string,避免无谓的内存浪费。常见的情况是很大的 Lua 代码块,很密集的生成路径,大块 xml/json 等等,见下面的例子。</li> <li>已经被自动或手动 Intern 的字符串在之后的整个生命期中常驻内存无法移除,但可以使用上面提供的 <code>UniqueString</code> 类实现周期性的清理。</li> </ol> <p>下面是一些<strong>不合理的</strong>常见的代码内的常量字符串的情况 (都是常驻内存无法释放的)</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">query</span> <span class="p">=</span> <span class="s">@&#34;SELECT foo, bar </span></span></span><span class="line"><span class="cl"><span class="s"> FROM table </span></span></span><span class="line"><span class="cl"><span class="s"> WHERE id = 42&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">lua_code_block</span> <span class="p">=</span> <span class="s">@&#34; </span></span></span><span class="line"><span class="cl"><span class="s"> local ns = foo.bar(self.nID) </span></span></span><span class="line"><span class="cl"><span class="s"> for i,v in ipairs(self.imgs) do </span></span></span><span class="line"><span class="cl"><span class="s"> if (i - 1) &lt; ns then </span></span></span><span class="line"><span class="cl"><span class="s"> Obj.SetActive(self.imgs[i], true) </span></span></span><span class="line"><span class="cl"><span class="s"> else </span></span></span><span class="line"><span class="cl"><span class="s"> Obj.SetActive(self.imgs[i], false) </span></span></span><span class="line"><span class="cl"><span class="s"> end </span></span></span><span class="line"><span class="cl"><span class="s"> end </span></span></span><span class="line"><span class="cl"><span class="s">&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kt">string</span><span class="p">[]</span> <span class="n">resFiles</span> <span class="p">=</span> <span class="k">new</span> <span class="kt">string</span><span class="p">[]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;Assets/Scenes/scene_01.unity&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;Assets/Scenes/scene_02.unity&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;Assets/Scenes/scene_03.unity&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;Assets/Scenes/scene_04.unity&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;Assets/Scenes/scene_05.unity&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span></code></pre></td></tr></table> </div> </div><p>附:</p> <ul> <li><a class="link" href="http://broadcast.oreilly.com/2010/08/understanding-c-stringintern-m.html" target="_blank" rel="noopener" >Understanding C#: String.Intern makes strings interesting</a> 是很好的材料,对弄清楚 intern 的一些 dirty 细节非常有帮助</li> </ul> 2016.11 秒打时间戳 (日常生活中有哪些十分钟就能学会并可以终生受用的技能?) https://gulu-dev.com/post/2016-11-07-timestamp/ Mon, 07 Nov 2016 06:30:00 +0000 https://gulu-dev.com/post/2016-11-07-timestamp/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-11-07-timestamp/timestamp.png" alt="Featured image of post 2016.11 秒打时间戳 (日常生活中有哪些十分钟就能学会并可以终生受用的技能?)" /><p><em>题图引自 <a class="link" href="https://www.timestamp.io/" target="_blank" rel="noopener" >timestamp.io</a></em></p> <hr> <p>上周在知乎上答了一题:</p> <ul> <li><a class="link" href="https://www.zhihu.com/question/20894671/answer/129254002" target="_blank" rel="noopener" >日常生活中有哪些十分钟就能学会并可以终生受用的技能?</a></li> </ul> <hr> <p>说一个微不足道只要 1 分钟就能 get 的小玩意,但是没想到真正改变了我的工作习惯,每天都要用到数十次。</p> <p>我有一个开机启动的 AutoHotkey 脚本,里面设置了一些常用的环境配置,这样每到新的机器上直接把这个文件拖过来双击一下就是自己熟悉的环境了。</p> <p>这个脚本里其他的就不说了,下面三个快捷输入的使用频率最高:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> ::[d:: </span></span><span class="line"><span class="cl"> FormatTime, DateString, , [yyyy-MM-dd] </span></span><span class="line"><span class="cl"> SendInput %DateString% </span></span><span class="line"><span class="cl"> return </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> ::[t:: </span></span><span class="line"><span class="cl"> FormatTime, TimeString, , [yyyy-MM-dd HH:mm] </span></span><span class="line"><span class="cl"> SendInput %TimeString% </span></span><span class="line"><span class="cl"> return </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> ::[s:: </span></span><span class="line"><span class="cl"> FormatTime, TimeString, , [HH:mm] </span></span><span class="line"><span class="cl"> SendInput %TimeString% </span></span><span class="line"><span class="cl"> return </span></span></code></pre></td></tr></table> </div> </div><p>它们的作用分别是:</p> <ul> <li>当输入 <code>[d</code> 时回车,能够自动展开成当天的日期 [2016-10-28]</li> <li><code>[t</code> 和 <code>[s</code> 分别展开成 [2016-10-28 09:06] 和 [09:06]</li> <li>d, t, s 分别是 data/time/short 的首字母,当然也可以是别的字母</li> </ul> <p>好了,到这里就具备<strong>随时随地对你经手的任意材料任意信息秒打时间戳</strong>的能力了。虽然刚开始这么做的时候一点也没意识到,但现在回过头来看,这个能力实在地改变了我对时间的感知,作用堪比第一次戴上手表——“拥有了随时随地查看确切时间的能力”。</p> <p>很难一下说明白这种能力有啥帮助——毕竟很多同学会说,很多工具都会自动帮你记录时间的呀,比如你每一个另存的文件,每条新增的 Evernote ,每条在 GitHub 上提交的记录,都有系统自动记录的创建时间。不过这些信息要么不那么可靠,要么存在于不容易取到的地方,喜欢使用 Markdown 的话就难免会更偏爱这种手刷的纯文本时间戳一些。</p> <p>举个例子吧,下面是我的一条日常的工作 journal:</p> <ul> <li>[2016-10-22] {opt} {gl} xx 工具的进一步利用 <ul> <li>[2016-10-24] 确认了 xx 在新版本上可以正常工作</li> <li>[2016-10-25] a+ 设计了 xx 的改进方案 (详见 yy)</li> <li>[2016-10-26] a+ 调查 zz 情况,提出方案,待下周实施</li> <li>[2016-10-26] a+ 调查 kk,确认一个是 mm,一个是 nn,下周改进</li> <li>[2016-10-27] 确认了 oo 需要 pp 空间(需要至少 qq 供此功能正常工作)</li> </ul> </li> </ul> <p>[注]</p> <ul> <li>发现知乎的代码高亮居然可以选到 AutoHotkey,好评~</li> <li>journal 里的大括号是为了避免跟 Markdown 的常用中小括号冲突 []/()</li> <li>journal 里的 bold a+ 是指有后继追加任务</li> </ul> 2016.11 CppCon 2014-2016 选荐合辑 https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/ Sun, 06 Nov 2016 22:00:00 +0000 https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/ <p>用了两个周末,快速过了一遍 CppCon 2016 的 112 个演讲,筛出了下面我觉得最有价值的 18 个 (其中有八个 must-reads 标注了星号)。</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cppcon2016_picked.png" width="790" height="815" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cppcon2016_picked_hub10f885b8354012876d5b187df4de0d1_112698_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cppcon2016_picked_hub10f885b8354012876d5b187df4de0d1_112698_1024x0_resize_box_3.png 1024w" loading="lazy" alt="cppcon2016_picked" class="gallery-image" data-flex-grow="96" data-flex-basis="232px" ></p> <p>文档的实际链接地址是:</p> <ul> <li><a class="link" href="https://github.com/mc-gulu/dev-awesomenesses/blob/master/awesome-cppcon.md" target="_blank" rel="noopener" >https://github.com/mc-gulu/dev-awesomenesses/blob/master/awesome-cppcon.md</a></li> </ul> <p>为了方便查看,这里直接使用该页面的截图 (如无意外不会更新)。在 <a class="link" href="https://github.com/mc-gulu/dev-awesomenesses/blob/master/awesome-cppcon.md" target="_blank" rel="noopener" >awesome-cppcon</a> 页面中,也包含了 CppCon 2014 时我挑出的 10 个值得一看的演讲及其它的相关信息。后续的更新和问题修复也都会出现在那里。</p> <hr> <p>下面挑了几张图,没有时间去看列表的同学可以随便翻翻感受一下。</p> <hr> <p><strong>The roots of C++</strong> - 一张图展示 C++ 的来源</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/the_roots_of_cpp.jpg" width="1261" height="649" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/the_roots_of_cpp_hu0698a11576f11a6c0998b660cbe1365b_121939_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/the_roots_of_cpp_hu0698a11576f11a6c0998b660cbe1365b_121939_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="the_roots_of_cpp" class="gallery-image" data-flex-grow="194" data-flex-basis="466px" ></p> <p><strong>Major Design Decisions</strong> - How does it change the way we write code?</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/major_design_decisions.png" width="1217" height="815" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/major_design_decisions_hu9746a3534033cf45a187b327d04299e7_93563_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/major_design_decisions_hu9746a3534033cf45a187b327d04299e7_93563_1024x0_resize_box_3.png 1024w" loading="lazy" alt="major_design_decisions" class="gallery-image" data-flex-grow="149" data-flex-basis="358px" ></p> <p><strong>#C++ users</strong> - C++用户量的历史增长情况 (I)</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cpp_users_1.png" width="884" height="472" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cpp_users_1_huf1803b1f84bef98626cd74ff0bb8275a_176192_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cpp_users_1_huf1803b1f84bef98626cd74ff0bb8275a_176192_1024x0_resize_box_3.png 1024w" loading="lazy" alt="cpp_users_1" class="gallery-image" data-flex-grow="187" data-flex-basis="449px" ></p> <p><strong>#C++ users</strong> - C++用户量的历史增长情况 (II)</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cpp_users_2.png" width="1173" height="693" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cpp_users_2_hud9871bfb26c5004e930e3cfe8f676e64_85685_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/cpp_users_2_hud9871bfb26c5004e930e3cfe8f676e64_85685_1024x0_resize_box_3.png 1024w" loading="lazy" alt="cpp_users_2" class="gallery-image" data-flex-grow="169" data-flex-basis="406px" ></p> <p><strong>uftrace</strong> - A function graph tracer for userspace programs</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/uftrace.png" width="1149" height="907" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/uftrace_hu42029b67efce67032335d3df7176c637_126517_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/uftrace_hu42029b67efce67032335d3df7176c637_126517_1024x0_resize_box_3.png 1024w" loading="lazy" alt="uftrace" class="gallery-image" data-flex-grow="126" data-flex-basis="304px" ></p> <p><strong>Lock Analyzer</strong> - Rainbow Six Siege - Quest for Performance</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/r6-lock-analyzer.png" width="1483" height="800" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/r6-lock-analyzer_hu2340b9b7e8739916c6d55cbd2d3be6ae_209169_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/r6-lock-analyzer_hu2340b9b7e8739916c6d55cbd2d3be6ae_209169_1024x0_resize_box_3.png 1024w" loading="lazy" alt="r6-lock-analyzer" class="gallery-image" data-flex-grow="185" data-flex-basis="444px" ></p> <p><strong>Single Source Compilation Model</strong> - Towards Heterogeneous Programming in C++ (I)</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/Single-Source-Compilation-Model.png" width="1486" height="796" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/Single-Source-Compilation-Model_hu50809aabae87e74424ee1c124bbf847e_97531_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/Single-Source-Compilation-Model_hu50809aabae87e74424ee1c124bbf847e_97531_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Single-Source-Compilation-Model" class="gallery-image" data-flex-grow="186" data-flex-basis="448px" ></p> <p><strong>Multi Compilation Model</strong> - Towards Heterogeneous Programming in C++ (II)</p> <p><img src="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/Multi-Compilation-Model.png" width="1517" height="804" srcset="https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/Multi-Compilation-Model_hucec1b0c28dca43406991a7ade50f7daf_102500_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-11-06-awesome-cppcon-14-16/images/Multi-Compilation-Model_hucec1b0c28dca43406991a7ade50f7daf_102500_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Multi-Compilation-Model" class="gallery-image" data-flex-grow="188" data-flex-basis="452px" ></p> <hr> <p>[注]</p> <ul> <li>如果时间充裕,晚些时候会再对有趣的讲演专文另行记录。眼下先这样吧。</li> <li>本文同时发在我的知乎专栏 (<a class="link" href="https://zhuanlan.zhihu.com/p/23465369" target="_blank" rel="noopener" >链接</a>)</li> </ul> 2016.09 我的时间分配变迁记 (原问题:程序员工作中占时间最长的是哪个步骤?) https://gulu-dev.com/post/2016-09-12-time-division/ Mon, 12 Sep 2016 08:42:00 +0000 https://gulu-dev.com/post/2016-09-12-time-division/ <p>本文是前天晚上的一个答案,稍作修订,放到这里。</p> <p><a class="link" href="https://www.zhihu.com/question/50549013/answer/121526486" target="_blank" rel="noopener" >程序员工作中占时间最长的是哪个步骤?</a></p> <p>下面的图表是我个人工作十年以来各项时间占用的大致变化情况。需要说明的是,这不是由精确的统计汇总而来,而只是大致估算;不是我认为理想的情况,而只是此期间实际发生的情况。</p> <p><img src="https://gulu-dev.com/post/2016-09-12-time-division/1.png" width="756" height="143" srcset="https://gulu-dev.com/post/2016-09-12-time-division/1_hu7cdc5de7cfa312556cd940829133d2dc_4986_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-09-12-time-division/1_hu7cdc5de7cfa312556cd940829133d2dc_4986_1024x0_resize_box_3.png 1024w" loading="lazy" alt="1" class="gallery-image" data-flex-grow="528" data-flex-basis="1268px" ></p> <p>(<strong>设计和实现</strong>) 长期来看,你愿意花在设计和实现的时间,基本上取决于你对这项工作的热爱程度。我见过一些进入管理岗位的同事,这两项迅速地滑落至接近 0 的;也见过始终对编码本身情有独钟的——这并没有对错,只是不同个体的兴趣,权衡和选择。</p> <p>(<strong>测试和调试</strong>) 总得来说,随着工程能力的提高,程序员的职业生涯里的测试和调试时间应该是稳步降低的。基本上你总是能把越来越多本来需要手动完成的事情交给机器去做,比如包括__运行某个游戏流程__和__查看渲染效果__这类本来需要人来做的事情,运用恰当的脚本和图像对比手段,在绝大部分情况下都能比人做得更快更好。</p> <p>(<strong>沟通</strong>) 沟通需要单独拎出来说一下。随着时间推移,不管是否情愿,用于沟通的时间应该是逐渐增加的。但当接近或到达一个分水岭 (对我来说是 C 阶段的 50%) 时,我意识到如果不做点什么,很快会演变成做一整天沟通,只有晚上瞅着空写写代码的凄惨结局,于是学着不断地有意识地限制沟通的频率和长度,利用各种策略来避免无效的沟通,到现阶段能大致控制在工作时间的 1/3 左右。</p> <p><img src="https://gulu-dev.com/2.png" loading="lazy" alt="2" ></p> <p>关于几个阶段,简单补充说明一下,</p> <p>阶段 A 时我刚刚毕业入行,在一家外企做普通程序员。由于需求相对比较固定,主导权和解释权一般都掌握在 Game Design 的手上。作为普通工程师,在引擎提供的框架内做有限的功能,大部分工作是不太需要架构设计的编码工作。</p> <p>阶段 B 时,我在一家网游公司做客户端引擎组的组长,负责引擎技术和相关的人员管理。这是我成长最快的一个阶段,也是职业生涯前期里,安排相对均衡的一个阶段。由于三个原因:1. 需要在一穷二白的基础上建立完整的 3D 游戏工作流程 2. 缺乏有经验的 TA (那个时候国内还没这个概念) 3. 我本人缺乏管理的经验,因此在沟通 (尤其是跨部门沟通) 上投入了较多的时间。</p> <p>阶段 C 时,我是一个初创公司的技术负责人,跟阶段 B 相比,更为艰巨和困难,而对于游戏的制作和流程有了一定认识和思考的基础之后,我希望能够有机会面对更大的挑战。虽然条件艰苦资源匮乏,但一年多的时间打造出来一个技术团队,个个能够独当一面,而我穿梭其中,铺路搭桥。在这个时间点上,我个人最缺乏的是对资本,市场和商业逻辑本身的理解,因此造成了一些先天和后天的限制。团队解散时和大家淡定作别,回到家时一个大老爷们在书房里关上门泣不成声。</p> <p>阶段 D 和 E 就不多说了,我试着把自己的目光从一个接一个的移动靶上拉远,不再以“开宝箱”的心态面对项目,试着让自己沉静下来,试着不去用声名财富衡量自己的事业和人生,试着做到“宁静而致远”,试着让自己比以前更从容。随着年岁渐长,我也许不再能彻夜加班,但仍会去思考和提炼,投入地去做那些我认为有价值,有意义的事。</p> <p>让我觉得踏实的是,我慢慢地知道了自己是谁。</p> 2016.08 为动态加载的 Unity C# DLL 添加调试支持 https://gulu-dev.com/post/2016-08-30-unity-external-dll-debugging/ Tue, 30 Aug 2016 19:00:00 +0000 https://gulu-dev.com/post/2016-08-30-unity-external-dll-debugging/ <p>昨天遇到了一个 C# DLL 动态载入后调试信息缺失的问题,今天上午解决后记录一下,以便遇到这个问题的同学可以参考。</p> <p>(注)此文中的截图内文字偏小,可以 Ctrl + 鼠标滚轮放大查看。</p> <hr> <h2 id="问题描述">问题描述</h2> <p>我们知道,Unity 中的 <code>Debug.Log()</code> 系列函数不仅能输出用户内容,而且能通过类似 <code>StackTraceUtility.ExtractStackTrace()</code> 这样的机制把该输出对应的堆栈打出来;当用户代码出现未捕获异常时,Unity 也会利用该机制输出异常及相关的完整堆栈信息。</p> <p>如这个函数:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">void</span> <span class="n">PrintStacktraceOrdinary</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Debug</span><span class="p">.</span><span class="n">LogFormat</span><span class="p">(</span><span class="s">&#34;Stacktrace (ordinary): \n{0}&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">Environment</span><span class="p">.</span><span class="n">StackTrace</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>会输出下面的结果:</p> <p><img src="https://gulu-dev.com/_image/2016-08-30-unity-external-dll/1.png" loading="lazy" alt="1" ></p> <p>注意,此图中 StackTrace 上的每个函数都有完整的调试信息(函数,源文件,行号)。</p> <hr> <p>但是,当调用链中有一部分位于一个外部 DLL 中时,位于外部 DLL 中的这一部分函数调用,是无法像正常的函数那样显示堆栈的。</p> <p>如下面位于单独 DLL 的函数:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">namespace</span> <span class="nn">test_stacktrace_dll</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">class</span> <span class="nc">Foo</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">string</span> <span class="n">GetStacktraceInDLL</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">Environment</span><span class="p">.</span><span class="n">StackTrace</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>在 Unity 工程中像下面这样使用</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">void</span> <span class="n">PrintStacktraceInsideUserDLL</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Debug</span><span class="p">.</span><span class="n">LogFormat</span><span class="p">(</span><span class="s">&#34;Stacktrace (inside user dll): \n{0}&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">test_stacktrace_dll</span><span class="p">.</span><span class="n">Foo</span><span class="p">.</span><span class="n">GetStacktraceInDLL</span><span class="p">());</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>会输出下面的结果:</p> <p><img src="https://gulu-dev.com/_image/2016-08-30-unity-external-dll/2.png" loading="lazy" alt="2" ></p> <p>注意,此图中 Stacktrace 的第二行 <code>GetStacktraceInDLL()</code>,也就是外部 DLL 内的函数,尾部是不显示文件和行号信息的。</p> <p>这样的话,如果项目的 C# 代码放在外部的 DLL 里(一般有更好的代码组织和模块化,更好的 VS IL 代码生成,方便代码热更新等原因),调试的时候就会缺失不少辅助信息。</p> <h2 id="问题解决">问题解决</h2> <p>我们知道,每一个 VS 编译出来的 C# DLL 都带有一个 pdb 存储着调试相关的信息,如果能让 Unity 项目在运行时定位到这个 pdb 理论上就可以获取对应的信息了。对于一个常规的 C# 程序,只要把某个 DLL 对应的 .pdb 文件拷到 .exe 所在目录就可以了,但我试了一下把 .pdb 拷到项目 Assets 所在目录或 Unity.exe 所在目录,发现仍然无效,此时我开始推测是 Unity 所用的 mono 与 VS 生成的 pdb 之间的兼容性问题。</p> <p>随即想到,既然 Unity 项目自己生成的 Assembly 能顺利找到调试信息,那么这些 mono 生成的调试信息一定是存在项目工程内的某个地方,于是开始翻项目目录,最终在 Library 下的子目录里找到了这两个文件:</p> <p><img src="https://gulu-dev.com/_image/2016-08-30-unity-external-dll/3.png" loading="lazy" alt="3" ></p> <p>看起来 Unity/mono 生成的调试文件名叫 .mdb,也就是说只要把 VS 生成的 pdb 文件转成 mdb 就可以了。很快找到方法:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">&lt;unity_root&gt;\Unity-5.3.6f1\Editor\Data\Mono\lib\mono\2.0\pdb2mdb.exe &lt;target_assembly&gt;.dll </span></span></code></pre></td></tr></table> </div> </div><p>使用这个生成 mdb 后放入 <code>Assets</code> 目录,顺利得到额外的调试信息:</p> <p><img src="https://gulu-dev.com/_image/2016-08-30-unity-external-dll/4.png" loading="lazy" alt="4" ></p> <hr> <h2 id="动态载入--未捕获的异常">动态载入 &amp; 未捕获的异常</h2> <p>如果这个 DLL 是动态载入的呢?</p> <p>使用下面的代码动态载入这个 DLL 并调用上面的 <code>GetStacktraceInDLL()</code> 函数:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">localAssmlyPath</span> <span class="p">=</span> <span class="s">&#34;Assets/test_stacktrace_dll.bin&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">byte</span><span class="p">[]</span> <span class="n">src</span> <span class="p">=</span> <span class="n">File</span><span class="p">.</span><span class="n">ReadAllBytes</span><span class="p">(</span><span class="n">localAssmlyPath</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kt">string</span> <span class="n">localSymbolPath</span> <span class="p">=</span> <span class="s">&#34;Assets/test_stacktrace_dll.bin.mdb&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kt">byte</span><span class="p">[]</span> <span class="n">symbolBytes</span> <span class="p">=</span> <span class="n">File</span><span class="p">.</span><span class="n">ReadAllBytes</span><span class="p">(</span><span class="n">localSymbolPath</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">Assembly</span> <span class="n">assembly</span> <span class="p">=</span> <span class="n">Assembly</span><span class="p">.</span><span class="n">Load</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">symbolBytes</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">Type</span> <span class="n">type</span> <span class="p">=</span> <span class="n">assembly</span><span class="p">.</span><span class="n">GetType</span><span class="p">(</span><span class="s">&#34;test_stacktrace_dll.Foo&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">MethodInfo</span> <span class="n">getStacktrace</span> <span class="p">=</span> <span class="n">type</span><span class="p">.</span><span class="n">GetMethod</span><span class="p">(</span><span class="s">&#34;GetStacktraceInDLL&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">BindingFlags</span><span class="p">.</span><span class="n">Public</span> <span class="p">|</span> <span class="n">BindingFlags</span><span class="p">.</span><span class="n">Static</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">Debug</span><span class="p">.</span><span class="n">LogFormat</span><span class="p">(</span><span class="s">&#34;Stacktrace (dynamically): \n{0}&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">getStacktrace</span><span class="p">.</span><span class="n">Invoke</span><span class="p">(</span><span class="k">null</span><span class="p">,</span> <span class="k">null</span><span class="p">));</span> </span></span></code></pre></td></tr></table> </div> </div><p>载入时一同载入对应的 mdb 文件,可以得到同样的结果。</p> <hr> <p>顺便测试一下未捕获的异常,手动在 DLL 中制造一个,一样能看到完整的调试信息:</p> <p><img src="https://gulu-dev.com/_image/2016-08-30-unity-external-dll/5.png" loading="lazy" alt="5" ></p> <p>多说一句,这里若干操作均未处理错误,实际项目里至少要检查 Assembly 是否加载成功,处理获取的函数是否有效等错误。</p> <h2 id="示例代码">示例代码</h2> <p>本文中所有的示例代码可以在这里找到:</p> <ul> <li><a class="link" href="https://github.com/gl-notes/gln-public/tree/master/%28gl-bits-prior-to-2017%29/%282016%29%2001.%20Debugging%20C%23%20External%20DLL%20%28Unity%29" target="_blank" rel="noopener" >Debugging C# External DLL (Unity)</a></li> </ul> 2016.08 (3/3) DOOM3 网络架构 https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/ Thu, 11 Aug 2016 14:35:00 +0000 https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/ <h1 id="33-doom3-网络架构">(3/3) DOOM3 网络架构</h1> <p>本文是系列的第三篇:</p> <ol> <li><a class="link" href="http://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes" target="_blank" rel="noopener" >DOOM3 技术点滴</a></li> <li><a class="link" href="http://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution" target="_blank" rel="noopener" >DOOM/Quake I/II/III 网络模型的演化</a></li> <li><a class="link" href="http://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture" target="_blank" rel="noopener" ><strong>DOOM3 网络架构</strong></a></li> </ol> <p>本文绝大部分为较简短的记录,进一步的描述请<a class="link" href="http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf" target="_blank" rel="noopener" >参考原文</a>。</p> <hr> <h2 id="架构">架构</h2> <p>客户端把输入采样等玩家动作发给服务器,服务器回之以 PVS 内的压缩后的状态快照。</p> <p><strong>C/S 架构图</strong></p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/1_architecture.png" width="468" height="184" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/1_architecture_huc2b7215b6caaaf356eda0be3bbce0b25_10961_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/1_architecture_huc2b7215b6caaaf356eda0be3bbce0b25_10961_1024x0_resize_box_3.png 1024w" loading="lazy" alt="1" class="gallery-image" data-flex-grow="254" data-flex-basis="610px" ></p> <p>Doom3 做到了<strong>同样的玩家输入序列总是能产生同样的结果</strong>,因为以下两点得到了保证:</p> <ol> <li>除玩家输入外整个系统的确定性 (system-wide deterministic)</li> <li>不管渲染性能如何,整个游戏的逻辑状态总是以 60 fps 的频率更新</li> </ol> <p><strong>C/S 时间线</strong></p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/2_timeline.png" width="608" height="212" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/2_timeline_hu522f133ac27a9b9dee11a908ef815673_4014_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/2_timeline_hu522f133ac27a9b9dee11a908ef815673_4014_1024x0_resize_box_3.png 1024w" loading="lazy" alt="2" class="gallery-image" data-flex-grow="286" data-flex-basis="688px" ></p> <p>服务器以 10-20 Hz 的频率向客户端发状态快照。由于快照是一个 rtt 之前的状态,客户端需要回到那个时间点上去处理这个“过去的”状态,然后再基于这个状态重新预测并刷新所有物体在当下的状态,如下图:</p> <p><strong>预测示意图</strong> (Prediction at the client with a snapshot rate at 20Hz and a ping of around 80 milliseconds)</p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/3_prediction.png" width="438" height="282" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/3_prediction_hu997f11eac004d862d700e5694c2b19de_3901_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/3_prediction_hu997f11eac004d862d700e5694c2b19de_3901_1024x0_resize_box_3.png 1024w" loading="lazy" alt="3" class="gallery-image" data-flex-grow="155" data-flex-basis="372px" ></p> <p>由于玩家输入的频率 (input per second) 远低于逻辑处理的频率 (60 Hz),一个合理的推论是,最接近当下的几个逻辑帧,继续沿用与之前同样的输入一般是安全的。客户端使用服务器同步过来的其他玩家的输入来预测其接下来的运动,这些物理响应的机制与服务器上的真实逻辑是一致的。</p> <p>与 Quake 3 不同的是,玩家在屏幕上看到的渲染结果与真实的逻辑状态是无时差的 (注意是无时差而不是 100% 绝对准确),因此不需要像 Q3 那样在本地延时比较大时需要充分考虑提前量,因为系统把下发同步的预测也完全实现了。系统的确定性保证了服务器和客户端可以运行完全一致的逻辑 (dead reckoning),因此得到至少与服务器上一样好的行为预测结果。Quake 3 的 bot 已经展示了通过算法来预测玩家移动可以达到什么样的程度,即使用慢速导弹武器 (火箭筒 RL) 也可以非常精确地命中。(Q3 bot 使用考虑碰撞检测的简化物理逻辑来预测玩家在之后的位置)</p> <p>与 Quake 3 不同,Doom 3 的服务器和客户端使用同一份代码来更新/预测实体的状态,这样不用担心早先提到的互相干扰,开发新的单人模式 (并兼容多人) 也变得更简单了。</p> <h2 id="通信">通信</h2> <h3 id="基于-udp-的轻量级-reliable--unreliable-实现-最小化额外负担">基于 UDP 的轻量级 reliable / unreliable 实现 (最小化额外负担)</h3> <p>对于大多数状态同步而言,像 TCP 那样重发价值不大,因为被重发的状态十有八九因为过期已经不再有意义。</p> <p>Doom 3 实现了下面这样一个基于 UDP 特性的 FPS 通信架构</p> <p><strong>层次结构</strong></p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/4_layers.png" width="419" height="208" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/4_layers_hu41024c50b0831e82a78dd6ba5b0f9f64_3540_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/4_layers_hu41024c50b0831e82a78dd6ba5b0f9f64_3540_1024x0_resize_box_3.png 1024w" loading="lazy" alt="4" class="gallery-image" data-flex-grow="201" data-flex-basis="483px" ></p> <p>上行和下行均为单连接,同时可发送 reliable &amp; unreliable 的消息 (前者确保抵达),后者用于输入 (c2s) 和状态 (s2c) 的同步,只有非常特定和关键的消息使用可靠方式发送。</p> <p>这个网络系统被设计为<strong>不间断地生成一个不可靠消息流</strong> (unreliable stream) (包括 10-20Hz 的状态同步和更高频的输入同步),可靠消息被驼运 (piggy back) 在这个不可靠消息流上 (蚂蚁搬家)。具体实现上,可靠消息被先缓存在队列里,每一个都由一个不可靠消息搭载着发出,ack 后再发下一个 (ack 直接借用了对面过来的 unreliable stream) 这样整个信道实现了最重要的保证:(通过1:1的驼载)<strong>任何一条可靠消息总是能在首个紧接着的不可靠消息之前抵达。</strong> (the message channel guarantees that a reliable message arrives before the first next unreliable messages comes through)</p> <p>此外,对于不可靠的信息流,客户端的发送频率比服务器高3-4倍 (可靠消息的运输和响应能力),这样的话来自服务器的可靠消息是不需要 timeout 机制的,因为接下来的几个客户端消息没有 ack 的话,服务器就可以直接重发了。</p> <h3 id="unreliable-message-headers">Unreliable Message Headers</h3> <p>整个系统的大部分信息是来自服务器的状态快照 (Snapshots) 和来自客户端的玩家输入 (User Commands),这些业务数据都通过 unreliable message 传递。(message header 如下图所示)</p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/5_mesage_headers.png" width="927" height="234" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/5_mesage_headers_hub46e30e7ca069017b42b898d1497af57_28838_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/5_mesage_headers_hub46e30e7ca069017b42b898d1497af57_28838_1024x0_resize_box_3.png 1024w" loading="lazy" alt="5" class="gallery-image" data-flex-grow="396" data-flex-basis="950px" ></p> <p>服务器:</p> <ul> <li>32 位 game id 里包含了游戏本身的识别 id,地图信息和关键的业务设置</li> <li>8 位的 message type 用来区分本条消息的类型。</li> </ul> <p>客户端:</p> <ul> <li>首个 seq id 是最近收到的服务器消息的序列号 (用于 ack),unreliable message 本身是不需要 ack 的,但是当需要的时候,服务器可以在特定的时间点上用这个 seq id 检查客户端是否有及时的反馈。</li> <li>game id 用于环境的合法性校验,没通过校验的话,服务器会追加一条完全配置信息,用于指导客户端去尝试进入正确的环境。</li> <li>快照的 seq id 用于差异压缩 (delta compression)</li> <li>同样也有 message type。</li> </ul> <h3 id="快照-snapshots">快照 (snapshots)</h3> <p>下图是下发快照的构成和完整的操作序列:</p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/6_snapshot.png" width="870" height="402" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/6_snapshot_hu1db9055c3e29f991ac9e5444a5c39dc9_67266_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/6_snapshot_hu1db9055c3e29f991ac9e5444a5c39dc9_67266_1024x0_resize_box_3.png 1024w" loading="lazy" alt="6" class="gallery-image" data-flex-grow="216" data-flex-basis="519px" ></p> <p>快照包含的几项关键信息:</p> <ul> <li>序列号 (seq id)</li> <li>帧编号 (frame id)</li> <li>帧时刻 (frame time)</li> <li>客户端领先的时间量 (client ahead time, 参考客户端最近一次发上来的时刻及延时)</li> </ul> <p>实际的业务数据信息 (以下信息均做了差异压缩):</p> <ul> <li>entity states 是与上次快照相比较的状态变化</li> <li>pvs bit string 是 pvs 的完整可见状态列表 (这个信息由服务器随时下发更新)</li> <li>pvs 无关的游戏状态更新</li> <li>其他玩家的指令信息</li> </ul> <h3 id="用户指令-user-commands">用户指令 (User Commands)</h3> <p>下图是上行的用户指令构成和完整的操作序列:</p> <p><img src="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/7_user_cmds.png" width="906" height="352" srcset="https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/7_user_cmds_hu2ed18e7591cdb8c1641780b69e7563e2_59440_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture/7_user_cmds_hu2ed18e7591cdb8c1641780b69e7563e2_59440_1024x0_resize_box_3.png 1024w" loading="lazy" alt="7" class="gallery-image" data-flex-grow="257" data-flex-basis="617px" ></p> <ul> <li>调试用的客户端预测毫秒数</li> <li>这一组用户指令中,第一个的所在帧编号</li> <li>后续的每个 user command 对应接下来的一帧,反映了输入的变化差异</li> </ul> <h2 id="压缩">压缩</h2> <h3 id="bit-packing">Bit Packing</h3> <ul> <li>移除逻辑上的无用位。</li> <li>如 Health (HP) 虽然是 32 位整形,但实际在 0-100之间,只需 7 位就够了。</li> <li>浮点精度大部分取值范围不大的情况下 (如实体的移动速度,角度,朝向等) 只需要半精度。</li> </ul> <h3 id="差异压缩-delta-compression">差异压缩 (Delta Compression)</h3> <ol> <li>变量级的差异压缩。如果一个变量没变过,就写一个 0 (1 bits) 如果变过,就写 1 (1 bits) + 实际变量内容 (bit packed)</li> <li>实体级的差异压缩。 <ul> <li>快照之间的差异比较</li> <li>基于一个包含完全实体信息的公共基 (common base)</li> <li>当进出某个客户端的 pvs 时开始/结束同步</li> </ul> </li> <li>pvs 差异压缩。 <ul> <li>每个实体 1 bit 则 4096 个完整信息会消耗 512 字节</li> <li>由于不同帧之间 pvs 变化不大,可以按组压缩,每组 32 bits</li> <li>如果任何一组没有实体进出 pvs,写个 0 (1 bits)</li> <li>假设 pvs 没有任何变化,4096 个对象只需要 16 字节</li> </ul> </li> </ol> <p>客户端随着 User Commands 上报的 ack 频率远高于下发快照的频率,所以丢包也没关系。服务器一旦收到 ack 就可以更新公共基并用 reliable message 通知客户端做同样的改动,驼运机制保证了 reliable message 总是先于新快照抵达客户端,这样被 ack 的快照总是能在处理新快照前被用于更新客户端的公共基。这样,公共基的状态维护就可以保证是整体上同步的</p> <h3 id="消息压缩-0-compressor">消息压缩 (0-compressor)</h3> <p>上面的差异压缩会产生大量的 0 (没有变化),所以开销最小也最有效的压缩是针对 0 的特殊处理。</p> <p>每次处理 3 位,如果中间有一位不为 0 就保持不变,否则继续读,直到遇到不为零的情况,此时写下三个零 (3 bits) 和重复次数 (3 bits)</p> <p>最大压缩比为 4:1,这里可以用不同的位数但 3 被验证为实际压缩比最高的。</p> <p>举个例子:</p> <blockquote> <p>000'000'000'010'000'000'000'000'110'000'000'000'000'000</p> </blockquote> <p>会被压缩为</p> <blockquote> <p>000'011'010'000'100'110'000'101</p> </blockquote> <p>这个例子里压缩比为 14:8。</p> <p>反过来也可以针对这种压缩方式对快照中的变量排列进行优化。把变量按照改变频率分组放在一起,以促使产生更多的连续 0。</p> <h3 id="效果">效果</h3> <ul> <li>bit packing: 10-15%</li> <li>delta compression: 90%+</li> <li>zero-compressing: 15-50%</li> </ul> <h2 id="更多的潜在改进">更多的潜在改进</h2> <ul> <li>快照的公共基是从空状态开始的,实际上对于任何一个已加载地图,可以从一个已完全初始化的状态开始,避免一上来的流量开销</li> <li>一些使用 reliable 的事件只要不影响游戏的逻辑进行 (如特效,光照等) 可以改成 unreliable 并缓存一下</li> <li>一些本来是同步过来的实体本质上只是游戏逻辑的衍生,是可以由客户端自行维护的</li> <li>客户端的预测可以改得更加细粒度 <ul> <li>开发者应可更容易指定哪些不需要预测 (直接使用快照的插值)</li> <li>可以随时关掉某个实体的同步 (比如挂了的怪物) 纯粹由客户端接管</li> </ul> </li> <li>可以把所有的实体以同样的频率更新加以改进,让那些不那么重要的实体以较低的频率更新 (LOD-syncing)</li> <li>对于不重要的实体,客户端的多帧预测往往可以合并为较少的较大帧 (降低运算量)</li> </ul> <p>[系列完]</p> 2016.07 (2/3) DOOM/Quake I/II/III 网络模型的演化 https://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution/ Sun, 24 Jul 2016 08:00:00 +0000 https://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution/brands.jpg" alt="Featured image of post 2016.07 (2/3) DOOM/Quake I/II/III 网络模型的演化" /><h1 id="23-doomquake-iiiiii-网络模型的演化">(2/3) DOOM/Quake I/II/III 网络模型的演化</h1> <p>这一篇是上一篇<a class="link" href="http://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes" target="_blank" rel="noopener" >DOOM3 技术点滴</a>的自然延续,但内容上独立成篇,实际上描述了 id software 从 DOOM 1 开始的若干款 FPS 游戏在网络架构方面的演化。同样的,更多的细节可参考<a class="link" href="http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf" target="_blank" rel="noopener" >这里</a>和 Quake III Arena 网络协议规范(非官方) (看起来在互联网上已经找不到链接了)。</p> <hr> <p>本文是系列的第二篇:</p> <ol> <li><a class="link" href="http://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes" target="_blank" rel="noopener" >DOOM3 技术点滴</a></li> <li><a class="link" href="http://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution" target="_blank" rel="noopener" ><strong>DOOM/Quake I/II/III 网络模型的演化</strong></a></li> <li><a class="link" href="http://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture" target="_blank" rel="noopener" >DOOM3 网络架构</a></li> </ol> <h2 id="一般性说明">一般性说明</h2> <p>游戏网络架构通常体现在四个要素的平衡上:一致性,响应性,带宽,延迟 (consistency, responsiveness, bandwidth and latency requirements)</p> <p>“Multiplayer gaming is about <strong>shared reality</strong>.”</p> <p>FPS 游戏的状态通常表现为一个实体列表:玩家,怪物,导弹,门等,这些实体 (entities) 与其全部作为不同的元素去区分对待,不如提供一个公共的结构和接口来简化通信。</p> <p>“Networking in first person shooters is all about synchronizing the state of multiple copies of the same game entities such that <strong>all players experience the same changes and events</strong> in the virtual environment.”</p> <p>为了达到即时同步这些状态的目的,有些实现方式需要参与者去管理和维护其自有的那份拷贝,通过施加一致的逻辑来推动所有的状态去同步地更新,而另一些实现则是随着时间的流逝不断地比较和发送最小的状态变化和差异。</p> <h2 id="p2p-模型-doom">P2P 模型 (DOOM)</h2> <p>DOOM (1994) 的网络模型是完全同步的 P2P 系统。该系统每秒钟对玩家的动作 (move/turn/use/fire, etc.) 采样 35 次 (得到一个 tick command) 并发送给其他所有玩家,每个玩家都接受来自所有玩家的 tick command,当某个玩家收到所有其他玩家的下一帧 tick command 后,该玩家的本地游戏状态推进到下一帧。这样的后果是全局性的延迟 (每个玩家从做出动作到收到反馈的响应时间) 由最慢网络连接的玩家决定。</p> <p>这个网络模型逻辑上非常简单,但存在这些问题:</p> <ol> <li>所有玩家都需要主动维护完美的状态同步,由于硬件不同(有时甚至是未初始化的变量)等引入的不一致,会让每个参与者细微的不同被累积下来,导致参与者之间显著的视觉和逻辑的差异。这种不一致的引入很难查,因为只有当它们累积起来才会有明显的效果,而等感觉到差异时,真正的问题已经发生很久了。</li> <li>完全同步的网络无法跨平台。不同的硬件上,由不同编译器生成的汇编指令有时会产生轻微不同的行为 (浮点指令尤甚)。</li> <li>随着玩家数量增长,延迟会迅速变得难以接受。而且只要有一个玩家的网络有波动,会影响到所有人的体验。</li> <li>随着玩家数量增长,带宽需求会指数性地同步增长。</li> <li>同步网络由于只发送 tick command,所有玩家必须同时启动游戏 (来保证游戏状态的一致性) 无法做到随时的加入和退出。</li> <li>由于玩家本地维护了所有的状态,方便了作弊的实现。</li> </ol> <h2 id="packet-server-包的简单中继">Packet Server (包的简单中继)</h2> <p>这个模型在原版 DOOM 的基础上增加了一个 Packet Server,负责转发所有的 tick command。玩家不再直连其他所有玩家,而是连到这个服务器 (某个玩家机器上) 以获取最新的状态。这样改进后,同步量降低了,而且如果一个玩家很卡,只会影响到他自己的游戏体验。但上述的大多数问题依然存在。</p> <h2 id="client-server-quake-iiiiii">Client Server (Quake I/II/III)</h2> <p>Quake I/II/III 实现了比较典型的 C/S 架构 (1996),这个模型中服务器负责所有的逻辑判断,客户端本质上只是一个渲染终端。玩家把自己的操作和输入发送给服务器,收到一个实体列表用于渲染。服务器把压缩后的快照发给客户端 (10-20Hz) 客户端使用这些快照来插值或推导出平滑连贯的体验 (interpolates between, or extrapolates from the last two snapshots)。</p> <blockquote> <p>在一般情况下(比如在古代的引擎Quake 1中),客户端收集到用户命令后发送给服务器,此后就在等待服务器返回新的游戏状态。这是很笨的。在Quake 3中,客户端不会傻等,而会预测可能的游戏状态,其实预测状态所用的代码跟服务器端的代码是一样的,所以服务器端的状态和客户端的状态往往是一致的。如果确实不一致,则“服务器为准原则”将生效。</p> <ul> <li>&ldquo;Quake III Arena 网络协议规范(非官方)&rdquo;</li> </ul> </blockquote> <h3 id="响应性和预判">响应性和预判</h3> <p>这个模型同样有响应性问题,从输入的采样和发送到屏幕反馈同样需要一个 roundtrip 延时。为了克服延时客户端预测了玩家的下一步行动 (在<a class="link" href="http://gulu-dev.com/post/2014-03-15-dynamic-prediction-and-latency-compensation" target="_blank" rel="noopener" >之前的 blog</a>中有提到)。玩家的输入在发出去的同时,本地立刻处理,而环境状态做了上文说到的 interpolate/extrapolate,也就是说玩家看到的自身是 (可预计的) 操作结果,而其他人是过去的状态。(这一点与魔兽世界是一致的) 这个 C/S 架构是异步的。对任何一个玩家而言,服务器的全局模拟落后于该玩家在本地的实际操作快照,而环境的状态同步更是落后于全局模拟。</p> <p>这个模型允许中途加入和退出 (除了做 server 的玩家,如果不是 dedicated 的话)。由于玩家的判断基于的是其他玩家过去的状态,实际的击中检测发生在晚些时候的服务器上,在延时较高的情况下,玩家需要不断考虑延时状况并打提前量才能在未来的实际判断中击中对方。</p> <h3 id="延迟补偿的潜在问题">延迟补偿的潜在问题</h3> <p>半条命在这个基础上引入了一种特定的延迟补偿 (lag compensation),当玩家向某个目标 (若干毫秒前的状态) 射击时,做实际检测的服务器会采用该目标若干毫秒前的状态来检验是否击中。这么做需要服务器把之前一小段时间的状态持续地保存下来,这样不仅增加了实现复杂度,而且导致了某种程度的不一致性。延时高的玩家反而更容易因为补偿获得更有利的判断,严重影响游戏体验 (实例见<a class="link" href="http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf" target="_blank" rel="noopener" >这里</a>第六页末尾,值得一读)。这种补偿只能对目标的位置回滚,而所有其他环境状态的改变却已无法倒退,这也会影响实际的体验。</p> <h3 id="工程问题逻辑和预测代码分离">工程问题:逻辑和预测代码分离</h3> <p>Q3 里服务器上跑的逻辑代码 (&ldquo;game code&rdquo;) 跟客户端跑的渲染和预测代码 (&ldquo;client game code&rdquo;) 实现在物理上不同的模块里,但却需要对彼此的内部细节非常清楚 (才能保证预测和实际行为的一致性)。这个强耦合使得扩展游戏变得很困难,这也是难以实现单人游戏模式的原因之一。有时使用 Q3 引擎的游戏得为多人模式和单人模式发布两个不同的 exe,其中单人模式直接使用 game code 来简化逻辑流程。</p> <h3 id="插值推导的局限性">插值/推导的局限性</h3> <p>由于快照的接收频率往往低于实际渲染的帧速,就需要上文提到的 interpolate/extrapolate,考虑物理模拟和交互的话,(为了跟服务器逻辑一致) 推导会增加额外的实现复杂度。这些插值对位置数据很有效,但其他一些状态很难插值,有时性能也是问题,比如四元数的 slerp 就挺费的 (<a class="link" href="http://gulu-dev.com/post/2016-07-23-doom3-bfg-notes#toc_6" target="_blank" rel="noopener" >上一篇末尾</a>提到了相关的优化)</p> <h3 id="压缩状态同步冗余固定字长">压缩、状态同步冗余、固定字长</h3> <p>Quake III 里只有在 PVS 内的实体才会被同步状态,而且被同步的是压缩后的与上一次同步比较的差值 (delta compressed relative to the entity states from a previous snapshot) 这导致的结果是如果一个物体频繁进出 PVS 就没法做 delta 比较,总是发送完整状态,会导致不少冗余的同步量。</p> <blockquote> <p>为了提高网络通讯速度,降低带宽,Quake 3中采用了压缩的技术。这并不是指用一些压缩算法来直接压缩数据。而是指,在传送游戏状态数据时,只传送改变了的游戏状态,而不是全部发送过来。一般来说,这个叫做Delta技术。</p> <ul> <li>&ldquo;Quake III Arena 网络协议规范(非官方)&rdquo;</li> </ul> </blockquote> <p>出于简化,Q3 使用了固定长度的同步结构,导致不少字段被不同的功能各种复用,一晦涩复杂度就上去了。</p> <hr> <p>[本文完,系列待续]</p> 2016.07 id tech 网络模型演化 (1/3) DOOM3 技术点滴 https://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes/ Sat, 23 Jul 2016 23:43:00 +0000 https://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes/bfg.jpg" alt="Featured image of post 2016.07 id tech 网络模型演化 (1/3) DOOM3 技术点滴" /><h1 id="13-doom3-技术点滴">(1/3) DOOM3 技术点滴</h1> <p>今天下午读到<a class="link" href="http://fabiensanglard.net/doom3_documentation/index.php" target="_blank" rel="noopener" >一组 DOOM 3 相关的技术文章</a>,这些文章描述的是十年前 (2006) 的技术,但依然有较强的参考价值。其中的一份技术记录描述了 DOOM3 BFG 开发过程中遇到的一些特定的性能问题。</p> <p>这里的记录和描述非常简略和仓促,更详细的解释可<a class="link" href="http://fabiensanglard.net/doom3_documentation/DOOM-3-BFG-Technical-Note.pdf" target="_blank" rel="noopener" >参考原文</a>。</p> <p>[注] DOOM3 BFG 是 DOOM3 的全平台资料片。</p> <hr> <p>本文是系列的第一篇:</p> <ol> <li><a class="link" href="http://gulu-dev.com/post/2016-07-23-id-1-of-n-doom3-bfg-tech-notes" target="_blank" rel="noopener" ><strong>DOOM3 技术点滴</strong></a></li> <li><a class="link" href="http://gulu-dev.com/post/2016-07-24-id-2-of-n-network-model-evolution" target="_blank" rel="noopener" >DOOM/Quake I/II/III 网络模型的演化</a></li> <li><a class="link" href="http://gulu-dev.com/post/2016-08-11-id-3-of-n-doom3-network-architecture" target="_blank" rel="noopener" >DOOM3 网络架构</a></li> </ol> <h2 id="技术背景">技术背景</h2> <p>DOOM3 在发售时 (2004) 的性能状况是,在最低配的机器上效果全关 640<em>480 的情况下维持在 20fps,而资料片 DOOM 3 BFG 的目标是在 2012 年的机器上 1280</em>720 下平稳地地运行在 60 fps 上,这意味着使用原版的 1/3 的时间绘制 3 倍于原版的像素数量。</p> <p>硬件变化上,这些年中 CPU 频率变化不大,但多核化使得多线程能够更有效率 (而 DOOM3 原版是单线程);GPU 发展很快,使用新功能来优化特定的渲染特性 (比如用 coarse Z-Cull / Hierarchical-Z 来优化 Stencil Shadow Volumes)就很重要;内存的性能提高不大,导致单次内存访问对应的可执行指令数提高了,利用这一点可以做很多对应的优化。</p> <h2 id="内存约束">内存约束</h2> <p>从 DOOM3 到 DOOM3 BFG 的一大困扰是缓存颠簸 (<a class="link" href="https://en.wikipedia.org/wiki/Thrashing_%28computer_science%29" target="_blank" rel="noopener" >cache thrashing</a>),由于 DOOM 的光影模型及由此导致的特殊的内存访问模式,使得内存带宽问题一直比较严重。</p> <p>DOOM3 通常会惰性地对“可能不需要的运算”做尽可能的延迟,这就需要在代码中用一些特定的标记来保存状态信息 (某一样东西算过没有,是否需要重算,等等) 而对多核更友好的则是现在比较流行的流式编程模型 (streaming programming model) ([注]也是曾提到的向无状态的 functional 的思维转换)。</p> <p>角色的可见部分和阴影体 (visible meshes and shadow volumes) 的蒙皮都在 GPU 上完成,这些 GPU 上的顶点避免了运行时拷贝。而 CPU 这边也同样需要动态生成的索引来生成角色的 shadow volume 及 light volume 内的部分,这个动态的数据量还是挺大的,在 Mars City 1 的过场中,每帧 shadow volume / light culling 的数据需求在 6MB 左右,数据生成在 2.5MB 左右。这里生成的索引使用了合并写 (<a class="link" href="https://en.wikipedia.org/wiki/Write_combining" target="_blank" rel="noopener" >write-combined buffer</a>) 由于这些线程存在不同线程内不同时序的读,很容易造成大量的缓存颠簸。这种颠簸的结果是,即使在使用了流式模型的 x86 上,也很难达到与 Cell SPU 处理器 (PS3) 匹配的吞吐量。这类读取使用了 SSE4.1 的一个指令 _mm_stream_load_si128() 来在合并写上尽可能地做合并读而不经过缓存。虽然合并写及其上的读取<a class="link" href="https://fgiesen.wordpress.com/2013/01/29/write-combining-is-not-your-friend/" target="_blank" rel="noopener" >被认为是效率低下</a>,但好在这里都是静态数据(也就是仅写入一次)。</p> <h2 id="用即时运算替代预计算存储">用即时运算替代“预计算+存储”</h2> <p>在首节末尾提到,CPU和内存演化速率不同是可以被利用的。这里用蒙皮来做一个非常典型的实例。在原版 DOOM3 里每个角色每帧只蒙一次皮,运算结果存下来给需要的场合用(构建 shadow volume 和渲染);而 DOOM3 BFG 内同一个角色在一帧内可能会蒙皮多次,仅仅是因为这样做速度更快。(在 GPU 上) 对一个 mesh 蒙皮的运算开销已经小于从内存中读取“未蒙皮版”的源数据的开销了。也就是说,<strong>每次都即时蒙皮的开销并不比去内存里读已蒙皮的数据高</strong>了。更何况还有如下几个巨大的优势:</p> <ol> <li>如果是用于渲染的话,读取和蒙皮都发生在 GPU 上,不需要通过总线,能获得较大的总线带宽节省</li> <li>未蒙皮的源数据可以所有相同的模型共享一份,而蒙过皮的话是每个角色都得存一份,内存开销是 1:n,能获得较大内存带宽的节省</li> <li>在多线程读的情况下,读同样的数据可以降低缓存颠簸的几率</li> </ol> <p>DOOM3 BFG 的 shadow volume 本质上是一组(针对未蒙皮数据即时生成的)索引。举个例子,如果一个蒙皮角色与两个投影光源交互,就会做 7 次蒙皮:2*2 次分别是两个 shadow volumes 的生成和渲染;1 次是正常的渲染,剩下两次是两个 <a class="link" href="https://simonschreibt.de/gat/doom-3-volumetric-glow/" target="_blank" rel="noopener" >light surfaces</a> 的渲染。由于前面提到的诸多原因,7 次蒙皮的性能比 DOOM3 原版的 1 次蒙皮后到处使用还要高,而且随着时间的推移,优势还会被进一步放大。</p> <p>这再一次凸显了 functional 的潜在巨大优势(跟直觉相反,即使考虑较大数据量,无状态仍然是更优的)而且由于不再维护一个 skinned version,代码更清晰和易懂了,理解和调试的成本降低许多。无状态也使得很多操作(如 shadow volumes 的构建)可以完全并行化。(关于 id 在 functional programming 的实践上更多的细节看<a class="link" href="http://www.gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php" target="_blank" rel="noopener" >这里(John Carmack: In-depth: Functional programming in C++)</a>)</p> <h2 id="内存-缓存友好的数据结构">内存-缓存友好的数据结构</h2> <p>这里总得来说是把之前实现的一些要么冗余,要么 cache-unfriendly 的一些数据结构改写为对缓存更友好的版本。总得来说其实我不太理解为啥在原始版本中很多可以用数组的地方需要用链表,而文中也一再表示所谓改好了的 &lsquo;idList&rsquo; 本质上也就是 resizable heap array(跟 <code>vector</code> 区别不大了)</p> <h2 id="结语">结语</h2> <p>最重要的几句话,可以直接摘录了:</p> <ul> <li>if these threads touch a significant amount of memory then cache thrashing may occur while many CPUs are poorly equipped to avoid large scale cache pollution. Without additional instructions to manage or bypass the cache, <strong>a shared cache between all CPU cores can result in less than ideal performance</strong>.</li> <li>various data structures that exhibit poor performance on today&rsquo;s hardware. These data structures tend to result in <strong>poor memory access patterns</strong> and <strong>excessive cache misses</strong>.</li> </ul> <h2 id="其它技术点">其它技术点</h2> <ul> <li><a class="link" href="http://fabiensanglard.net/doom3_documentation/37725-293747_293747.pdf" target="_blank" rel="noopener" >Slerping Clock Cycles</a> 这一篇描述了怎么使用 SSE 来对两个四元数做平滑的 Slerp (球面线性插值) (最近对 Unity 项目做优化的时候发现 Unity 的对应函数实在是效率不高,很想翻出来自己实现一把,转念一想跨平台的巨坑赶紧忍住了)</li> <li><a class="link" href="http://fabiensanglard.net/doom3_documentation/37726-293748.pdf" target="_blank" rel="noopener" >From Quaternion to Matrix and Back</a> 用 SSE 来优化四元数和矩阵互转的数学运算</li> <li><a class="link" href="http://fabiensanglard.net/doom3_documentation/37728-293750.pdf" target="_blank" rel="noopener" >Fast Skinning</a> 依然是用 SSE 来优化 CPU 上的蒙皮</li> </ul> <p>这几篇都是附代码的,顺便说一下,这些汇编代码非常清晰,连我这种汇编苦手都能基本看清脉络(主要是很久以前用 SSE2 实现过矩阵的基础运算),很适合用来熟悉 <a class="link" href="https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions" target="_blank" rel="noopener" >Intel SSE</a> 指令集。</p> <ul> <li><a class="link" href="http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf" target="_blank" rel="noopener" >The DOOM III Network Architecture</a> 这一篇是有价值的综述,晚些时候单独整理。</li> </ul> <hr> <p>[本文完,系列待续]</p> 2016.07 暴雪游戏开发趣闻 (若干则) https://gulu-dev.com/post/2016-07-03-blizzcon2015-talks/ Sun, 03 Jul 2016 20:23:00 +0000 https://gulu-dev.com/post/2016-07-03-blizzcon2015-talks/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-07-03-blizzcon2015-talks/blizzard.jpg" alt="Featured image of post 2016.07 暴雪游戏开发趣闻 (若干则)" /><p>这是 <a class="link" href="https://www.youtube.com/watch?v=mx_C7LkB1Q8" target="_blank" rel="noopener" >(Youtube) Blizzcon 2015 Engineering Community Amphitheater Discussion</a> 的部分内容。挑了重点,简单记录了一下。</p> <h2 id="设计和工程">设计和工程</h2> <ul> <li> <p>风暴英雄的数据驱动做得很好 (因此设计自由度很大)。在这样灵活度的支持下,设计师在 Blizzcon 上想搞个大新闻:两个不同的玩家可以控制同一个英雄。程序员们一听都慌了,后来想了想,搞定了。</p> </li> <li> <p>工程师们不会随便说“这不行”,要想让设计师们尽情发挥就不该随便说 No ——尤其是你要是不给弄,设计师们分分钟自给自足把自己的需求给实现了(掩面)。有张图 (此处应有图) 上画的是,一个垃圾桶上放了个插着电的熨斗,熨斗上放了咖啡壶,咖啡壶里装着意大利面。duang, 搞定,意大利面做成了~</p> </li> <li> <p>守望先锋有 “strike team” 内含来自不同部门的 n 个人 (Gameplay/Engine/Designer/Animator/FX Artist) 在一起配合完成一个功能 (通常是某个特定的英雄,如 Hanzo) 这些人一起配合,把一个特性打造到极致。(下一条紧接着说的是 —— 即使这样,D.Va 还是很难搞,花了 n 个月 —— There are two types of heroes in Overwatch: D.Va and not D.Va.)</p> </li> </ul> <hr> <ul> <li> <p>从工程师的角度,寻找并解决设计师的痛点很重要 (就像产品经理找用户的痛点那样) 要是设计师随便开个对话框都有 128 个复选框等着他们的话,根本就不会有啥好心情去为玩家创造优质体验了。所以工程师需要尽可能地把无关的细节隐藏起来。</p> </li> <li> <p>工程师和设计师的目标是互补的:设计师的目标是“让尽可能多的玩家获得最好的体验”,工程师的目标是“尽量不要让任何人有糟糕的体验” (这两个互补的目标能够让他们考虑和覆盖尽可能多的边界情况,带来更好的体验)</p> </li> <li> <p>解决设计问题的时候,他们考虑的最多的不是解决方案,而是对玩家实际体验的影响。</p> </li> </ul> <h2 id="编码和优化">编码和优化</h2> <ul> <li> <p>对 WOW 的服务器团队来说,如果开源代码能解决问题的话,他们会用的。在客户端的声明中能看到大量被使用的开源许可证。他们倾向于不要重复造轮子。</p> </li> <li> <p>服务器上的 Lua 引擎在经年累月的各种新增需求轮番轰炸下已经有点不堪重负了 (worse than not having documentation) 比如有 17 个名叫 teleport 的各不相同的函数用来传送~~ 对写插件的人来说挺痛的对吧,对内部开发者更痛。</p> </li> </ul> <hr> <ul> <li> <p>暴雪的绝大多数团队都是做 Code Review 的,而且积极使用现代的 C++ 特性来改善代码的可读性,尤其是考虑到暴雪产品的超长生命期。</p> </li> <li> <p>如果对同一个问题有一个快速方案和一个正确方案,团队往往会选择后者 (即使会花更多的时间) 回头修那些设计糟糕的系统非常痛苦。</p> </li> <li> <p>是努力尝试理解当前的逻辑,还是试着重写那些“看起来有点乱”的逻辑,这经常会是一个问题。一个好的标准是,即使是老代码,只要有清晰并明确定义的方式去扩展,那么就是“好的”代码,不必大动干戈。</p> </li> <li> <p>常用的方案往往过于通用,不总是能解决暴雪在开发游戏时面对的问题。暴雪的团队总是会试着跳出给定工具的限制,限制他们的往往是他们自己的设计。</p> </li> </ul> <hr> <ul> <li> <p>最初的 WOW 开发者在背包里用了数组,数组的起始部分是装备,接着是背包里的道具,再后面是后来加的银行。这些不同的数据的位置通过一系列算术运算来定位,而且相关的代码充满了硬编码。这些情况最直接的后果是背包没法很方便地扩充。大伙都忙着做功能,到后来已经修不动了。这就是固定尺寸背包的由来。</p> </li> <li> <p>VTune 和一些内部工具被用于性能分析。性能回归是自动化的:比如“在某个地图放上 10 个英雄混战”,每个成功的 build 之后都自动运行并比较性能情况,这样性能上一旦有啥异动能第一时间捕获。做优化时,重要的是取舍:优化的能力,时间,可维护性,优化后新增的限制等等。大量的优化时间被用于与美术沟通,简化碰撞体。</p> </li> </ul> <hr> <ul> <li> <p>Battle.Net 以一致的方式 (最小公分母) 对待所有暴雪的游戏。比如如果战网决定增加好友数量,需要所有的游戏都打好对应的补丁,支持新的数量之后才能进行</p> </li> <li> <p>已经在运营的游戏有大量的静态数据,所以补丁和更新往往意味着多地间大量的数据复制。使用 live test 确保基本的正常。服务器组需要以特定的顺序开启和关闭,尤其是 WOW / Battle.Net (他们的部署体系更古老一些)</p> </li> </ul> <h2 id="项目和资源管理">项目和资源管理</h2> <ul> <li>守望先锋团队使用 Perforce 管理代码和资源</li> <li>Battle.net 使用 Git/Jenkins</li> <li>WOW 服务器团队使用 SVN (但正在逐步迁移到 Git)</li> <li>Team 1 使用 Git/Perforce/Jenkins</li> </ul> <p>(暴雪的团队代号)</p> <ul> <li> <p>Team 1 - SC2 and Heroes</p> </li> <li> <p>Team 2 - WoW</p> </li> <li> <p>Team 3 - Diablo</p> </li> <li> <p>Team 4 - Overwatch</p> </li> <li> <p>Team 5 - Hearthstone</p> </li> <li> <p>WOW 团队主体没有使用太多的敏捷开发,但一些小团队正在开始做 scrum。暴雪不太在意一致性,在项目管理上团队都有自己的自由度。</p> </li> </ul> <h2 id="其它">其它</h2> <ul> <li> <p>WOW 团队正在研究 DX12 等新技术,不过现阶段还没啥好说的。</p> </li> <li> <p>在暴雪有很多人体验 VR,但他们更关心精彩的点子,而不是只想着堆一摞高科技。</p> </li> <li> <p>所有的团队都在往移动上靠,炉石之后更是如此。玩家在移动设备上玩的呼声很高,团队内部也是如此。</p> </li> <li> <p>(对应聘者) 有一个完整的游戏项目经验,对你的简历无疑具有很高的价值。对学习本身的热情和技能多样性同样很重要。</p> </li> </ul> <hr> <p>只是稍做了整理,看起来仍有点乱,见谅。</p> <p>[注]</p> <ul> <li>本文同时发于<a class="link" href="http://gulu-dev.com/post/2016-07-03-blizzcon2015-talks" target="_blank" rel="noopener" >我的 Blog</a>,我的<a class="link" href="https://zhuanlan.zhihu.com/p/21478562" target="_blank" rel="noopener" >知乎专栏</a>和<a class="link" href="https://cowlevel.net/article/1826853" target="_blank" rel="noopener" >奶牛关</a></li> <li>本文已授权 GameRes <a class="link" href="http://www.gameres.com/667969.html" target="_blank" rel="noopener" >全文转载</a></li> </ul> 2016.06 测量被 Lua 隔断的 Unity C# 代码性能 https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua/ Thu, 02 Jun 2016 18:39:00 +0000 https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua/stopwatch.jpg" alt="Featured image of post 2016.06 测量被 Lua 隔断的 Unity C# 代码性能" /><h2 id="问题描述">问题描述</h2> <p>Unity 项目在实践中往往选择使用 Lua 作为更上层的逻辑脚本。这一方面是由于 Unity 本身对热更不是很友好,用 Lua 热更灵活得多,另一方面也是简化与服务器共享代码和数据。目前多种不同的 Unity + Lua 集成方案中,实践中采用比较多的是庞巍伟同学的 <a class="link" href="https://github.com/pangweiwei/slua" target="_blank" rel="noopener" >slua</a> 方案。</p> <p>使用 Lua 的团队,往往倾向于“较重的”集成,也就是暴露相当大规模的引擎接口给 Lua,这样逻辑上才能有足够的自由度对游戏和引擎做全面的控制。当 C#/Lua 之间的互操作接口迅速增长到成千上万的数量时,一个重要的问题就会浮现出来:C# 和 Lua 的交互层对引擎是封闭的,很多引擎内建的工具,没有办法跨越宿主 (C#) 和脚本 (Lua) 的边界。这些受到影响的机制里,最重要的就是 Unity Profiler。</p> <p>Unity Profiler 是 Unity 提供的一个有力的性能分析工具 ,能够在优化阶段有效地帮助定位瓶颈,有时也容易借机发现一些潜藏的 bug。而当我们定位到某个 Lua 函数有较大的开销(CPU 或内存 GC Alloc)时,由于跨语言边界的影响被阻拦,就难以进一步观察更多的细节。</p> <h2 id="正常的做法">“正常的”做法</h2> <p>由于 Profiler 的 API 接口也一并暴露给了脚本,正常的做法是:根据 C# 这边的有问题的调用,翻到对应的 Lua 代码,把相关的脚本逻辑读一遍,为那些潜在的开销大的逻辑添加对应的性能剖析采样 Profiler.BeginSample()/EndSample(),来定位问题代码段落,然后再翻回对应 C# 函数,再在里面加上测试代码印证我们的想法。</p> <p>实际上我们知道,大多数情况下(如果不算 bug),与引擎部分相比,逻辑脚本的 CPU 开销是相对比较低的(逻辑代码里以 if 判断居多,遇到需要循环的情况都非常少,一般用不到啥非常复杂的运算——或者说在设计得当的情况下,复杂的运算都会交给底层去整块整块地做),而容易造成困扰的托管内存分配导致 GC 卡顿的内存问题,也是完全由脚本调回来的 C# 代码造成的。</p> <p>这样分析下来,往往绕一圈又回到 C# 里,中间付出了大量无谓的在脚本里兜圈子的时间不说,被逼着读和分析重复性高的脚本逻辑代码,也大大增加了精力和脑力的负担。</p> <h2 id="aop">AOP</h2> <p>既然知道了反正总是要从 Lua 回到 C# 的,那么有没有什么简单的办法,一劳永逸地为所有暴露给 Lua 的 C# 接口加上性能剖析采样呢?</p> <p>如果能做到这一点,我们就可以无视中间脚本层 (Lua) 的干扰,在 C# 环境内解决所有问题。</p> <p>俺的目光很自然地投向了 AOP (<a class="link" href="https://en.wikipedia.org/wiki/Aspect-oriented_programming" target="_blank" rel="noopener" >Aspect Oriented Programming</a>),这种技术能帮我们在不用修改目标函数代码的情况下,加入我们想执行的代码(就像 Python 的 decorator 那样)。</p> <p><img src="https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua/python-deco.png" width="479" height="263" srcset="https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua/python-deco_hue4db0ac8bbf166e5a22405edad4e2266_14480_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-06-02-profiling-csharp-code-behind-lua/python-deco_hue4db0ac8bbf166e5a22405edad4e2266_14480_1024x0_resize_box_3.png 1024w" loading="lazy" alt="python-deco" class="gallery-image" data-flex-grow="182" data-flex-basis="437px" ></p> <p>经过一番研究,我成功地得出以下这条结论:</p> <p><strong>现有的一些针对 C# 的 AOP 方法,在 Unity 的 mono 下,基本都跪了~~</strong></p> <p>还能不能让俺过一个快乐的儿童节了~~</p> <hr> <p>在这些尝试里,最接近成功的是:使用 lambda expression 包一层,添加相关代码后,再注册给 slua,然而,slua 需要为注册进来的函数添加下面的 attribute:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="na"> [MonoPInvokeCallbackAttribute (typeof (xxx))]</span> </span></span></code></pre></td></tr></table> </div> </div><p>而 C# 不支持为 lambda expression 添加 attribute,所以 &amp;_&amp; ……</p> <h2 id="利用-slua-代码生成的简明做法">利用 slua 代码生成的简明做法</h2> <p>发现难以通过 C# 本身的语言机制解决问题之后,我把目光投向了 slua:既然所有的绑定代码是 slua 生成的,那么不如直接修改生成代码,把采样代码生成到接口的绑定函数里~</p> <p>找到普通函数接口的生成位置</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">void</span> <span class="n">WriteFunctionImpl</span><span class="p">(</span> <span class="n">StreamWriter</span> <span class="n">file</span><span class="p">,</span> <span class="n">MethodInfo</span> <span class="n">m</span><span class="p">,</span> <span class="n">Type</span> <span class="n">t</span><span class="p">,</span> <span class="n">BindingFlags</span> <span class="n">bf</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>在一头一尾添加了对应的生成代码(<code>BeginSample()</code> 的参数可以直接用 <code>MethodInfo.Name</code> 得到正确的函数名 ),运行 slua 的 Make 生成一下,得到下面的结果(单个函数):</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="na">[MonoPInvokeCallbackAttribute(typeof(xxx))]</span> </span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">public</span> <span class="kt">int</span> <span class="n">xxx</span><span class="p">(</span><span class="n">IntPtr</span> <span class="n">l</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Profiler</span><span class="p">.</span> <span class="n">BeginSample</span><span class="p">(</span> <span class="s">&#34;xxx&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="m">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">catch</span><span class="p">(</span> <span class="n">Exception</span> <span class="n">e</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">error</span><span class="p">(</span> <span class="n">l</span><span class="p">,</span> <span class="n">e</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">finally</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Profiler</span><span class="p">.</span> <span class="n">EndSample</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>EndSample() 在 finally 内,保证每个出口都能正确配对。</p> <hr> <h2 id="粒度控制">粒度控制</h2> <p>接下来更进一步,我们希望有某种粒度的控制能力,只为某个关心的类生成,甚至只为该类内关心的函数生成。回到前面的函数生成所在的方法,可以看到签名:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">void</span> <span class="n">WriteFunctionImpl</span><span class="p">(</span> <span class="n">StreamWriter</span> <span class="n">file</span><span class="p">,</span> <span class="n">MethodInfo</span> <span class="n">m</span><span class="p">,</span> <span class="n">Type</span> <span class="n">t</span><span class="p">,</span> <span class="n">BindingFlags</span> <span class="n">bf</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>其中第二个参数可以用来筛选我们关心的函数(可以跟 m.Name 比较来过滤字符串),而第三个参数 <code>Type t</code> 可以用来筛选对应的类(通过 <code>if ( t == typeof(TargetClass))</code>),这样就可以只在我们需要的时候,为特定的类和函数生成了。</p> <hr> <p>对应改动在<a class="link" href="https://gist.github.com/mc-gulu/fdc154e072055ba9369557acb74461c9" target="_blank" rel="noopener" >这里</a> (&lt;15行)。</p> 2016.05 类型安全的 C++/Lua 任意参数互调用 https://gulu-dev.com/post/2016-05-19-cpp-lua-vargs/ Thu, 19 May 2016 22:42:00 +0000 https://gulu-dev.com/post/2016-05-19-cpp-lua-vargs/ <p>在 C++ 和 Lua 协作时,双方的互调用是一个绕不开的话题。通常情况下,我们直接使用 Lua/C API 就可以完成普通的参数传递过程。但在代码中直接操作 lua stack,容易写出繁冗和重复的代码。这时我们往往会借助 tolua++ 之类的库,把参数传递的工作自动化,降低负担。</p> <p>进一步讲,由于 Lua 的参数传递在个数和类型上非常灵活(任何一个函数可以传递任意个数和类型的参数),有时我们会希望在与 C++ 的互操作时保留这种灵活性,比如 C++ 向 Lua 发一个消息时,如果可以是一个消息 ID 带上任意数量和类型的参数,就会很方便(反过来也一样)。由于 C++ 能通过可变参的模板函数实现类型安全的参数传递,与 Lua 的动态参数列表相结合后,我们就能在一个接口上实现更大的跨语言自由度。</p> <hr> <p>有不少第三方库能够简化 C++ 和 Lua 之间的互调用,这次我们使用 LuaBridge 来完成工作。开始前我们先介绍一下普通的互调用怎么做。</p> <p>首先,从 C++ 调 Lua 的函数:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- lua side</span> </span></span><span class="line"><span class="cl"><span class="kr">function</span> <span class="nf">foo</span><span class="p">(</span><span class="n">str</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kr">return</span> <span class="n">string.format</span><span class="p">(</span><span class="s2">&#34;%s, %d, %f&#34;</span><span class="p">,</span> <span class="n">str</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kr">end</span> </span></span></code></pre></td></tr></table> </div> </div><div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// C side </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">luabridge</span><span class="o">::</span><span class="n">LuaRef</span> <span class="n">foo</span> <span class="o">=</span> <span class="n">luabridge</span><span class="o">::</span><span class="n">getGlobal</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="s">&#34;foo&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="k">auto</span> <span class="n">retString</span> <span class="o">=</span> <span class="n">foo</span><span class="p">(</span><span class="s">&#34;bar&#34;</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mf">0.25f</span><span class="p">);</span> <span class="c1">// 这里先忽略错误处理 </span></span></span></code></pre></td></tr></table> </div> </div><p>接着是 Lua 调 C++:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// C side </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">int</span> <span class="nf">CallMe</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">arg1</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">arg2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">stoi</span><span class="p">(</span><span class="n">arg1</span><span class="p">)</span> <span class="o">+</span> <span class="n">std</span><span class="o">::</span><span class="n">stoi</span><span class="p">(</span><span class="n">arg2</span><span class="p">);</span> <span class="c1">// 同样先不管错误处理 </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">luabridge</span><span class="o">::</span><span class="n">getGlobalNamespace</span><span class="p">(</span><span class="n">L</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">beginNamespace</span><span class="p">(</span><span class="s">&#34;native&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">addFunction</span><span class="p">(</span><span class="s">&#34;call_me&#34;</span><span class="p">,</span> <span class="n">CallMe</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">endNamespace</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- lua side</span> </span></span><span class="line"><span class="cl"><span class="n">sum</span> <span class="o">=</span> <span class="n">native.call_me</span><span class="p">(</span><span class="s2">&#34;15&#34;</span><span class="p">,</span> <span class="s2">&#34;20&#34;</span><span class="p">)</span> <span class="c1">-- sum = 35</span> </span></span></code></pre></td></tr></table> </div> </div><p>嗯,可以看到,在 LuaBridge 的帮助下,双方互调用的参数和返回值符合各自的习惯,不用写任何额外的代码。</p> <hr> <p>好了,热身完毕。现在我们看一下 C++ 调用 Lua 的可变参接口。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- lua side</span> </span></span><span class="line"><span class="cl"><span class="kr">function</span> <span class="nf">g_post</span><span class="p">(</span><span class="n">msgID</span><span class="p">,</span> <span class="p">...)</span> </span></span><span class="line"><span class="cl"> <span class="n">_queue</span><span class="p">:</span><span class="n">appendMsg</span><span class="p">({</span><span class="n">id</span><span class="o">=</span><span class="n">msgID</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">{...}})</span> </span></span><span class="line"><span class="cl"><span class="kr">end</span> </span></span></code></pre></td></tr></table> </div> </div><p>我们在可作为 functor 使用的 <code>luabridge::LuaRef</code> 上做一个简单的封装,如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">template</span><span class="o">&lt;</span><span class="k">class</span> <span class="nc">TRet</span><span class="p">,</span> <span class="k">class</span><span class="err">... </span><span class="nc">U</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="n">TRet</span> <span class="n">PostMessage</span><span class="p">(</span><span class="n">U</span><span class="o">&amp;&amp;</span><span class="p">...</span> <span class="n">u</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 获取对应的函数 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">auto</span> <span class="n">refFunc</span> <span class="o">=</span> <span class="n">GetGlobal</span><span class="p">(</span><span class="s">&#34;g_post&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">refFunc</span><span class="p">.</span><span class="n">isFunction</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">luabridge</span><span class="o">::</span><span class="n">LuaRef</span><span class="p">(</span><span class="n">L</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// 生成携带所有参数的 functor </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">auto</span> <span class="n">func</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">bind</span><span class="p">(</span><span class="n">refFunc</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">forward</span><span class="o">&lt;</span><span class="n">U</span><span class="o">&gt;</span><span class="p">(</span><span class="n">u</span><span class="p">)...);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// implCallGlobal() 实现略, 使用 try/catch 处理错误,并把返回值转回需要的类型 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nf">implCallGlobal</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">func</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>有了这样的接口,就可以在 C++ 这边用下面的方式去调:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="c1">// C side </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">PostMessage</span><span class="p">(</span><span class="n">MsgType_A</span><span class="p">,</span> <span class="s">&#34;foo&#34;</span><span class="p">,</span> <span class="s">&#34;bar&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">PostMessage</span><span class="p">(</span><span class="n">MsgType_B</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mf">0.25f</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">(</span><span class="s">&#34;std::string goes as well.&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="c1">// 任意的参数组合... </span></span></span></code></pre></td></tr></table> </div> </div><p>而在 Lua 端的队列里,就可以得到</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- lua side</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="n">id</span><span class="o">=</span><span class="n">MsgType_A</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;foo&#34;</span><span class="p">,</span> <span class="s2">&#34;bar&#34;</span> <span class="p">}</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="n">id</span><span class="o">=</span><span class="n">MsgType_B</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">{</span><span class="mi">100</span><span class="p">,</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;std::string goes as well.&#34;</span> <span class="p">}</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="c1">-- args 表内可以容纳传过来的任意参数 </span> </span></span></code></pre></td></tr></table> </div> </div><p>对于特定的消息类型,Lua 只需检测自己关心的参数是否匹配即可。 这样从某种程度上把动态语言的灵活性延伸到了宿主语言。</p> <hr> <p>而反过来 Lua 以任意参数化的方式调 C++ 就稍麻烦一点,因为 C++ 本质上是静态的,函数的参数类型需要在编译时完全确定。</p> <p>我们可以这么做:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- 在 Lua 端简单封装一下</span> </span></span><span class="line"><span class="cl"><span class="kr">function</span> <span class="nf">g_post_native</span><span class="p">(</span><span class="n">msgID</span><span class="p">,</span> <span class="p">...)</span> </span></span><span class="line"><span class="cl"> <span class="n">native.post</span><span class="p">(</span><span class="n">msgID</span><span class="p">,</span> <span class="p">{...})</span> </span></span><span class="line"><span class="cl"><span class="kr">end</span> </span></span></code></pre></td></tr></table> </div> </div><div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="c1">// C side </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">int</span> <span class="nf">Post</span><span class="p">(</span><span class="kt">int</span> <span class="n">msgID</span><span class="p">,</span> <span class="n">luabridge</span><span class="o">::</span><span class="n">LuaRef</span> <span class="n">args</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 这里的 switch 可以用 template &lt;int N&gt; 来避免分支处理,并消除每一个 case 内重复的代码。具体实现暂略,这里为了清晰直接手写 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">switch</span> <span class="p">(</span><span class="n">msgID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nl">MsgA</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">auto</span> <span class="n">t</span> <span class="o">=</span> <span class="n">tuple_cast</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span><span class="p">(</span><span class="n">args</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">ProcessA</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">),</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">));</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nl">MsgB</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">auto</span> <span class="n">t</span> <span class="o">=</span> <span class="n">tuple_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">float</span><span class="p">,</span> <span class="kt">float</span><span class="o">&gt;</span><span class="p">(</span><span class="n">args</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">ProcessB</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">),</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">),</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">2</span><span class="o">&gt;</span><span class="p">(</span><span class="n">t</span><span class="p">));</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">FAILED_BAD_ID</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这里使用 tuple_cast 的好处是把所有的类型转换重复代码收拢到一处,对自定义类型的扩展也很容易。 tuple_cast() 函数本质上是把一个 LuaRef 根据期望类型(由模板参数指定)展开成一个 std::tuple,对于任何一组给定的类型,递归地在编译期完成展开。具体的技术在之前的 blog 中有提到,这里不再赘述。</p> <p>好了,现在可以在 Lua 端这样调了:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- lua side</span> </span></span><span class="line"><span class="cl"><span class="n">g_post_native</span><span class="p">(</span><span class="n">MsgType_A</span><span class="p">,</span> <span class="s2">&#34;foo&#34;</span><span class="p">,</span> <span class="s2">&#34;bar&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">g_post_native</span><span class="p">(</span><span class="n">MsgType_B</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mf">12.5</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>然后在 C++ 端直接定义接受明确参数列表的函数</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="c1">// C side </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">int</span> <span class="nf">ProcessA</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">s1</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">s2</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">ProcessB</span><span class="p">(</span><span class="kt">int</span> <span class="n">arg1</span><span class="p">,</span> <span class="kt">float</span> <span class="n">arg2</span><span class="p">,</span> <span class="kt">float</span> <span class="n">arg3</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>这样的最大好处是,不管是写脚本的脚本程序员,还是写宿主语言的工程师,都可以以各自语言习惯的方式去写,尤其是 C++ 端程序员,总是可以用 tuple_cast 转成自己期望的参数列表,让所有的接口函数做到 self-documenting。</p> 2016.05 时之沙 - 我对时间的理解和领悟 https://gulu-dev.com/post/2016-05-07-sand-of-time/ Sat, 07 May 2016 09:54:00 +0000 https://gulu-dev.com/post/2016-05-07-sand-of-time/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-05-07-sand-of-time/sand.jpg" alt="Featured image of post 2016.05 时之沙 - 我对时间的理解和领悟" /><p>这是一篇看过 <a class="link" href="https://www.zhihu.com/people/yu-gu-47" target="_blank" rel="noopener" >@顾煜</a> 老师的 <a class="link" href="https://zhuanlan.zhihu.com/p/20824083" target="_blank" rel="noopener" >信息过载</a> 和 <a class="link" href="https://www.zhihu.com/people/ChenZhangyu" target="_blank" rel="noopener" >@陈章鱼</a> 老师的 <a class="link" href="https://zhuanlan.zhihu.com/p/20858666" target="_blank" rel="noopener" >独孤求败的剑冢,和时间管理的书单</a> 后有感的 reblog,主题可能稍有呼应但略有不同,不管啦~~</p> <hr> <p>工作十年来,我在看待自己的时间上,大致有下面的三个阶段:</p> <h3 id="唯快不破">唯快不破</h3> <p>刚毕业没几年的自己,飞扬跳脱,总会试着以最快的速度完成手头的任务,正如对代码的优化那样,对时间管理效率的追求没有止境。简单地说,没有最快,只有更快。什么,你说实现这个系统只需要一周?No,我只需要三天。这个阶段的我,对时间的认识就像是待挤的海绵——时间就像xx,挤一挤总是有的,对吧~~</p> <h3 id="分拣梳理">分拣梳理</h3> <p>后来我意识到,快是有代价的。当你在高速公路上开的越来越快时,两旁飞快重复和闪退的街景会降低你的反应,你的视野会变得越来越狭窄,如果不能学会控制和驾驭,那么翻车是迟早的。</p> <p>在这个阶段,我学会了把不同的事情区分开,梳理暗流和分岔,在工作和生活中保持稳定的节奏。要同时面对多个任务?没关系,良好的隔离让它们不会互相影响,顾此失彼。处理不好工作和生活的平衡?没关系,上班前旋转起你的“陀螺”,下班时别忘了清理你的“回收站”。</p> <h3 id="顺其自然肆意流淌">顺其自然,肆意流淌</h3> <p>再后来,三十岁慢慢近了。</p> <p>一直以来,为了“醒转来生活(wake-up-and-live)”,我一直是“工作和生活应当很好地分离”的拥护者,认为工作时间和私人生活不应互相侵占。后来我逐渐领悟到,如果说生活是一幅不断变动的大油画,那么工作应该是有机地融入这幅大油画中的一个角落——这个角落时而喧嚣,时而静谧,但总会自然地以相匹配的节奏和步调与整幅油画相呼应。可以说,正是在我学会不去刻意地割裂工作和生活之后,把时间之河看作是浑然一体的流淌,才真正感受到真实生活的呼吸。</p> <p>当有多个不同的 commitment (没想好中文的对应词是啥) 时,我已不再像以前那样一定要把这些任务,目标或承诺“从物理上”隔离开,像学生上课或者番茄时钟那样固定时间切换以换取某种“节奏感”和“效率”,而是允许它们以自己的节奏自由地流动。在我头脑的时间之河里,每一滴流动的水都不再像以前那样刻意地约束,限制和“被管理”,它们可以有自己的速度和节奏,就像花开花谢那样,自然地产生和消亡。</p> <p>我现在已经可以做到,在全神贯注完成一项任务时,在某个时刻,直接跳转并开始另一件完全无关的事,而没有心理的波动和切换的负担,在某个时间点上再切回去自然地继续。而最关键和神秘的是,我自己也根本说不出这种任务切换的原则或时机,这一切只是自然而然地发生。</p> <p>比如我这篇小短文,就是在早上到达公司后一个小时内写下的。虽然清晨我已理好今天的工作节奏,但我现在已经不再需要努力去克制和保持这种节奏,也不再担心和惧怕“随时倾听内心的声音”会打乱这种节奏,而只是顺应自己真实的状态。当我上楼梯想到这些时,就打开电脑把这些文字写下来,此之谓“顺其自然,肆意流淌”。</p> <hr> <p>能有相对宽松的工作和生活环境,是允许这一切发生的基础。这正是源于我两年前从上海搬家到珠海的决定,更是我能够有机会在西山居这样的能够允许我自然随性甚至肆意任性的组织里工作的幸运。在游戏行业,虽然还没有什么值得一提的工作成果,但说到幸运,我觉得自己还蛮幸运的 (此处应腆着脸笑两声)~~</p> <p>PS: 有同学问我周六来公司,原因是这个周六合并到五一假期了 :)</p> 2016.04 《京华烟云》 摘录 https://gulu-dev.com/post/2016-04-24-moment-in-peking/ Sun, 24 Apr 2016 19:39:00 +0000 https://gulu-dev.com/post/2016-04-24-moment-in-peking/ <p><img src="https://gulu-dev.com/post/2016-04-24-moment-in-peking/2016-04-24-moment-in-peking.jpg" width="396" height="592" srcset="https://gulu-dev.com/post/2016-04-24-moment-in-peking/2016-04-24-moment-in-peking_hu303a375bc1298eddd357ad88b409a9de_35580_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2016-04-24-moment-in-peking/2016-04-24-moment-in-peking_hu303a375bc1298eddd357ad88b409a9de_35580_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="title" class="gallery-image" data-flex-grow="66" data-flex-basis="160px" ></p> <p>昨天在飞机上读完《京华烟云》,余味掩卷不散,顺手整理了一下笔记。</p> <hr> <h2 id="关于京华烟云-林语堂长女林如斯">关于《京华烟云》 (林语堂长女林如斯)</h2> <p>(节选)</p> <p>&hellip;我知道父亲每晨著作总是起来走走吃吃水果,当他写完红玉之死,父亲取出手帕擦擦眼睛而笑道:“古今至文皆血泪所写成,今流泪,必至文也。”有情感又何妨。</p> <p>《京华烟云》在实际上的贡献,是介绍中国社会于西洋人。几十本关系中国的书,不如一本道地中国书来得有效。关于中国的书犹如从门外伸头探入中国社会,而描写中国的书却犹如请你进去,登堂入室,随你东西散步,领赏景致,叫你同中国人一起过日子,一起欢快,愤怒。</p> <p>&hellip;你一翻开来,起初觉得如奔涛,然后觉得幽妙,流动,其次觉得悲哀,最后觉得雷雨前之暗淡风云,到收场雷声霹雳,伟大壮丽,悠然而止。留给读者细嚼余味,忽恍然大悟;何为人生,何为梦也。而我乃称叹叫绝也!未知他人读毕有此感觉否?故此书非小说而已!或可说,“浮生若梦”是此书之主旨。</p> <p>&hellip;此书的第三部题为“秋季歌声”(即第三个题目),取庄周“臭腐化为神奇,神奇化为臭腐”,生死循环之道为宗旨:秋天树叶衰落之时,春已开始,起伏循环,天道也。故第三卷描写战争,可谓即描写旧中国的衰老,就是新中国的萌芽。故书中有“晚秋落叶声中,可听出新春的调子,及将来夏季的强壮曲拍”等语。</p> <p>&hellip;木兰的生活变迁,也很值得研究:从富家生长享用一切物质的安适,后变为村妇,过幽雅山居的生活,及最后变为普通农民,成为忍苦,勇敢,伟大的民众大海中的一滴水。父亲曾说:“若为女儿身,必做木兰也!”可见木兰是父亲的理想女子。</p> <p>一九三八的春天,父亲突然想起翻译《红楼梦》,后来再三思虑而感此非其时也,且《红楼梦》与现代中国距离太远,所以决定写一部小说。最初两个月的预备全是在脑中的,后来开始打算,把表格画得整整齐齐的,把每个人的年龄都写了出来。几样重要事件也记下来。自八月到巴黎时动笔,到一九三九年八月搁笔。其中搬迁不算,每晨总在案上著作,有时八页,有时两页,有时十五页,而最后一天共写了十九页,成空前之纪录。其中好多佳话或奇遇,都是涉笔生趣,临文时杜撰出来的。</p> <p>父亲不但在红玉之死后挥泪而已,写到那最壮丽的最后一页时,眼眶又充满了眼泪,这次非为个人悲伤而掉泪,却是被这伟大的民众所感动,眼泪再收也收不住了。作者写得自己哭了,怎么会叫读者忍着眼泪咽下去呢?</p> <p>《京华烟云》是一本可以随时翻看的小说,并不是一定要有闲时才看,最好是夜阑人静时独自个儿看;困倦时,起来喝口清茶自问道:“人生人生,我也是其中之一小丑否?”</p> <hr> <h2 id="京华烟云简短介绍">《京华烟云》简短介绍</h2> <p>《京华烟云》是林语堂用英语写作的一部长篇小说。它一直被视为阐述东方文化的权威著述。&hellip;在创作方法上,运用了社会全景图式法&hellip;展示了一幅漫长的历史画卷,起自1901年的义和团运动,终于抗日战争爆发之时,讲述了中国两大家族长达四十年的兴衰荣辱。书中还描写了从义和团运动、到八国联军入侵、到辛亥革命和五四运动、到军阀混战、到北伐战争、到全民抗日其间的重大事件,以及蕴含在这些重大事件背后的中华民族的文化传统,包括政治、经济、哲学、宗教、文学、艺术、民俗等。</p> <p>《京华烟云》每卷引一段庄子的语录为题旨,传达出浓厚的道家思想。道家思想既是小说的血脉,还形成了小说的结构,从道家的天命观——得道的途径——道之为用三个递进的层次解读小说。既有道家超凡脱俗、淡泊人生的理想,又有儒家忧国忧民、兼济天下的责任感;既推崇西方的科学、文化,又对资产阶级自由、平等、博爱把以极大的兴趣。</p> <p>《京华烟云》书写了生活的和谐与恬淡,生命的超脱与自由自在。整部书中充溢着浓厚的道家文化,平缓自然、从容豁达。让读者在淳朴、宁静和芬芳的基调中,在平和安详、自尊自爱的文化氛围中感受道家文化的无穷魅力。展示了中国特有的文化意境和中国传统的儒道哲学思想,向西方人展示了战乱年代的中国社会生活、丰富多彩的文化及其宗教思想。</p> <p>《京华烟云》小说内容博大精深,感情真切自然,品格优雅含蓄。在哲学精神方面,《京华烟云》以庄周哲学统领全书,其中也穿插着中庸之道的儒学与万物平等的佛学,主要表达的是“一切人生浮华皆如烟云“的道学思想,强调了人的永生是种族的延绵,新陈代谢是世间万物永恒的真谛。</p> <hr> <h2 id="京华烟云-摘录">《京华烟云》 摘录</h2> <hr> <blockquote> <p>福气不是自外而来的,而是自内而生的。一个人若享真正的福气,或是人世间各式各样儿的福气,必须有享福的德行,才能持盈保泰。在有福的人面前,一缸清水会变成雪白的银子;在不该享福的人面前,一缸银子也会变成一缸清水。</p> </blockquote> <hr> <blockquote> <p>桂姐问:“你说什么?你到底醒了没醒?” 曼娘说:“你捏我。”桂姐依话捏她。曼娘觉得微微一疼,自言自语说:“这次大概真醒过来了。” “你刚才梦见什么了?你刚才跟人说话,跟人辩论,说你没有做梦,说那个人是做梦。” “我梦见我做了一个怪梦……后来由第二个梦中醒来,回到第一个梦里,那时火还没灭,地上还有雪……噢,我都糊涂了!”</p> </blockquote> <hr> <blockquote> <p>木兰曾听见父亲说:“心浮气躁对心神有害。”他的另一项理由是:“正直自持,则外邪不能侵。”在木兰以后的生活里,有好多时候儿她想起父亲这句话来,这个道理竟成了她人生的指南,她从中获得了人生的乐观与勇气。</p> </blockquote> <hr> <blockquote> <p>木兰的父母还不知道究竟怎样安排她的将来,她父亲则更无定见。道家总是比儒家胸襟开阔。儒家总认为自己对,道家则认为别家对,而自己也许会错。所以非正统派的姚思安对西洋思想没有偏见,甚至于对自己女儿的婚事也提到自由结婚,就是由当事人男女自己决定,这正合乎道家的“道法自然”的道理。他认为把青年男女的婚姻付之于不加深思熟虑的青年的盲目冲动,这种西洋的想法极微妙而深奥,正像道家的道理一样。</p> </blockquote> <hr> <blockquote> <p>在北京,人生活在文化之中,却同时又生活在大自然之内,城市生活极高度之舒适与园林生活之美融合为一体,保存而未失,犹如在有理想的城市,头脑思想得到刺激,心灵情绪得到宁静。&hellip; 在北京城的生活上,人的因素最为重要。北京的男女老幼说话的腔调儿上,都显而易见的平静安闲,就足以证明此种人文与生活的舒适愉快。因为说话的腔调儿就是全民精神上的声音。</p> </blockquote> <hr> <blockquote> <p>道教精义和科学,是姚大爷的两大爱好。在他的头脑里,这两种思想是十分协调融和的。这也许是很自然之理,因为道家思想注重自然,而儒家思想则最注重人事,注重文化,注重历史。道教中伟大的哲学家庄子,感觉到自然对人的魔力,自然中四季无终止的运行,自然中生长衰微的法则,自然中万物之纷杂无穷的类别,以及自然中难以言喻的神秘。自然界这个宇宙,在矛盾冲突的多个力量之中,遵守着一个无关于个人的、无以名之的、默默无言的神祇所定的法则,而变迁,而变化,而相互作用,相互影响。这个默默无言的神祇,根本实在无以名之,而道家只好名之曰“道”,却又坚持这个道,本来无名,又不可以以任何名字相称。就是说,所谓“道”,用什么名字相称也是不适当的。姚先生的想法是,西方的科学现在正窥启自然的奥秘。</p> </blockquote> <hr> <blockquote> <p>曼娘说:“不过这个也不能叫什么特别。素云也不见得怎么好。她的两个哥哥,也是北京最坏的恶少,放荡无耻,玩弄女人。那样人家儿若能把财产保得久,老天爷就没长眼了。我要把眼睛睁得大大的,看看他们怎么个下场。”木兰说:“我爸爸常常告诉我,他曾经亲眼看见多少贫穷之家兴起来,多少富贵之家衰下去。他告诉我说,最重要的事就是不要依赖着金钱。人应当享受财富,也要随时准备失去了财富时应当怎么过日子。”</p> </blockquote> <hr> <blockquote> <p>木兰的父亲对钱已经看得很开,大把花钱,没有比嫁一位掌上明珠更慷慨了。这就是人在福中要享福,莫在福后空回想。财富,在黑暗天空中放出的烟火,看来是霞光万道,光彩耀目,结果只是烟消光散,黑灰飘落,地上留下些乌焦的泥巴烟花座子而已。</p> </blockquote> <hr> <blockquote> <p>有一次,银幕上演一个去交际的妇女,穿上夜礼服要出去参加宴会时,台下一个老绅士从座位上立起来,向观众大声说:“看那些洋女人!上半身儿满满的,却毫不遮盖;下半身儿空空的,却偏要遮盖。在上边儿,没褂子;在下边儿,没裤子!”观众吼声雷动。 一个洋人在后喊叫:“Quiet!”叫观众静下来。出乎洋人的意料,这位中国老绅士不但懂他的英文,而且转过身去,用漂亮的英文把刚才说的中国话的意思说了一遍。洋人大惊,也因老人妙语诙谐而大笑。北京的洋人,后来渐渐知道这位老哲学家叫辜鸿铭,提到他都肃然起敬,无限仰慕,这反而更鼓励起这位老人加甚揶揄西洋文明。</p> </blockquote> <hr> <blockquote> <p>不过姚思安虽然对这个红尘世界又回心转意,不可解的是有点儿缺乏信心。这位原先存心出家的人,现在又开始以满腔热情来享受人生,简直像是腾云驾雾恣情遨游一般。可以说他是半在尘世半为仙。由于他的研读道家典籍和静坐修炼,他已经达到道家的物我两忘之境。因为家就是“自我”的扩大,所以他对家也就失去了真正信赖。由于这种态度,他就越能享受人生,只要他这份儿非一般富人所能拥有的财富能存在一天,他也就能享受其财富。他自然也不把自己的财富看得有什么重要。</p> </blockquote> <hr> <blockquote> <p>立夫已经和红玉很熟识了。一天,立夫对莫愁说了一句怪话:“宇宙之中,应当有六行,不只是五行。红玉应属于玉。她由皮到骨都是玉的,纯洁、高傲、坚硬、脆弱易碎。” 莫愁说:“身为玉质,有利也有弊。玉永远不受污染,并且硬而脆。但是最精美的玉应当发柔和之光。你看她硬是不肯讨我父母的欢心,是不是?”</p> </blockquote> <hr> <blockquote> <p>木兰说:“这就是你爱得太深了。爱是永远不能封口儿的创伤。女人爱别人的时候儿,一定会觉得自己失去了什么,那是她心灵的一部分,她于是各处去寻找失去的那部分灵魂,因为她知道,若不去找到,自己便残缺不全,便不能宁静下来。只有和自己的意中人在一起时,才又完整如初;但是自己的意中人一旦离开,自己又失去意中人携走的那一部分,那就直到重新和意中人团聚时,才又得到安宁。”</p> </blockquote> <hr> <blockquote> <p>这个默默无言的黑暗的岩石,在高山日落的时候,横压在立夫和木兰的心头,那块巨大的石碑,是向人类文化历史坚强无比的挑战者。立夫说:“你记得秦始皇怕死,派五百童男童女到东海求长生不死之药吗?而今物在人亡。” 木兰说出谜一般的话:“因为石头无情。” 这时暮霭四合,黑暗迅速降临,刚才还是一片金黄的云海,现在已成为一片灰褐,遮盖着大地。游云片片,奔忙一日,而今倦于漂泊,归栖于山谷之间,以度黑夜,只剩下高峰如灰色小岛,于夜之大海独抱沉寂。大自然也日出而作,日入而息。这是宇宙间的和平秩序,但是这和平秩序中却含有深沉的恐怖,令人凛然畏惧。</p> </blockquote> <hr> <blockquote> <p>立夫问:“他们现在提倡那些幼稚的东西,您认为有道理吗?他们甚至连祖先崇拜都攻击。他们要把所有旧的一扫而空。他们甚至把‘贤妻良母’都骂作是阻碍妇女发展独立的低落观念!” 姚先生说:“让他们去做。他们主张的若是对,自然会有好处;若是错,对正道也没有什么害处。实际上,他们错的偏多,就犹如在个人主义上一样。不用焦虑,让他们干到底吧。事情若是错,他们过一阵子也就腻了。你忘记《庄子》了吗?没有谁对,也没有谁错。只有一件事是对的,那就是真理,那就是至道,但是却没有人了解至道为何物。至道之为物也,无时不变,但又终归于原物而未曾有所改变。” 这位老人的眼睛在眉毛下闪亮,他犹如一个精灵,深知长生不朽之秘一样。</p> </blockquote> <hr> <blockquote> <p>在餐饮之际,少男少女,错杂共座,对于爱情,对于政治,大家畅所欲言,杂以打趣诙谐。姚思安先生对在他的花园之中这种谈情说爱的场面,完全以特别的宽容处之。他一生最后的本分,就是看着阿非娶得佳偶。他对红玉的健康颇为焦虑,恐怕他瞑目之后,红玉不能和阿非白头偕老。所以他对于他俩的订婚,始终没有采取什么明确的步骤,但是他也并不去阻拦。这位道家姚先生完全是静观情势的自然演变,顺从自然之道。</p> </blockquote> <hr> <blockquote> <p>关于木兰和莫愁,巴固以他高度诗般的风格告诉了辜鸿铭先生。他说:“木兰的眼睛长长的,莫愁的眼睛圆圆的。木兰的活泼如一条小溪,莫愁的安静如一池秋水。木兰如烈酒,莫愁似果露。木兰动人如秋天的林木,莫愁的爽快如夏日的清晨。木兰的心灵常翱翔于云表,莫愁的心灵静穆坚强如春日的大地。”</p> </blockquote> <hr> <blockquote> <p>我在人世对这个家的职责,已然完了。我在你母亲去世时为什么一滴眼泪也没流,你们大概会纳闷儿。一读《庄子》,你们就会明白。生死,盛衰,是自然之理。顺逆也是个人性格的自然结果,是无可避免的。虽然依照一般人情,生离死别是难过的事,我愿你们要能承受,并且当做自然之道来接受。你们现在都已经长大成人,对人生要持一个成人的看法。你们若在人生的自然演变方面能看得清楚,我现在就要告诉你们的事情,你们也不会太伤心。&hellip; 我告诉你们,我就要出外云游了。大家谁也不用掉眼泪。你母亲的丧事一完,阿非和宝芬也出发往英国去之后,我就要离开你们。不用伤心。世界上,没有父母会跟儿女一辈子的。十年后,我若还活着,我会回来看你们。不要想法子去找我,我会回来找你们。 “你们曾听见有人离家去当隐士。世人对人生只有两个态度:入世,出世。不要怕这两个名词。我和你母亲和你们,已经在一起生活了多年,看着你们长大,美满地结了婚。我们已经过得很快活,也尽了人生的本分。现在我可要松松心了。不要以为我去修仙。我若给你们讲些道理,也许你们不能懂。我要出外,是要寻求我真正的自己。寻求到自己就是得道,得道也就是寻求到自己。你们要知道‘寻求到自己’就是‘快乐’。我至今还没有得道,不过我已经洞悟造物者之道,我还要进一步求取更深的了悟。红玉自己有了她独特的了解。你们要想她的好处。阿非,记住,她的死是为了让你快乐。除去至道,谁能注定事情会这样演变呢?”</p> </blockquote> <hr> <blockquote> <p>立夫对木兰用戏剧式的努力使他从监狱里获得释放,他也只用普通道谢的客套话表示谢意而已。但是后来他思索那冒险的含义,他的感受很深。他想起了木兰和他单独在监狱的夜晚木兰所说的话,那是在去见王司令官之前。木兰说:“我会不惜更大的牺牲救你的命。”万一王司令若像那奉军司令之对付高教授太太,那该怎么办?木兰会不会牺牲了她的贞洁救他的命呢?木兰,他知道,一向不受习俗的思想的拘束,也许她会不惜一切!这个问题自然不能问,只好藏在自己心里。他记忆中那伟大的爱情的考验,他无法摆脱,那爱情变了形,成了他感情的动力,倾注在学术研究上。</p> </blockquote> <hr> <blockquote> <p>姚老先生从容微笑说:“在华山我从一只老虎前面经过,我望了望它,它望了望我,它偷偷溜走了。我告诉你们,孩子,我这旅行,一半是游山玩水观赏风景,一半是自我求解脱。这两个目的是不可分的。也许你们不明白。自我解脱的基础在于身体的锻炼。人必须无钱无忧虑,随时死就死。这样你才能像个死而复生的人一样云游四方。你要把每一天,每一刹那都当做苍天赐予的,你必须感谢上苍。你身上不带钱,则盗贼不近身。但是你不能这样子旅行,那就必须把身体锻炼好——你的手,你的脚,最重要是你的胃。必须能够找到什么吃什么,或者能挨饿,不吃东西。必须室内室外都可以睡觉,不管什么天气都能忍受。你若没有这么一个身体,就不能旅行。”</p> </blockquote> <hr> <blockquote> <p>木兰问:“爸爸,你信不信人会成仙?道家都相信人会成仙的。” 父亲说:“完全荒唐无稽!那是通俗的道教。他们根本不懂庄子。生死是自然的真理。真正的道家会战胜死亡。他死的时候儿快乐。他不怕死,因为死就是‘返诸于道’。你记得庄子临死的时候儿告诉弟子不要葬埋他吗?弟子们怕他的尸体会被老鹰吃掉。庄子说:‘在上为鸟鸢食,在下为蝼蚁食。夺彼与此,何其偏也?’至少在我的丧礼上,我不愿请和尚来念经。” 木兰听见父亲引证《庄子》时微弱的笑声,很受感动,也颇觉意外。木兰说:“那么您不相信人的不朽了?” “孩子,我信。由于你、你妹妹、阿非,和你们所生的孩子,我就等于不朽。我在你们身上等于重新生活,就犹如你在阿通、阿眉身上之重新得到生命是一样。根本没有死亡。人不能战胜自然。生命会延续不止的。”</p> </blockquote> <hr> <p>更多的摘录和笔记,可以看<a class="link" href="https://github.com/mc-gulu/gl-sdream/tree/master/post-reading" target="_blank" rel="noopener" >这里</a>的 PDF 档 ([2016-04-24]《京华烟云》摘录.pdf)。有趣的书评里,<a class="link" href="https://book.douban.com/review/2084081/" target="_blank" rel="noopener" >这一篇</a>可以一读。</p> 2016.04 和孩子一起长大 https://gulu-dev.com/post/2016-04-20-grow-up-with-your-children/ Wed, 20 Apr 2016 07:32:00 +0000 https://gulu-dev.com/post/2016-04-20-grow-up-with-your-children/ <p>此文写于大约两年前 (2014-07-13),整理旧文档时发现的。</p> <hr> <p>和孩子一起长大</p> <p>不知不觉,儿子已经三岁半了。随着他的日渐成长,我越来越觉得有必要,把成长过程中一些经验,教训和弯路记录下来,也算是一份小小的成长记录吧。</p> <p>跟我小时候“略微内向腼腆,对外界以顺应为主”不同,想想是一个倔强,顽皮, 颇有主见的孩子。记得半年前还在上海的时候,有一次带他去那种充气的儿童乐园玩,里面是各种充气的小房子和滑梯。想想和比他高一个头的孩子们一起爬充气滑梯,那些孩子三两下就爬上去了,他折腾了半天还在原地。我在旁边看了一会儿,想伸手帮帮他,结果他果断地把我的手推开,一脸“我就不信这个邪”的表情,在那儿满头大汗地扑腾,各种姿势各种摔倒,十五分钟后,终于全身湿透地站在了滑梯顶上。我瞬间眼泪就出来了。</p> <p>有时,这种倔强会显得很任性。作为家长,什么时候应该鼓励,什么时候应该劝阻,甚至制止,中间的界限很微妙,大部分时候都很难找到一个简单的原则去处理。不过我发现,只要能始终做到平等沟通和耐心引导,很多时候就能妥善化解,如果做不到这两点,往往就会以一场哭闹收尾。以前在小足迹上托班的时候,每天放学一定要到地下车库去看车,坐电梯一定要自己按楼层按钮,坐车一定要自己关车门,都是这样。很多生活琐事的处理看似影响不大,其实知易行难,细究起来,真实生活从来都由不得照搬书上的最佳实践。</p> <hr> <p>作为父母,我们在成年人的世界里待得太久,经常会在不经意间忘了,孩子眼中的世界,跟我们看到的大不一样。有的时候,认真地同他讲话,他未必会静静聆听;而不经意的言辞和举动,也许孩子会按自己的逻辑做出完全预想不到的解释。所以要想与孩子真正平等地相互理解和沟通,就得放下教条和成见,以伙伴的方式跟他讲话。</p> <p>有一点我们做的还不够,就是给孩子足够的社交经验。现在想想对“什么是自己的 / 什么是别人的 /什么信息需要积极响应”这些都还没什么程度上的把握,用书上的话讲就是“注意力的分散度偏弱 / 反应阀值较高”。表现在实际生活里,就是“经常使用命令的口吻而不是征询的语气 / 有时会很专注,沉浸在自己的世界里,对周遭的变化和他人的互动不够敏感”这些是我们需要注意着重改善的地方。</p> <p>带一个孩子长大的一个非常有趣的地方是,有机会观察一个人的人格是如何在幼小的时候一点一滴形成的。想想现在已经有了一点自己的偏好和品味了,有一次妈妈给他理发,剃了一个接近光头的短发,结果他盯着镜子里的自己,半响冒出来一句“我不喜欢这个头发,我不要是光头”安抚了半天才接受了自己的新造型。穿衣服也开始挑了,有一次给他拿的裤子他不要穿,就在原地一边跳一边嚷嚷“我不要黑色的,我不喜欢黑色的,我要穿橙色的,就是那个 orange ”。</p> <hr> <p>也许是我小时候接触的国学启蒙太少,现在可以有机会跟自己的孩子一起重新启蒙一下,觉得很开心。有段时间每天回来都跟想想一起读三字经,两个人都很投入。有一次问他“幼不学老何为”什么意思,他答“小的时候不好好学习,就会变老了”乍一听不通,再想了想又觉得比“老了可怎么办”深刻得多。</p> <p>如果说几年来,初为人父的我积累了什么心得的话,我想其实就是弄明白了一个道理:在一个家庭里,父母是和孩子一起成长的。在跟孩子的对话中,我逐渐明白过来,所谓“教育”,并非一成不变的单向输送,而是父母和子女之间,一个互相理解,相互影响的过程。在这个过程里,不仅孩子在成长,父母也在成长。来自长辈和书本的经验再多,也不能替代真实的互动和感受。</p> <hr> <p>有一次带想想在新加坡的樟宜机场,妈妈在购物,我们俩在行人休息处玩,我看他有点无聊,就说 “妈妈还要逛一会儿,咱俩来玩转圈吧~~”</p> <p>想想用夸张的语调说 “转圈?~大~本~爷~没~时~间~陪你转圈!!!” ~~~</p> <p>当时我就愣住了~</p> <p><img src="https://gulu-dev.com/post/2016-04-20-grow-up-with-your-children/image.png" width="650" height="417" srcset="https://gulu-dev.com/post/2016-04-20-grow-up-with-your-children/image_hu014c71ed6e922ce75cc8ca9f7e463e64_40962_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-20-grow-up-with-your-children/image_hu014c71ed6e922ce75cc8ca9f7e463e64_40962_1024x0_resize_box_3.png 1024w" loading="lazy" alt="image" class="gallery-image" data-flex-grow="155" data-flex-basis="374px" ></p> <p>过了一会儿才想明白,肯定是跟光头强学的——“本大爷”被他记反了……</p> <p>PS. 这个梗现在已经被我们玩坏了,现在我想寻他开心就会说:“大本爷晚上想吃点儿啥?”</p> 2016.04 使用 Premake 自动化 Android 编译脚本的维护 https://gulu-dev.com/post/2016-04-17-premake-for-android/ Sun, 17 Apr 2016 15:27:00 +0000 https://gulu-dev.com/post/2016-04-17-premake-for-android/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-04-17-premake-for-android/premake0.png" alt="Featured image of post 2016.04 使用 Premake 自动化 Android 编译脚本的维护" /><h2 id="需求">需求</h2> <p>在使用 ndk 做 Android 开发时,一件比较麻烦的事情是编译脚本 (也就是 Android.mk 和 Application.mk ) 的维护工作。今天尝试了一下把这个维护的工作自动化,完成以后简单记录一下。</p> <p>在日常工作里我尝试过不少生成脚本的自动化工具,用下来觉得最方便的是 <a class="link" href="https://premake.github.io/" target="_blank" rel="noopener" >Premake</a>,这个工具的好处是 Lua 语法,跟 cmake 之类的脚本相比,非常清晰易读。我用 C++ 写的库,大多是用这个工具生成工程文件 (sln/vcxproj) 的,这样的生成两大好处:</p> <ol> <li>可以使工程文件与磁盘上的目录结构自动对应。</li> <li>所有的编译选项清楚地列在一个简短的 Lua 内,维护时不容易出错。</li> </ol> <p>这一次我的目标就是使用 Premake 把 Android.mk 和 Application.mk 这两个脚本的维护自动化,最好能做到一键生成 vs2013/android 两个平台对应的编译脚本。</p> <hr> <h2 id="make-it-run">Make it run</h2> <p>从<a class="link" href="https://github.com/premake/premake-core/wiki/Modules" target="_blank" rel="noopener" >这个页面</a>可以看到,Premake 没有原生对 android ndk 的支持,但在 Third-Party Modules 里可以看到两个相关的包,一个是 TurkeyMan(Manu Evans) 的 <a class="link" href="https://github.com/TurkeyMan/premake-android" target="_blank" rel="noopener" >premake-android</a>,另一个是 Meoo(Bastien Brunnenstein) 的 <a class="link" href="https://github.com/Meoo/premake-androidmk" target="_blank" rel="noopener" >premake-androidmk</a>,这一次我们使用后者。</p> <p>阅读 Premake 的文档 <a class="link" href="https://github.com/premake/premake-core/wiki/Using-Modules" target="_blank" rel="noopener" >Using Modules</a> 可以知道,使用第三方模块本质上就是一个普通的 require,我们 git clone 到本地后,按照 <a class="link" href="https://github.com/Meoo/premake-androidmk" target="_blank" rel="noopener" >README</a> 这样照葫芦画瓢写个例子测试一下吧。</p> <p>写完以后发现,作者在 readme 里没有写具体用什么命令来执行……翻代码之后发现正确姿势是这样的:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">premake5.exe --file=premake_android.lua androidmk </span></span></code></pre></td></tr></table> </div> </div><p>运行后—— duang~~ 得到一个 Lua 报错,大致是说几个期望的表为 nil,翻代码看了一下,是几个实际上无需指定的 optional 参数,作者提供的例子里也没有指定。那么把代码 fork 到本地修一下吧。 (<a class="link" href="https://github.com/mc-gulu/premake-androidmk/commit/ee7cf5a9658d4fa0fbb274d7c18d693074703f89" target="_blank" rel="noopener" >这里</a>) 是对应的修复 commit。</p> <p>再次运行就一切正常了。</p> <p><img src="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake1.png" width="459" height="150" srcset="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake1_hu55b0bb21fc025d439b4fb84b7825f260_5706_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-17-premake-for-android/premake1_hu55b0bb21fc025d439b4fb84b7825f260_5706_1024x0_resize_box_3.png 1024w" loading="lazy" alt="premake1" class="gallery-image" data-flex-grow="306" data-flex-basis="734px" ></p> <h2 id="注意事项">注意事项</h2> <p>需要注意以下事项,</p> <ul> <li> <p>生成脚本 (此处为 premake_android.lua) 内所有的路径为相对于生成脚本的路径,而非相对于 Android.mk 的路径,Premake 会完成路径的转换</p> </li> <li> <p>为了生成恰当的 LOCAL_MODULE_FILENAME,需要在 project 内指明 targetname</p> </li> </ul> <p><img src="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake2.png" width="281" height="90" srcset="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake2_hud347998814d1125f3854c25d13d5cb1e_2518_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-17-premake-for-android/premake2_hud347998814d1125f3854c25d13d5cb1e_2518_1024x0_resize_box_3.png 1024w" loading="lazy" alt="premake2" class="gallery-image" data-flex-grow="312" data-flex-basis="749px" ></p> <ul> <li>以下三个选项是最常用到的选项,分别是 Android.mk 的生成路径,abi 选择和 sdk 版本号的选择</li> </ul> <p><img src="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake3.png" width="246" height="68" srcset="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake3_hu3327c11a7b6a123fe479d6992d7a3039_1960_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-17-premake-for-android/premake3_hu3327c11a7b6a123fe479d6992d7a3039_1960_1024x0_resize_box_3.png 1024w" loading="lazy" alt="premake3" class="gallery-image" data-flex-grow="361" data-flex-basis="868px" ></p> <ul> <li>实际的 include $(BUILD_STATIC_LIBRARY) 在配置项 $(PM5_CONFIG) 对应的条件分支内,编译时需要指明这个配置项的值 (如下图中的情况需要在编译时指明 <code>ndk-build PM5_CONFIG=debug</code>,或在引用脚本里指明是使用 debug 还是 release )</li> </ul> <p><img src="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake4.png" width="425" height="220" srcset="https://gulu-dev.com/post/2016-04-17-premake-for-android/premake4_hu2c858661e5114198d2a20e95c162e52b_9808_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-17-premake-for-android/premake4_hu2c858661e5114198d2a20e95c162e52b_9808_1024x0_resize_box_3.png 1024w" loading="lazy" alt="premake4" class="gallery-image" data-flex-grow="193" data-flex-basis="463px" ></p> <h2 id="代码清单">代码清单</h2> <p>这里是全部的脚本清单,可以看到与同类型的工具相比,非常简洁和易于维护:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">require</span> <span class="s2">&#34;modules/premake-androidmk/androidmk&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">solution</span> <span class="s2">&#34;mici&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">configurations</span> <span class="p">{</span> <span class="s2">&#34;Debug&#34;</span><span class="p">,</span> <span class="s2">&#34;Release&#34;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">language</span> <span class="s2">&#34;C++&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">location</span> <span class="s2">&#34;android/jni&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">ndkabi</span> <span class="s2">&#34;default&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">ndkplatform</span> <span class="s2">&#34;android-19&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">filter</span> <span class="s2">&#34;configurations:Release&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">optimize</span> <span class="s2">&#34;On&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">filter</span> <span class="s2">&#34;configurations:Debug&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">optimize</span> <span class="s2">&#34;Debug&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">project</span> <span class="s2">&#34;mici_static&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">targetname</span> <span class="s2">&#34;libmici&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">kind</span> <span class="s2">&#34;StaticLib&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">includedirs</span> <span class="s2">&#34;../&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">files</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;../mici/**&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>使用以下的命令一键生成 vs2013/android 的工程文件和编译脚本:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">premake5.exe --file=premake_vs2013.lua vs2013 </span></span><span class="line"><span class="cl">premake5.exe --file=premake_android.lua androidmk </span></span></code></pre></td></tr></table> </div> </div><hr> <p>我的修复版 premake-androidmk 在<a class="link" href="https://github.com/mc-gulu/premake-androidmk" target="_blank" rel="noopener" >这里</a>维护。</p> 2016.04 我的日常一天 https://gulu-dev.com/post/2016-04-09-a-working-day/ Sat, 09 Apr 2016 08:08:00 +0000 https://gulu-dev.com/post/2016-04-09-a-working-day/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-04-09-a-working-day/0.title.jpg" alt="Featured image of post 2016.04 我的日常一天" /><div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> 我从未度过没有编程的一天,这就是我的全部。 </span></span><span class="line"><span class="cl"> — John Carmack </span></span></code></pre></td></tr></table> </div> </div><h2 id="日常一天">日常一天</h2> <h3 id="清晨">清晨</h3> <p>清晨很清醒,可以用来做计划,也可以做简单的重构和清理,就像开场前的热身那样,帮助进入工作状态。</p> <h3 id="上午">上午</h3> <p>上午设计和实现__每天的首要目标__相关的任务。我发现不结合代码的,纯粹的为了设计而支出的时间是很低效的,边想边写比较容易保持状态,不跑偏,不过度设计。</p> <p>中午看看各种新闻八卦,上上B站,刷刷知乎什么的。</p> <h3 id="下午">下午</h3> <p>下午处理__次要和临时目标__,和那些需要静下心来反复思考和权衡的事情,这一类事情的特点是“要想清楚,但是动手不多”。</p> <p>下班前一个小时集中修复各类 bug,如果一天的效率还算不错,那么这会儿体力下降,应该缓一下了。把能今天收掉的就收掉,给明天少留一点负担。</p> <h3 id="晚上">晚上</h3> <p>晚上如果不累的话,我一般看看书,整理一下读书笔记,用涂书笔记把重要的段落摘录下来,写上一两句自己的感想;如果有点累,就看看电影,打打游戏啥的。</p> <p>心情好的话会唱首歌,还有就是会读一下白天遇到的各种好玩但还没来得及读的材料。</p> <hr> <h2 id="工具辅助">工具辅助</h2> <p>我使用下面几个工具作为日常的辅助。</p> <h3 id="rescuetime">RescueTime</h3> <p>我使用 RescueTime 帮助自己记录时间实际使用情况,最大的好处是零设置和零干扰。</p> <p><img src="https://gulu-dev.com/post/2016-04-09-a-working-day/1.rescue.png" width="1032" height="327" srcset="https://gulu-dev.com/post/2016-04-09-a-working-day/1.rescue_hu3e435b56dd895128def1a1f471eba512_36099_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-09-a-working-day/1.rescue_hu3e435b56dd895128def1a1f471eba512_36099_1024x0_resize_box_3.png 1024w" loading="lazy" alt="1.rescue" class="gallery-image" data-flex-grow="315" data-flex-basis="757px" ></p> <p>这里统计了本周记录在案的工作时间,包括每一天的具体情况。也可以看到自己在每种类型的工作上花费的时间,还有自己的工作节奏评分和与上周相比的趋势。可以看到我一般周一是峰值,在周三会回升一下,周五会比较低一点 (<em>^-^</em>)</p> <p><img src="https://gulu-dev.com/post/2016-04-09-a-working-day/2.rescue.png" width="1016" height="364" srcset="https://gulu-dev.com/post/2016-04-09-a-working-day/2.rescue_hu29106a55706991ad77a27bb0bd6b3059_25295_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-09-a-working-day/2.rescue_hu29106a55706991ad77a27bb0bd6b3059_25295_1024x0_resize_box_3.png 1024w" loading="lazy" alt="2.rescue" class="gallery-image" data-flex-grow="279" data-flex-basis="669px" ></p> <p>这里可以看到细目,每一个程序使用时间,频率,文档或资源的具体内容。</p> <h3 id="todoist">Todoist</h3> <p>我使用 Todoist 来帮助自己跟踪当前要处理的事务,临时追加的事务,最大的好处是手机上可以随时添加和处理。</p> <p><img src="https://gulu-dev.com/post/2016-04-09-a-working-day/3.todoist.png" width="759" height="662" srcset="https://gulu-dev.com/post/2016-04-09-a-working-day/3.todoist_huab88eb1cd50471d953ca0538ccf38246_49106_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-09-a-working-day/3.todoist_huab88eb1cd50471d953ca0538ccf38246_49106_1024x0_resize_box_3.png 1024w" loading="lazy" alt="3.todoist" class="gallery-image" data-flex-grow="114" data-flex-basis="275px" ></p> <h3 id="trello">Trello</h3> <p>我使用 Trello 来记录当前工作的系统上的进展情况,最大的好处是清晰明白,一目了然。</p> <p><img src="https://gulu-dev.com/post/2016-04-09-a-working-day/4.trello.png" width="1126" height="558" srcset="https://gulu-dev.com/post/2016-04-09-a-working-day/4.trello_hu336998bd19411507769619fa88c12994_52292_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-09-a-working-day/4.trello_hu336998bd19411507769619fa88c12994_52292_1024x0_resize_box_3.png 1024w" loading="lazy" alt="4.trello" class="gallery-image" data-flex-grow="201" data-flex-basis="484px" ></p> <h3 id="evernote">Evernote</h3> <p>Evernote 来充当临时剪贴板,记录一些临时信息,资料和笔记,还有我的工作日志也在这里。最大的好处是零碎信息的收拢。</p> <p><img src="https://gulu-dev.com/post/2016-04-09-a-working-day/5.evernote.png" width="764" height="658" srcset="https://gulu-dev.com/post/2016-04-09-a-working-day/5.evernote_huf74c73c0c58e526156aabbd698e181fa_37092_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-09-a-working-day/5.evernote_huf74c73c0c58e526156aabbd698e181fa_37092_1024x0_resize_box_3.png 1024w" loading="lazy" alt="5.evernote" class="gallery-image" data-flex-grow="116" data-flex-basis="278px" ></p> <h3 id="pocket">Pocket</h3> <p>我使用 Pocket 来收藏那些开发过程中搜到的,有价值但一时来不及读的信息,其他有趣的阅读材料也在这里。</p> <p><img src="https://gulu-dev.com/post/2016-04-09-a-working-day/6.pocket.png" width="967" height="639" srcset="https://gulu-dev.com/post/2016-04-09-a-working-day/6.pocket_hu9cdddac07e471bfac39697db8f2ab6fa_206992_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2016-04-09-a-working-day/6.pocket_hu9cdddac07e471bfac39697db8f2ab6fa_206992_1024x0_resize_box_3.png 1024w" loading="lazy" alt="6.pocket" class="gallery-image" data-flex-grow="151" data-flex-basis="363px" ></p> <hr> <h2 id="杂感-random-thoughts">杂感 (Random Thoughts)</h2> <p>尽量__不要在早上看新闻__ (公众号/朋友圈)。脑子里塞一堆对你当天没任何帮助的八卦和杂乱信息流相当于主动降低大脑的速度和容量,经年累月这么干的话,相当于别人都在不断地提升自己的头脑,而你却在把自己的头脑越磨越钝。不要担心你不知道发生了什么,如果发生的事情真的重要,它很快就会以别的方式进入你的视野。</p> <hr> <p>实现一个系统时,应像高考考场和面试时当场写代码那样,<strong>给自己时间上的约束</strong>,先易后难,勇于舍弃一些超出时间的点。以前常犯的一个毛病是一叶障目,容易钻牛角尖,在一个坑里越陷越深,缺乏随时跳出上下文的能力,也是没有大局观的表现。</p> <hr> <p>一般我有两种工作节奏: Coding Day(编码日) 和 Talking Day(说话日),前者是设计,实现,测试,调试等具体的代码相关的工作,后者是交流,商量,讨论等沟通性的工作,笼统来说,前者是跟电脑打交道,后者是跟人打交道,一般某一个具体的工作日会以其中一个为主,两个反复交叉的话很影响节奏,容易让两个的效率都降低。</p> <hr> <p>人生只有九百个月。你还记得你上个月做了哪些有价值的事情吗?上上个月呢?在过去的三个月里你学到了什么?去年的目标达成了吗?今年以来认识了哪些有价值的人?在哪些方面得到了他们的有价值的帮助?你帮助他们了吗,你的帮助对他们有真正的价值吗?时不时地回顾一下,可以帮助自己梳理清楚来龙去脉,降低迷路和死胡同的可能,也能鼓励自己创造真正的价值,探索更多的可能性。</p> <hr> <p><strong>&ldquo;Most people work at only a fraction of their potential.&rdquo;</strong></p> <blockquote> <p>&ldquo;Using your time effectively is very important, and there is often a non-linear relationship between the amount of time you can stay focused and the amount that you can learn or accomplish. It is often possible to get more done in a highly focused 12 hour stretch than in a normal 40 hour work week that is interspersed with email, chat, and other distractions. Someone that can be completely obsessive about something does have an advantage, but the same questions about focus apply for any amount of time you choose to devote to an undertaking. Most people work at only a fraction of their potential.&rdquo; - John Carmack</p> </blockquote> 2016.02 (C++) 快速辨别左值和右值的两个方法 https://gulu-dev.com/post/2016-02-07-lvalue-rvalue/ Sun, 07 Feb 2016 22:41:00 +0000 https://gulu-dev.com/post/2016-02-07-lvalue-rvalue/ <p>除夕夜,抢红包的间隙跑过来答一发知乎的提问~~</p> <hr> <h3 id="问题c左值和右值区别">问题:C++左值和右值区别</h3> <p>问题:<a class="link" href="https://www.zhihu.com/question/39846131" target="_blank" rel="noopener" >关于C++左值和右值区别有没有什么简单明了的规则可以一眼辨别?</a></p> <hr> <p>看了一下现有的答案,讲概念和理论的多,谈可操作性的少。我来说两个办法吧,不过都不是俺原创的。</p> <h3 id="来自-scott-meyers-的方法">来自 Scott Meyers 的方法</h3> <p>判断表达式是否是左值,有一个简单的办法,就是<strong>看看能否取它的地址</strong>,能取地址的就是左值。</p> <blockquote> <p>A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can’t, it’s usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue. &ndash; <em>&ldquo;Effective Modern C++&rdquo;, Introduction - Terminology and Conventions, Scott Meyers</em></p> </blockquote> <h3 id="来自-joseph-mansfield-的方法">来自 Joseph Mansfield 的方法</h3> <p>理解下面的几句话就可以了,顺带也清楚地表达了 <code>std::move</code> 和 <code>std::forward</code> 的区别和联系</p> <ul> <li>左值可看作是 “<strong>对象</strong>”,右值可看作是 “<strong>值</strong>” (Lvalues represent objects and rvalues represent values)</li> <li>左值到右值的转换可看做 “<strong>读出对象的值</strong>” (Lvalue-to-rvalue conversion represents reading the value of an object)</li> <li><code>std::move</code> 允许 <strong>以 “值” 的方式</strong> 处理任何的表达式 (allows you to treat any expression as though it represents a value)</li> <li><code>std::forward</code> 允许在处理的同时,保留 <strong>表达式为“对象”还是“值”</strong> 的特性 (allows you to preserve whether an expression represented an object or a value)</li> </ul> <p>那么这里的“对象” (object) 和 “值” (value) 是什么意思呢?</p> <p>任何一个有价值的 C++ 程序都是如此:a) 反复地操作各种类型的对象 b) 这些对象在运行时创建在明确的内存范围内 c) 这些对象内存储着值。 (Every useful C++ program revolves around the manipulation of objects, which are regions of memory created at runtime in which we store values.)</p> <p>这一句话的解释实际上就指向了上面的第一条方法——只有特定的内存区域才可以被取地址。</p> <h3 id="举个栗子">举个栗子</h3> <p>先定义一个变量,</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">foo</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>此时,</p> <ul> <li>表达式 <code>foo</code> 是一个左值,可以取地址 (<code>&amp;foo</code>) (方法一),<code>foo</code> 本身是一个拥有明确内存范围的对象 (方法二)</li> <li>表达式 <code>foo + 5</code> 是一个右值,无法取地址 (<code>&amp;(foo + 5)</code>) (方法一),是一个需要被存储到对象里的值 (方法二)</li> </ul> <hr> <p>关于第二种方法,更详细的讨论可见下面 Joseph Mansfield 的文章。此文行文流畅,对理解 lvalue/rvalue/move/forward 很有帮助。</p> <ul> <li><a class="link" href="http://josephmansfield.uk/articles/lvalue-rvalue-metaphor.html" target="_blank" rel="noopener" >(Joseph Mansfield) The lvalue/rvalue metaphor</a></li> </ul> 2016.02 从外部结束一个 goroutine (Go) https://gulu-dev.com/post/2016-02-02-kill-goroutine/ Tue, 02 Feb 2016 05:27:00 +0000 https://gulu-dev.com/post/2016-02-02-kill-goroutine/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-02-02-kill-goroutine/gopher.png" alt="Featured image of post 2016.02 从外部结束一个 goroutine (Go)" /><h2 id="需求分析-3个原因">需求分析 (3个原因)</h2> <p>产生这个需求,通常有以下的原因:</p> <ol> <li>这个 goroutine 的运行超出了太多预计的时间,以致后续的计算不再有意义</li> <li>这个 goroutine 阻塞在某个 read/write channel 变得没有响应</li> <li>这个 goroutine 阻塞在某个系统调用,外部调用或业务逻辑的死循环</li> </ol> <p>这种时候很自然地就会产生“主动外部 kill goroutine”的需求 (正如手动结束掉一个无响应的进程那样)。</p> <p>然而 goroutine 被设计为不可以从外部无条件地结束掉,只能通过 channel 来与它通信。也就是说,每一个 goroutine 都需要承担自己退出的责任。(A goroutine cannot be programmatically killed. It can only commit a cooperative suicide.)</p> <p>以下我们分可响应 (1 &amp; 2) 和不可响应 (3) 两种情况分开讨论</p> <h2 id="处理仍可响应-channel-的-goroutine-1--2">处理仍可响应 channel 的 goroutine (1 &amp; 2)</h2> <p>最直接的方法是关闭与这个 goroutine 通信的 channel <code>close(ch)</code>。如果这个 goroutine 此时阻塞在 read 上,那么阻塞会失效,并在第二个返回值中返回 false (此时可以检测并退出);如果阻塞在 write 上,那么会 panic,这时合理的做法是在 goroutine 的顶层 recover 并退出。</p> <p>更健壮的设计一般会把 data channel (用于传递业务逻辑的数据) 和 signal channel (用于管理 goroutine 的状态) 分开。不会让 goroutine 直接读写 data channel,而是通过 select-default 或 select-timeout 来避免完全阻塞,同时周期性地在 signal channel 检查是否有结束的请求。</p> <p>以上的方法可以处理前两种情况。</p> <h2 id="处理无法响应-channel-的-goroutine-3">处理无法响应 channel 的 goroutine (3)</h2> <p>对于第三种情况,程序员能做的就是:</p> <ol> <li>尽量使用 Non-blocking IO (正如 go runtime 那样)</li> <li>尽量使用阻塞粒度较小的 sys calls (对外部调用也一样)</li> <li>业务逻辑总是考虑退出机制,编码时避免潜在的死循环</li> <li>在合适的地方插入响应 channel 的代码,保持一定频率的 channel 响应能力</li> </ol> <p>关于 blocking syscall,需要注意的是 Go runtime 会启动新的 OS 线程去调度剩下的 goroutines,如果不能及时从阻塞中恢复并持续有新的 blocking goroutine 的话,OS 线程数量会线性地增长,这是一种非常不理想的情况,极端例子可以看下面的 &ldquo;why 1000 goroutine generats 1000 os threads?&quot;。</p> <h2 id="references">References</h2> <ul> <li>(golang-nuts) <a class="link" href="http://grokbase.com/t/gg/golang-nuts/133vrbqtbr/go-nuts-end-a-blocked-goroutine-from-outside" target="_blank" rel="noopener" >End a blocked goroutine from outside</a></li> <li>(StackOverflow) <a class="link" href="http://stackoverflow.com/questions/6807590/how-to-stop-a-goroutine/8098837#8098837" target="_blank" rel="noopener" >how to stop a goroutine</a></li> <li>(StackOverflow) <a class="link" href="http://stackoverflow.com/questions/6328679/in-golang-does-it-make-sense-to-write-non-blocking-code" target="_blank" rel="noopener" >in golang, does it make sense to write non-blocking code?</a></li> <li>(golang-nuts) <a class="link" href="https://groups.google.com/forum/#!topic/golang-nuts/2IdA34yR8gQ" target="_blank" rel="noopener" >&ldquo;why 1000 goroutine generats 1000 os threads?&rdquo;</a></li> <li>(Quora) <a class="link" href="https://www.quora.com/Go-programming-language/What-happens-when-a-goroutine-blocks" target="_blank" rel="noopener" >What happens when a goroutine blocks?</a></li> <li>(知乎) <a class="link" href="https://www.zhihu.com/question/20862617" target="_blank" rel="noopener" >golang的goroutine是如何实现的?</a></li> </ul> 2016.02 遮挡剔除的低端解决方案 https://gulu-dev.com/post/2016-02-01-occlusion-culling/ Mon, 01 Feb 2016 20:05:00 +0000 https://gulu-dev.com/post/2016-02-01-occlusion-culling/ <img src="proxy.php?url=https://gulu-dev.com/post/2016-02-01-occlusion-culling/oc.png" alt="Featured image of post 2016.02 遮挡剔除的低端解决方案" /><p>这是今天在知乎上的一个回答,原题如下:</p> <p><strong><a class="link" href="https://www.zhihu.com/question/38060533" target="_blank" rel="noopener" >问题:求问有哪些合用的遮挡剔除技术?</a></strong></p> <p>问题补充:</p> <pre><code>我们的flash3d的fps游戏 面向的受众 机器性能差的占不少,很多都是很低端的集成显卡。像素填充率很低。目前我们用的遮挡剔除技术是quake的bsp+portal。但是在一些没有明显房间分划的场景就显的几乎没什么用了。 求问 有什么合用的剔除技术适用于室外场景,没有明确房间概念 但是互相之间还是有遮挡关系的 另外umbra3d所用的技术都是公开的么? 如果我要能做到它的程度 有哪些paper可以参考。因为umbra3d没有针对flash的sdk.所以买了也没法用上 另外我对这个领域的知识是很碎片化的 请高手推荐些paper或者书籍能让我对这个领域有些系统的认识 知道哪些问题可以解决 而哪些问题其实目前还解决不了 </code></pre> <hr> <p>以下部分是我的答案:</p> <hr> <p>@庞巍伟的方案是比较流行的思路,不过感觉用在题主提到的低端集成显卡上,可能会有些限制。这里我先补充一些实时OC的思路和实践,然后再针对题主这种情况提个离线的方案,看起来可能 low 一点,仅供参考。</p> <hr> <p>先说明一下,遮挡剔除 (Occlusion Culling) 本质上是这样一个过程——消耗一小部分 CPU 来去掉不可见的物体,不改变最终渲染的画面的同时,降低 GPU 的负载。</p> <p>实时的 OC 已经有题主提到的 umbra 方案,背后的原理是 <a class="link" href="https://en.wikipedia.org/wiki/DPVS" target="_blank" rel="noopener" >dPVS (Wikipedia)</a> 以及早先的 <a class="link" href="https://en.wikipedia.org/wiki/Potentially_visible_set" target="_blank" rel="noopener" >PVS (Wikipedia)</a>。这两个页面上有非常简略的介绍,可以作为起点看一下,然而年久失修,上面不少链接已经 404 了。</p> <p>其中的一篇原理</p> <ul> <li><a class="link" href="https://mediatech.aalto.fi/~timo/publications/MSc_thesis.pdf" target="_blank" rel="noopener" >SurRender Umbra: A Visibility Determination Framework for Dynamic Environments</a></li> </ul> <p>虽然早了一些 (Oct2000),但内容比较详实,是很好的参考。</p> <p>此文的作者是 Timo Aila 博士,主页在<a class="link" href="https://mediatech.aalto.fi/~timo/" target="_blank" rel="noopener" >这里</a>,上面的文章是他的硕士论文,他的博士论文</p> <ul> <li><a class="link" href="http://lib.tkk.fi/Diss/2005/isbn9512274833/" target="_blank" rel="noopener" >Efficient Algorithms for occlusion culling and shadows (Jan2005)</a></li> </ul> <p>和这一篇讲 dPVS 的论文</p> <ul> <li><a class="link" href="https://research.nvidia.com/publication/dpvs-occlusion-culling-system-massive-dynamic-environments" target="_blank" rel="noopener" >dPVS: An Occlusion Culling System for Massive Dynamic Environments (2004)</a></li> </ul> <p>也可供参考。</p> <hr> <p>下面是一些游戏里的具体实践,其中不少思路与上面是一脉相承的:</p> <ul> <li><a class="link" href="http://webstaff.itn.liu.se/~perla/Siggraph2011/content/talks/40-silvennoinen.pdf" target="_blank" rel="noopener" >(Siggraph 2011) Occlusion culling in Alan Wake</a> Slides: (<a class="link" href="http://www.slideshare.net/Umbra3/siggraph-2011-occlusion-culling-in-alan-wake" target="_blank" rel="noopener" >slideshare link</a>)</li> <li><a class="link" href="http://www.slideshare.net/guerrillagames/practical-occlusion-culling-in-killzone-3" target="_blank" rel="noopener" >(Siggraph 2011) Practical Occlusion Culling in Killzone 3</a></li> <li><del><a class="link" href="http://dice.se/wp-content/uploads/CullingTheBattlefield.pdf" target="_blank" rel="noopener" >(GDC2011) (dice.se) Culling the BattleField (已失效)</a></del> <a class="link" href="http://www.frostbite.com/wp-content/uploads/2013/05/CullingTheBattlefield.pdf" target="_blank" rel="noopener" >(frostbite.com) Culling the BattleField</a></li> <li><a class="link" href="http://gdcvault.com/play/1014356/Practical-Occlusion-Culling-on" target="_blank" rel="noopener" >(GDC2011) Practical Occlusion Culling on PS3</a></li> <li><a class="link" href="http://www.selfshadow.com/talks/rwc_gdc2010_v1.pdf" target="_blank" rel="noopener" >(GDC2010) The Rendering Tools And Techniques Of Splinter Cell: Conviction</a></li> <li><a class="link" href="http://www.gdcvault.com/play/1017837/Why-Render-Hidden-Objects-Cull" target="_blank" rel="noopener" >(GDC2013) Intel: Why Render Hidden Objects? Cull Them With a Software Depth-Buffer Rasterizer!</a></li> <li><a class="link" href="http://www.slideshare.net/Umbra3/solving-visibility-and-streaming-in-the-the-witcher-3-wild-hunt-with-umbra-3" target="_blank" rel="noopener" >(Umbra2015) Solving Visibility and Streaming in the The Witcher 3: Wild Hunt with Umbra 3</a></li> <li><a class="link" href="http://www.slideshare.net/sampol1/nexon-developer-conference" target="_blank" rel="noopener" >(Umbra2013) Automatic Software Occlusion Culling for Massive Streaming Worlds</a></li> <li><a class="link" href="http://www.gamasutra.com/view/feature/164660/sponsored_feature_next_generation_.php" target="_blank" rel="noopener" >(Umbra2012) Next Generation Occlusion Culling</a></li> <li><a class="link" href="http://www.slideshare.net/Umbra3/visibility-optimization-for-games" target="_blank" rel="noopener" >(Umbra2011) Visibility Optimization for Games</a></li> </ul> <hr> <p>还有一些补充的材料,可以顺带参考一下:</p> <ul> <li><a class="link" href="http://www.gamasutra.com/view/feature/131801/occlusion_culling_algorithms.php" target="_blank" rel="noopener" >(gamasutra1999) Occlusion Culling Algorithms</a></li> <li><a class="link" href="https://developer.valvesoftware.com/wiki/Visibility_optimization" target="_blank" rel="noopener" >(valve) Visibility optimization (BSP)</a></li> <li><a class="link" href="https://www.cg.tuwien.ac.at/courses/Seminar/WS2006/dynamicvisibility.pdf" target="_blank" rel="noopener" >Dynamic Occlusion Culling</a></li> <li><a class="link" href="http://dcgi.felk.cvut.cz/home/bittner/publications/chc&#43;&#43;.pdf" target="_blank" rel="noopener" >CHC++: Coherent Hierarchical Culling Revisited</a></li> </ul> <hr> <p>嗯,列了一堆材料,现在简单说一下我开头提到的离线方案。</p> <p>简单说,离线方案就是预先在开发阶段生成好所谓的“<strong>潜在可见物体集(PVS)</strong>”,存下来,消耗一点存储空间,运行时只用付出查表的开销。最大的好处是运行时性能损失几乎为零,是典型的空间换时间。</p> <p>题主提到 bsp+portal 在没有明显房间分划的场景(室外场景)几乎没什么用,那么室外怎么处理呢?一个土一点的办法,就是空间上划分为较小的子区域,每个子区域生成一个可见列表,只要摄像机位于该区域内,就激活该列表,每帧查表,凡是不在此列表中的就被剔除。</p> <p>这些区域可以用一个简单的二维数组(棋盘格状),也可以用某种层次树结构(复用你的场景树)去组织。</p> <p>边界附近怎么办?同时激活两边区域的可见列表就好了。</p> <hr> <p>你可能已经看出来了,这是一个__较保守__的策略,列表容易变得臃肿——只要在该区域内曾被看到过,就会出现在列表里。嗯,没错,保守程度取决于区域划分的大小和场景的布局。但请注意,室外场景的特点是:区域内临近的点往往有很强的空间上的相关性 (spatial coherence) 也就是说,在划分得当的情况下,较大的 occluder 将足够产生较好的遮挡效果。举个例子,山脚下有个小镇,如果小镇本身是一个划定区域,那么这座山所挡住的绝大部分物体对小镇中的任一点均有效。</p> <p>咱们__抓大放小__,对低端机器只用追求性价比就可以了,追求极致的完全精确的 100% 不可见剔除意义并不大。</p> <hr> <p>空间开销上,通常一个场景内的对象数量在 65535 以内,可以用一个 ushort,假设一张地图的区域数量在 30~50 个,每个区域可见对象在1k~3k之间,那么每张地图所费磁盘尺寸在60k~300k左右,压缩一下到100k以内问题不大。顺便说一句,可以分块压缩,运行时只展开玩家附近的即可,不用全部展开在内存里,这样内存开销可忽略不计。</p> <p>接下来说一下怎么生成这个对象列表。两种方式,一种是从摄像机所在点发出全视角的射线,凡是 trace 到的对象皆认为可见;另一种是单色渲染到纹理,然后看实际渲染结果是否包含特定颜色来确认对应的物体是否在 framebuffer 可见。摄像机使用某种算法(如 flood-fill)来确保该区域内所有位置都包含在内。</p> <p>最后说一下透明物体的处理,很显然透明物体是不能做 occluder 只能做 occludee 的,那么分两个阶段把透明物体单独提出来判断即可。</p> <p>总得来说,这个方案最大的特点是——运行期没啥开销,代码逻辑也足够简单——简单到你会觉得太 low 以至于不好意思在项目里用——在这个讲究逼格的年代,用了都不好意思跟人家说自己是这么干的^_^</p> <hr> <p>嗯,大致如此。今天是农历传统的小年(腊月二十三),跟父亲喝了些酒,写得有点凌乱,见谅。</p> <ul> <li>本文在知乎上的回答页面在<a class="link" href="https://www.zhihu.com/question/38060533/answer/84429692" target="_blank" rel="noopener" >这里</a></li> <li>[2016-02-05] 修复 &ldquo;Culling the Battlefield&rdquo; 的失效链接</li> </ul> 2016.01 你好 2016 (统计,汇总及十大) https://gulu-dev.com/post/2016-01-01-hello-2016/ Fri, 01 Jan 2016 22:18:00 +0000 https://gulu-dev.com/post/2016-01-01-hello-2016/ <hr> <h2 id="简单统计">简单统计</h2> <p>今天是 2016 年的第一天,做个简单的统计吧:</p> <ul> <li>过去一年新增文章 24 篇,合 287,375 字节;总数 47 篇,合 514,328 字节</li> <li>根据 FarBox 的统计,最近两个月 PV 合计 20975 (日均 PV 约 350)</li> </ul> <hr> <h2 id="分类汇总">分类汇总</h2> <p>两个月前,我在知乎开了一个<a class="link" href="http://zhuanlan.zhihu.com/gu-lu" target="_blank" rel="noopener" >专栏</a>,那里会同步一些游戏开发相关的文章过去。这个专栏的第一篇是<a class="link" href="http://zhuanlan.zhihu.com/gu-lu/20289098" target="_blank" rel="noopener" >游戏开发汇总</a>,上面按照主题分类梳理和收集了这个 blog 上的资源,可视作一个不定期整理的目录,帮助更方便地找到某类主题的所有内容。</p> <hr> <h2 id="访问最多的十篇">访问最多的十篇</h2> <p>按照此刻的统计,这个 blog 上访问最多的十篇文章 (正好也是所有 2k+ 的):</p> <ul> <li>(13.6k) <a class="link" href="http://gulu-dev.com/post/2014-11-16-open-world" target="_blank" rel="noopener" >知乎回答: 开放世界游戏中的大地图背后有哪些实现技术?</a></li> <li>(4.7k) <a class="link" href="http://gulu-dev.com/post/2015-11-05-tips-for-non-programmers" target="_blank" rel="noopener" >在实际工作中评估你的工程师伙伴 - 写给非技术向小伙伴的参考</a></li> <li>(3.8k) <a class="link" href="http://gulu-dev.com/post/2015-05-09-legacy-code" target="_blank" rel="noopener" >知乎回答: 入职后发现项目组代码异常混乱,是去是留?</a></li> <li>(3.7k) <a class="link" href="http://gulu-dev.com/post/2014-09-23-cppcon14" target="_blank" rel="noopener" >CppCon2014 分类合辑 &amp; 十大推荐阅读列表</a></li> <li>(3.3k) <a class="link" href="http://gulu-dev.com/post/2014-06-08-nanomsg" target="_blank" rel="noopener" >nanomsg - zmq 的华丽转身</a></li> <li>(2.9k) <a class="link" href="http://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips" target="_blank" rel="noopener" >Unity 项目实践点滴</a></li> <li>(2.6k) <a class="link" href="http://gulu-dev.com/post/2014-07-28-tech-evaluation" target="_blank" rel="noopener" >如何判断一个技术(中间件/库/工具)的靠谱程度?</a></li> <li>(2.4k) <a class="link" href="http://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil" target="_blank" rel="noopener" >“千年故纸空读尽,恨把衣冠祭九州” - 小记《三国志11之血色衣冠》</a></li> <li>(2.3k) <a class="link" href="http://gulu-dev.com/post/2014-06-28-microsoft-crt" target="_blank" rel="noopener" >昔时因 今日意 侃侃微软的CRT</a></li> <li>(2.2k) <a class="link" href="http://gulu-dev.com/post/2015-03-16-handwriting-notes" target="_blank" rel="noopener" >如何对手写笔记进行漂亮和高效的排版?</a></li> </ul> <p>其实我自己更喜欢后面的几篇随笔 :)</p> <hr> <h2 id="小小寄语">小小寄语</h2> <p>新的一年,在开始填彼时挖的各种坑之前,告诫自己:</p> <ul> <li>不装逼,不卖弄,不浮夸,不要不懂装懂。</li> <li>不枯燥,不啰嗦,不说教,不要老气横秋。</li> <li>力求客观,但绝不冷漠,保持平实有趣味。</li> </ul> <p>希望可以说到做到罢。</p> <hr> <p>最后多说两句,请诸公众号朋友们,转载前麻烦先跟我说一声,转载时尽量不要修改文章的标题和内容,谢谢你们 ^_^ 其实比起告知,我更看重保持原文的重要性。有些时候,一经删节,原意就变了,甚至不经意的调侃也会变成有意的冒犯。至于一些不告而取的朋友,精力有限,我亦不会一一追究,你开心就好。</p> <hr> <p>[注] 本文摘自 “我的 2015 - 节奏与态度” 的 “我的自媒体” 一节。</p> 2015.12 GTA V 图形分析摘要 https://gulu-dev.com/post/2015-12-30-gtav-graphics/ Wed, 30 Dec 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-12-30-gtav-graphics/ <img src="proxy.php?url=https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/00_final_frame.jpg" alt="Featured image of post 2015.12 GTA V 图形分析摘要" /><p>此文信息主要摘自 Adrian Courrèges 同学的 <a class="link" href="http://www.adriancourreges.com/blog/2015/11/02/gta-v-graphics-study/" target="_blank" rel="noopener" >GTA V - Graphics Study</a> 系列 (<a class="link" href="http://www.adriancourreges.com/blog/2015/11/02/gta-v-graphics-study/" target="_blank" rel="noopener" >1</a>, <a class="link" href="http://www.adriancourreges.com/blog/2015/11/02/gta-v-graphics-study-part-2/" target="_blank" rel="noopener" >2</a>, <a class="link" href="http://www.adriancourreges.com/blog/2015/11/02/gta-v-graphics-study-part-3/" target="_blank" rel="noopener" >3</a>),俺昨晚从笔记拷出来时,给 Adrian 同学发了一封邮件,请他允许我 reblog 一下。在得到 Adrian Courrèges 同学的同意后,俺就放上来了。</p> <p>本文信息提炼后,去掉了绝大部分科普,如有不适,请配合原文的更多细节。</p> <h2 id="环境渲染">环境渲染</h2> <ul> <li>最外层的 cubemap 是每一帧实时生成的,目的是简化后续真实反射的渲染。</li> <li>这个 cubemap 是一张低精度的 128*128 纹理,每个面 30 左右 drawcall,都是地表天空等较大像素贡献的多边形</li> <li>全部是静态物体,所以车辆的外壳反射不了其他的车和角色</li> <li>这个 cubemap 随后被转成了双抛面图 (Dual-Paraboloid Map),投影过程类似球面映射,这样的话相关 PS 开销就从 6 个面降到了 2 个面 (一上一下,各 128*128),由于摄像机一般在车顶斜后方45度,反射的绝大部分时间都只用访问朝上的那个面,如果用 cubemap 就是 3~4 个面。整体来看,图像质量上四个侧面受影响最大,顶面和底面保留最多,由于摄像机看到的车顶通常反射顶面,所以效果损失很小</li> </ul> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/01_environment.jpg" width="1024" height="346" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/01_environment_huc706e6cfd2f8e5d657b3e38ceb749451_33799_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/01_environment_huc706e6cfd2f8e5d657b3e38ceb749451_33799_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img01" class="gallery-image" data-flex-grow="295" data-flex-basis="710px" ></p> <h2 id="主渲染流程">主渲染流程</h2> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/02_g_buffers.jpg" width="981" height="850" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/02_g_buffers_hua5677f0a1d0c0419e9f1f90f56c27b55_144681_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/02_g_buffers_hua5677f0a1d0c0419e9f1f90f56c27b55_144681_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img02" class="gallery-image" data-flex-grow="115" data-flex-basis="276px" ></p> <ul> <li>GTA V 的主渲染流程输出到 5 个 RT 并汇入最终的每个像素,透明物体后期另行处理。五个 RT 分别是 Diffuse/Normal/Specular/Irradiance/(Depth/Stencil),绘制这些主体像素大约花了 1900 drawcall。 <ul> <li>Diffuse RT 内保存了每个像素所属物体的本身的平坦材质色,一个特例 (看车的引擎盖可以发现) 是部分物体保存了方向光的结果,A 通道下面单独谈</li> <li>Normal RT 里保存了每个像素的法线信息,alpha 通道存了一些植被相关的遮罩信息</li> <li>Specular RT 内 RGB 通道分别保存了高光,光泽度和菲涅尔强度信息</li> <li>Irradiance RT 内在 R 和 G 通道保存了主次光源对每个像素的贡献,B 通道存了自发光信息 (车灯,路灯,霓虹灯什么的) A 通道存了一些角色皮肤和植被的标记</li> <li>Depth/Stencil 共用一个 RT,分开说 <ul> <li>深度图保存了每个实际绘制的像素到摄像机的距离,每个像素的实际 Z 值是以对数形式保存的,因为 float 越接近 0 越精确,使用对数 (或倒数) 能有效提高超远距离物体的精度 (降低 Z-Fighting)</li> <li>Stencil 用来标记每个像素上各种不同物件产生的不同信息,便于后期做针对性处理,具体在不同的位段做过标记的像素有:玩家控制角色/玩家控制车辆/NPC/NPC车辆/植被/天空</li> </ul> </li> </ul> </li> <li>整个渲染以“Front-to-Back”方式进行,这样可以最大化 early-z 的效果。这样的结果是越往后,单个 drawcall 上被拒掉的像素就越多,越省 PS 时间</li> <li>主渲染流程完成 (也就是这些G-Buffer合并之后) 后如下图 (无天空,水体及透明物体,但含所有的光照和阴影)</li> </ul> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/03_gb_combine.jpg" width="950" height="534" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/03_gb_combine_hu1ffd44aa4348fa6c599179fa52f194dc_182514_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/03_gb_combine_hu1ffd44aa4348fa6c599179fa52f194dc_182514_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img03" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <h2 id="裁剪-lod-和-alpha-stippling">裁剪、 LOD 和 Alpha Stippling</h2> <ul> <li> <p>(裁剪和LOD) GTA V 内判断一个物体是否渲染,是否以较高的精度渲染,是以单个物件为单位,在一个 compute shader 里完成的</p> </li> <li> <p>解释一下前面的 Diffuse 的 A 通道做了啥事</p> </li> </ul> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/04_alpha_stippling.jpg" width="950" height="534" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/04_alpha_stippling_hu04ebb4f3a73e34cf7f7061e722412174_145610_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/04_alpha_stippling_hu04ebb4f3a73e34cf7f7061e722412174_145610_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img04" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这个看起来像棋盘格一样的东西叫 <a class="link" href="http://n00body.squarespace.com/journal/2009/9/14/stippled-alpha.html" target="_blank" rel="noopener" >alpha stippling</a>,是用来<strong>规律性地</strong>有选择地拒掉像素的技术。目的是更平滑的 LOD 切换。传统 LOD 的不同级别之间切换的时候,会有 popping,而 stipple 之后普通物体看起来就会“发虚” (尤其是边缘),这样切换时 popping 会弱化很多</p> <h2 id="阴影-csm-和其他杂项-rmssaosss">阴影 (CSM) 和其他杂项 (RM/SSAO/SSS)</h2> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/05_shadow.jpg" width="950" height="238" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/05_shadow_hub854cd21f265d7869e8ac85df0009f43_21441_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/05_shadow_hub854cd21f265d7869e8ac85df0009f43_21441_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img05" class="gallery-image" data-flex-grow="399" data-flex-basis="957px" ></p> <ul> <li>CSM 输出到 4 个 1024*1024 单位,但显存内是一张连续的 1024 * 4096 贴图 (注:这个不错,以前的实践是根据摄像机距离使用不同尺寸的 shadow map,GTA V 的统一尺寸更合理,性能也更好)。</li> <li>四张贴图由四个不同参数的摄像机由近及远分别生成,越近的地方提供越多的细节;这样的问题是需要画四遍,好在 fov 很小裁剪效率更高,最终算下来 CSM 费了 1000 个 drawcall,这里的 drawcall 很廉价 (因为输入/运算/输出量都很小)</li> <li>shadow map 也经过了与上面类似的抖动处理,这样边缘经过模糊后会更平滑</li> <li>关于模糊有个小优化:先弄个 1/8 尺寸的贴图做一个轻量级的 blur,以明确哪些地方无阴影/部分阴影/全阴影,然后做全深度的 blur 时就能忽略那些无阴影和全阴影的像素了,可以省掉大量的像素操作</li> <li>(Planar Reflection Map) 这玩意费了 600 drawcall 反着画在一个 240*120 的纹理上,用于大面积水面的反射</li> <li>(SSAO) 半精度,做了一下深度相关的模糊</li> <li>(SSS) 嘴唇处的 3S 效果是很显著的,对比上面两张大图。由于 Stencil Buffer 里一个位段专门存了玩家控制角色的像素贡献,而 Specular Map 内的 a 通道存了皮肤的像素贡献,通过这些信息可以做到在最少的像素上做 3S (对脸部专门处理性价比高一些,因为这是个看脸的世界-_-)</li> </ul> <h2 id="水面雾效大气-体积阴影">水面,雾效,大气 (体积阴影)</h2> <ul> <li>用了两张图:Diffuse Map 里存了水的本色,Opacity Map 在 R 和 G 通道里分别存了水的透明度和那一像素的水深信息 (用于透明度贡献率的计算) 存水深还有个好处是,顺便把 z 值过了确定是否可见 (也就是说 G 通道决定了是否对最终效果有贡献)</li> <li>最后实际绘制用了先前提到的反射图,折射图和 Bump Map</li> </ul> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/06_water.jpg" width="996" height="215" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/06_water_hubabea7c1db8413389711b2029fc4dd71_153494_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/06_water_hubabea7c1db8413389711b2029fc4dd71_153494_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img06" class="gallery-image" data-flex-grow="463" data-flex-basis="1111px" ></p> <ul> <li>(<a class="link" href="https://docs.unrealengine.com/latest/INT/Engine/Rendering/LightingAndShadows/LightShafts/index.html" target="_blank" rel="noopener" >light-shaft</a> map) 用于把阳光无法直射的区域的亮度压下去,半精度,顺便 blur 了一下让效果更自然</li> <li>雾效的最重要的作用是掩盖远处低模的细节,数据来源以之前输出的深度图为主</li> <li>天空 (半球,一个 drawcall) 和云 (环状 mesh,见三国志9) 没有用生成的 (反正硬盘不值钱哇哈哈哈)</li> </ul> <h2 id="透明物体和修修补补">透明物体和修修补补</h2> <ul> <li>墨镜,挡风玻璃,大灯的灯罩</li> <li>扬起的尘埃尽可能 instancing 了</li> <li>这一帧所有透明物体共 11 个 drawcall</li> <li>利用前面存下来的像素把前面提到的参与 Alpha Stippling 的像素融合一下</li> </ul> <h2 id="后处理-aa-和-lens-distortion">后处理, AA 和 Lens Distortion</h2> <ul> <li>Tone Mapping 使用了 <a class="link" href="http://filmicgames.com/archives/75" target="_blank" rel="noopener" >Filmic Tonemapping Operators</a> (神海2)</li> <li>HDR 用了四分之一精度,亮度用一个独立 compute shader 存到一个单像素纹理上</li> <li>算曝光,然后把较强光滤一把,这时候一般只剩几个离得近的车灯了</li> <li>然后是先往小再往大的迭代,回到半精度,最后 bloom/gamma 一下</li> </ul> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/07_tonemap_exposure.jpg" width="950" height="534" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/07_tonemap_exposure_hu26f90e995b1443d775c881fba7f69a81_129570_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/07_tonemap_exposure_hu26f90e995b1443d775c881fba7f69a81_129570_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img07" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>曝光控制很重要,帧与帧之间的 coherence 控制一下,注意, GTA V 的“从暗转亮”比“从亮转暗”要快 (符合真实人眼感受)</p> <ul> <li>AA 不说了</li> <li>Lens Distortion 一个简单的 PS 稍微形变一下,让最终成像更有镜头感</li> </ul> <h2 id="界面">界面</h2> <p>左下角的 UI 小地图是预生成的小块 tile,道路和建筑全部是矢量化的,无级缩放不损精度而且省空间</p> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/08_ui.jpg" width="394" height="230" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/08_ui_hu56c42bee30cb077eb6d408cb35ce5a33_80496_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/08_ui_hu56c42bee30cb077eb6d408cb35ce5a33_80496_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img08" class="gallery-image" data-flex-grow="171" data-flex-basis="411px" ></p> <h2 id="统计">统计</h2> <p>(drawcall: <strong>4155</strong>, textures: <strong>1113</strong>, render targets: <strong>88</strong>)</p> <p>这儿不说啥了吧,看上面三个数字,嗯。</p> <h2 id="话题-a--lod">话题 a : LOD</h2> <p>R星的 LOD 确实令人发指,即使在 PS3 那 256M 内存上也是几百公里随便跑,进了游戏就没有 loading 这回事。高低精度的各种不同版本,满足不同情况下顺畅运行的需要。(当然这是游戏数据量大的原因之一)屏幕上目所能及的星星点点灯光大部分都是可找到出处的真实发光的光源。</p> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/09_bulb.jpg" width="950" height="751" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/09_bulb_hu436ef2be3faca0991981529dadaffd1f_251284_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/09_bulb_hu436ef2be3faca0991981529dadaffd1f_251284_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img09" class="gallery-image" data-flex-grow="126" data-flex-basis="303px" ></p> <p>每个方块都用了同一张 32*32 贴图(屏幕右下角),一共上万个面吧,分批量按静态/动态的更新频率 batch 一下。运动着的车灯是动态更新的,晚上远距离下只画车灯就够了,跑近了再切整模。</p> <p>山体都有对应的低精度模型来做基础的 diffuse 贡献,这些低模可能是先自动生成再手动调整的。这些低模还可以用来生成一些次要的像素 (反射什么的)</p> <p>这些以 GB 为量级的数据不断随着玩家的移动加载/释放,大部分在内存里是压缩状态。</p> <p>当视角从一个角色切到另一个(相距数公里)时,摄像机有一个拉远再拉近的效果,用动画来避免了瞬间的IO过载。正常开车的移动速度跟这个相比就慢多了,streaming 完全没有问题。而飞行速度就快多了,相应地,由于视线很远,大多细节可以去掉,或用低精度模型代替。</p> <h2 id="话题-b--特效">话题 b : 特效</h2> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/10_pool.jpg" width="950" height="534" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/10_pool_hu0c1eb193259c5eacd442f8cf3c4926c3_119398_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/10_pool_hu0c1eb193259c5eacd442f8cf3c4926c3_119398_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img10" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>游泳池和海面不同,游泳池只是动一下法线,海水动法线不够,顶点也是实时更新的。反射贴图的精度很低,因为各种特效一盖就看不出来了。</p> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/11_water_textures.jpg" width="988" height="206" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/11_water_textures_hu68b5ce3e5c31b531e1ff45b28913a4c0_168396_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/11_water_textures_hu68b5ce3e5c31b531e1ff45b28913a4c0_168396_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img11" class="gallery-image" data-flex-grow="479" data-flex-basis="1151px" ></p> <p>镜子是简化的没啥特效的水,反射质量也就是像素精度是可调的,距离超出范围就会变成黑板。</p> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/12_anamorphic.jpg" width="950" height="534" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/12_anamorphic_hu9ecde18b9fa3f7998eeaaf376d23a78d_90733_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/12_anamorphic_hu9ecde18b9fa3f7998eeaaf376d23a78d_90733_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img12" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>这个 Anamorphic Lenses 只有迎面过来的强光才会有,跟普通 Lens 一样生成的动态 sprite。</p> <h2 id="话题-c--景深">话题 c : 景深</h2> <p><img src="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/13_dof.jpg" width="997" height="826" srcset="https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/13_dof_hu9fd4d3f2fb44f8b48bcac3ed7689d356_446244_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-30-gtav-graphics/images/13_dof_hu9fd4d3f2fb44f8b48bcac3ed7689d356_446244_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="img13" class="gallery-image" data-flex-grow="120" data-flex-basis="289px" ></p> <p>用了 <a class="link" href="https://en.wikipedia.org/wiki/Circle_of_confusion" target="_blank" rel="noopener" >Circle of Confusion</a> 做景深,存了 signed (-1, 1) 符号直接用来表示与焦点的关系。这个 CoC Map 用来与深度配合,获取前景/背景的信息,相邻像素的模糊关系及是否位于焦点上等信息。结果是前景的模糊融入焦点范围 (这一块用一张专门的图分离出来做模糊),背景模糊与焦点分离。</p> <p>其他的 Heat Haze, God Ray 啥的,不多说了。</p> <p>最后,感谢 Adrian Courrèges 同学的精彩系列文章。</p> 2015.12 (C++) 使用 std::tuple 和完美转发解析任意命令行字符串 https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/ Tue, 22 Dec 2015 16:24:00 +0000 https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/ <p><img src="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/tuple.jpg" width="413" height="336" srcset="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/tuple_hu06a997e0c18b5b89ef03af1c4d35d754_102483_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/tuple_hu06a997e0c18b5b89ef03af1c4d35d754_102483_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="tuple" class="gallery-image" data-flex-grow="122" data-flex-basis="295px" ></p> <blockquote> <p>I heard you like tuple so i put tuple in your tuple so you have tuple in your tuple - Yo Dawg (<a class="link" href="http://memegenerator.net/instance/55178677" target="_blank" rel="noopener" >Source</a>)</p> </blockquote> <p>由于微信会对外部页面重新排版,在微信内置浏览器中,本文会出现下面的异常:</p> <ul> <li>文中的链接无法正常访问</li> <li>文中的代码段落多出了许多 html tag,变得不可读</li> </ul> <p>请选择右上角菜单“在浏览器中打开”即可正常阅读。(本文的<a class="link" href="http://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser" target="_blank" rel="noopener" >永久链接</a>)</p> <h2 id="需求">需求</h2> <p>今天上午写代码时,写着写着遇到一个需求,是在 C++ 程序里把一个命令行字符串解析成对应的一组变量。也就是把形如:</p> <blockquote> <p>app.exe foo 100 bar 0.05</p> </blockquote> <p>这样的一个字符串解析一下,放到下面这样的一组变量里:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">struct</span> <span class="nc">VariableGroup</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">float</span> <span class="n">precision</span> <span class="o">=</span> <span class="mf">0.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个需求很常见,相信大家都遇到过吧。在实际的程序里,同一个命令行可以有各种不同的用法,命令和参数,全部都手写的话,很罗嗦,维护也麻烦。如果是 Python,有现成的 <a class="link" href="https://docs.python.org/3/library/argparse.html" target="_blank" rel="noopener" >argparse</a> 和 <a class="link" href="http://docopt.org/" target="_blank" rel="noopener" >docopt</a>,C++ 的话,选择就少一些了,而我又不想用 <code>boost::tokenizer</code> 之类的库,脑子里突然闪过 <code>std::tuple</code> 这货。好吧,就是它了,试着手写一个类型安全,简洁轻便,又能有一定灵活性的版本吧。</p> <hr> <p>我们知道,<code>std::tuple</code> (以下简洁起见直接说 <code>tuple</code>) 最好玩的特性就是__可以装 n 个任意类型的变量__(一个加强版的 <code>std::pair</code>)正好对应“<strong>命令行字符串的参数类型和个数的随意组合</strong>”的需求。</p> <p>老规矩,先定义接口:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="p">...</span><span class="o">&gt;</span> <span class="n">parse</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">commandline</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">???</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>上面的代码中,接收一个字符串,返回一个 <code>tuple</code> 包含解析好的各项数据。如果能定义出这样的接口,那么用的时候我们就可以这样了:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">auto</span> <span class="n">v</span> <span class="o">=</span> <span class="n">parse</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">float</span><span class="o">&gt;</span><span class="p">(</span><span class="s">&#34;foo 123 0.05&#34;</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>这样解析过之后 <code>v</code> 就是一个包含了三个元素的 <code>tuple</code>,分别是 <code>std::string &quot;foo&quot;</code>、<code>int 123</code> 和 <code>float 0.05</code>。这个 <code>parse()</code> 函数应该可以接收任意数目任意类型的命令行参数,返回一个内含对应数目和类型的 <code>tuple</code>。</p> <p>清楚,简洁和方便,对吧 ^_^</p> <hr> <p>目标一明确,就可以开始挽起袖子写代码了。</p> <p>&hellip;&hellip;</p> <p>约过了一炷香时间,(好吧,两柱香 -_-)写好了。现在回头对一下前面提出的接口,能满足我们“既类型安全,又简洁轻便,还有一定灵活性”的需求吗?我们一起来看看吧。</p> <hr> <h2 id="接口">接口</h2> <p>实际写好的功能以类 <code>BtArgParser</code> 的形式提供:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span><span class="lnt">9 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">BtArgParser</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="k">public</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="n">BtArgParser</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">args</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="k">template</span><span class="o">&lt;</span><span class="k">class</span><span class="err">... </span><span class="nc">Args</span><span class="o">&gt;</span> <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">Args</span><span class="p">...</span><span class="o">&gt;</span> <span class="n">parse_tuple</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">template</span><span class="o">&lt;</span><span class="k">class</span><span class="err">... </span><span class="nc">Args</span><span class="o">&gt;</span> <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">Args</span><span class="p">...</span><span class="o">&gt;</span> <span class="n">parse_tuple_tolerated</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">template</span><span class="o">&lt;</span><span class="k">class</span><span class="err">... </span><span class="nc">Args</span><span class="o">&gt;</span> <span class="kt">bool</span> <span class="n">parse_tuple_no_throw</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">Args</span><span class="p">...</span><span class="o">&gt;*</span> <span class="n">out_tuple</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个类的构造函数接收一个命令行字符串,然后用户可以选择使用三个成员函数之一,完成解析的任务。</p> <p>这时候你要问了,不是说好的一个接口吗,怎么多出了两个来?</p> <p>别急,且容俺一一道来。</p> <p>我们知道,根据经验,传入的字符串一般是用户在终端敲进去的。既然是用户输入,就有出错的可能,可能敲少了,敲多了,命令敲错了,类型弄错了,等等等等……咱们的 <code>parse()</code> 函数需要在遇到情况的时候,告诉用户出错了。瞅瞅前面的用例,返回值已经被 <code>tuple</code> 占据了,难道让我们学 <code>map::insert</code> 那样,返回一个 <code>pair&lt;iterator, bool&gt;</code> 吗?(喂喂喂,说好的简洁轻便呢?)</p> <p>这里通过提供三个命名迥异的函数,提供不同的处理策略。</p> <ol> <li><strong>parse_tuple()</strong> 返回 <code>tuple</code> ,遇到错误时抛对应的异常(同标准库大多数函数的行为一致)</li> <li><strong>parse_tuple_tolerated()</strong> 返回 <code>tuple</code>,遇到错误尽可能恢复,并尽可能返回有效数据,不报错</li> <li><strong>parse_tuple_no_throw()</strong> 不抛异常,但返回 <code>bool</code> 表示是否解析成功,<code>tuple</code> 以参数返回</li> </ol> <p>这个看起来啰嗦的方案,实际上是让代码自带注释属性,而且顺便做到对查找友好 (grep-friendly) 的。来吧,跟我一起念,代码可读性是程序员生活质量的保证~~</p> <hr> <h3 id="parse_tuple-标准版">parse_tuple() 标准版</h3> <p>第一个函数 <code>parse_tuple()</code> 没啥好说的,使用姿势就和前面的接口描述几乎一致:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="n">BtArgParser</span> <span class="nf">p</span><span class="p">(</span><span class="n">args</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="k">auto</span> <span class="n">v</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">parse_tuple</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">float</span><span class="o">&gt;</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><p>注意这是抛异常的版本,如果不手动 try / catch 处理错误的话,当发生错误时会一层一层 unwind 直到程序退出或被外层捕获。这里可能抛出的异常都在 <code>std::exception</code> 之下。</p> <hr> <h3 id="parse_tuple_tolerated-容错版">parse_tuple_tolerated() 容错版</h3> <p>从上面可以知道,第二个函数 <code>parse_tuple_tolerated()</code> 会尽可能地把传进来的字符串和需求方 <code>tuple</code> 内的变量类型匹配,那么具体的策略是什么呢?如果命令行提供了多余的参数,那么直接忽略;如果参数不够,那么就补充空的字符串参数;如果类型不匹配,那么使用 0 (对应 int 和 float) 和 &quot;&quot; (对应 <code>std::string</code>) 去构造默认值。总之,能恢复就恢复,能分析出多少分析多少。</p> <p>如下所示,当运行这一句代码时:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">auto</span> <span class="n">v</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">parse_tuple_tolerated</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">float</span><span class="o">&gt;</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><p>有意地提供下图中黑体字部分以 ed scale 开头的两条(不匹配的)命令</p> <ol> <li>ed scale <strong>foo 1.32 bar</strong></li> <li>ed scale <strong>foo bar</strong></li> </ol> <p>(注意,黑体字部分将被解析为三个参数依次为 &lsquo;string&rsquo;, &lsquo;int&rsquo; 和 &lsquo;float&rsquo; 的 <code>tuple</code>):</p> <p><img src="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/info_2.png" width="525" height="312" srcset="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/info_2_hu13264ae7b2a302f5494cf6016690115c_11030_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/info_2_hu13264ae7b2a302f5494cf6016690115c_11030_1024x0_resize_box_3.png 1024w" loading="lazy" alt="info_2" class="gallery-image" data-flex-grow="168" data-flex-basis="403px" ></p> <p>紧接着命令的一行输出是返回的解析结果。简单描述一下,</p> <p>第一次匹配 (匹配内容为 <code>&quot;foo 1.32 bar&quot;</code>) 时,</p> <ul> <li><code>&quot;foo&quot;</code> 解析为 <code>std::string</code> 分析成功</li> <li><code>&quot;1.32&quot;</code> 解析为 <code>int</code> 强转为 1 分析成功</li> <li><code>&quot;bar&quot;</code> 解析为 <code>float</code> 失败,得到使用 0 构造的 0.0</li> </ul> <p>结果是能转换的就转换,转不了的返回 0。</p> <p>第二次匹配 (匹配内容为 <code>&quot;foo bar&quot;</code>) 时,</p> <ul> <li><code>&quot;foo&quot;</code> 解析为 <code>std::string</code> 分析成功</li> <li><code>&quot;bar&quot;</code> 解析为 <code>int</code> 失败,得到 0</li> <li>第三个参数没提供,直接返回 0.0</li> </ul> <p>结果是参数数目不符时,用零补足。</p> <p>可以看到,使用 <code>parse_tuple_tolerated()</code> 来做高容忍度的匹配,总会返回有效的 <code>tuple</code> (当然里面的值并不总是有意义)。有 Lua 经验的程序员可能已经看出来了,这与 Lua 处理函数参数时的容错机制 (不够就补足,多了就忽略) 是类似的。</p> <hr> <h3 id="parse_tuple_no_throw-静音版">parse_tuple_no_throw() 静音版</h3> <p>呼,喝口水,接着说第三个函数 <code>parse_tuple_no_throw()</code> 。这个函数除了把抛异常改为返回 <code>bool</code> (以及由此导致的使用参数来返回 <code>tuple</code>) 以外,内部行为与第一条保持一致。需要补充说明的是,仅靠返回 <code>bool</code> 来表示是否成功,一些情况下是不够的。实践中我把错误细节打印到了日志中,当需要调试的时候可以查看日志,如下所示:</p> <p>运行代码:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">float</span><span class="o">&gt;</span> <span class="n">v</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">p</span><span class="p">.</span><span class="n">parse_tuple_no_throw</span><span class="p">(</span><span class="o">&amp;</span> <span class="n">v</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>然后有意地提供下面图中黑体字部分的命令行参数,就可以看到如下图中的报错信息了</p> <p><img src="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/info_1.png" width="803" height="308" srcset="https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/info_1_hua33ff95ce16c591937eeee10d40aee86_18081_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-12-22-std-tuple-arg-parser/info_1_hua33ff95ce16c591937eeee10d40aee86_18081_1024x0_resize_box_3.png 1024w" loading="lazy" alt="info_1" class="gallery-image" data-flex-grow="260" data-flex-basis="625px" ></p> <p>可以看到,</p> <ul> <li>第一次,我们传了 <code>&quot;foo&quot;</code> 和 <code>&quot;bar&quot;</code> 两个字符串进来,报了错误 <code>std::logic_error</code>:参数个数不匹配,需要三个,传了两个。</li> <li>第二次我们传了 <code>123</code>,<code>&quot;foo&quot;</code> 和 <code>&quot;bar&quot;</code> 三个参数进来,又报 <code>std::bad_cast</code> 的错了:有类型转换失败,具体细节是第 1 个参数从 <code>std::string</code> 到 <code>int</code> 转换失败的。注意,这里的第一个实际上是第二个,我们保持 C/C++ 的传统,从 0 开始计数。</li> </ul> <p>在这一例中,123 被成功地解析成字符串 <code>&quot;123&quot;</code> 而解析第二个参数 <code>foo</code> 时遇到了错误,无法转成 <code>int</code>,就报错并返回 <code>false</code> 退出了。</p> <hr> <h2 id="细节">细节</h2> <p>好了,介绍到这里,本文就应该结束了。下次如果时间充裕的话,我们再来聊一下 <code>BtArgParser</code> 内部的实现吧。有兴趣自己看的同学,代码的传送门在<a class="link" href="https://github.com/gl-notes/gln-public/blob/master/%28gl-bits-prior-to-2017%29/%282015%29%2001.%20BtArgParser%20%E5%AE%9E%E4%BD%9C" target="_blank" rel="noopener" >这里 (BtArgParser) </a>。预备知识是 <a class="link" href="http://en.cppreference.com/w/cpp/utility/tuple" target="_blank" rel="noopener" ><code>std::tuple</code></a>,<a class="link" href="http://en.cppreference.com/w/cpp/utility/tuple/tuple_size" target="_blank" rel="noopener" ><code>std::tuple_size</code></a> 和 <a class="link" href="http://en.cppreference.com/w/cpp/utility/tuple/tuple_element" target="_blank" rel="noopener" ><code>std::tuple_element&lt;...&gt;::type</code></a>,以及<a class="link" href="https://en.wikipedia.org/wiki/Variadic_template" target="_blank" rel="noopener" >可变参模板</a>,<a class="link" href="https://en.wikipedia.org/wiki/Partial_template_specialization" target="_blank" rel="noopener" >模板偏特化</a>及<a class="link" href="https://en.wikipedia.org/wiki/Compile_time_function_execution" target="_blank" rel="noopener" >模板递归</a>的语意,这几点如果有疑问的话,可以点对应的链接进去了解一下。</p> <hr> <p>不多说了,要回去继续撸代码了。不得不说,写代码比写文章好玩多了~~</p> <p>[注]</p> <ul> <li>本文同时发在我的知乎专栏 (<a class="link" href="http://zhuanlan.zhihu.com/gu-lu/20438789" target="_blank" rel="noopener" >链接</a>)</li> </ul> 2015.12 小故事二则 https://gulu-dev.com/post/2015-12-18-two-stories/ Fri, 18 Dec 2015 06:42:00 +0000 https://gulu-dev.com/post/2015-12-18-two-stories/ <p>前段时间读了一篇短文《time》,随手翻译了一下。后来又读到了另一篇,年底了,放在一起发上来吧。</p> <hr> <p>第一个小故事:</p> <hr> <p>儿子:“爸爸,我能问一个问题吗?” 父亲:“当然喽,什么问题?” 儿子:“爸爸,你一个小时挣多少钱呢?” 父亲:“这跟你没关系。为什么你要问这个?” 儿子:“我就是想知道。告诉我好吗,你一个小时挣多少钱?” 父亲:“如果你非要知道的话,我一个小时挣 $100。” 儿子:“喔 (低下头)” 儿子:“爸爸,那你能借给我 $50 吗?” 父亲生气了。 父亲:“如果你问这个是想要钱去买些玩具什么的,现在就回你自己房间床上去。好好想想吧,自己怎么这么自私。我每天那么忙就为了满足这样的无理需求。”</p> <p>小男孩回到自己的房间,关上了门。 父亲坐了下来,开始变得更生气了。他怎么敢问这样的问题,就为了能弄点钱? 过了个把钟头,父亲平静了下来,开始想: 也许他是真的很想要这 $50 来买个东西,平常他不怎么要钱的。父亲打开了门,走进了儿子的房间。</p> <p>父亲:“儿子,你睡着了吗?” 儿子:“没有,我还醒着呢,爸爸。” 父亲:“我是在想,刚才我说得重了点儿。现在我不生气了,这是你要的 $50,拿去吧。”</p> <p>小男孩坐起来,笑了。 “谢谢你,爸爸!” 他拉开枕头,从下面扯出来一些皱皱巴巴的零钱。父亲看到小男孩原来是有钱的,就又开始生气了。小男孩慢慢地数清了自己的钱,抬头看自己的父亲。</p> <p>父亲:“你自己有钱怎么还问我要?” 儿子:“因为我的钱不够,不过现在够了。”</p> <p>“爸爸,现在我有 $100 了。我能买一个小时你的时间吗?明天早点回来,我真的很想跟你一起吃晚饭。”</p> <p>父亲呆住了,他抱住了自己的孩子,请他原谅自己。</p> <p>对所有努力工作的人,这只是一个小提醒。我们不应任由时光匆匆逝去,忽视了那些真正重要的人,那些离我们的心更近的人。如果你有值 $100 的时间,记得跟你爱的人分享吧。如果明天就死去,你所工作过的公司在几天之内就能找到替代的人选。但我们身后的家人和朋友将会用余生来感受缺憾。想想吧。</p> <p>有些事情更重要。</p> <hr> <p>(附原文)</p> <p>SON: &ldquo;Daddy, may I ask you a question?&rdquo; DAD: &ldquo;Yeah sure, what is it?&rdquo; SON: &ldquo;Daddy, how much do you make an hour?&rdquo; DAD: &ldquo;That&rsquo;s none of your business. Why do you ask such a thing?&rdquo; SON: &ldquo;I just want to know. Please tell me, how much do you make an hour?&rdquo; DAD: &ldquo;If you must know, I make $100 an hour.&rdquo; SON: &ldquo;Oh! (With his head down). SON: &ldquo;Daddy, may I please borrow $50?&rdquo; The father was furious.</p> <p>DAD: &ldquo;If the only reason you asked that is so you can borrow some money to buy a silly toy or some other nonsense, then you march yourself straight to your room and go to bed. Think about why you are being so selfish. I work hard everyday for such this childish behavior.&rdquo;</p> <p>The little boy quietly went to his room and shut the door. The man sat down and started to get even angrier about the little boy&rsquo;s questions. How dare he ask such questions only to get some money? After about an hour or so, the man had calmed down, and started to think: Maybe there was something he really needed to buy with that $ 50 and he really didn&rsquo;t ask for money very often. The man went to the door of the little boy&rsquo;s room and opened the door.</p> <p>DAD: &ldquo;Are you asleep, son?&rdquo; SON: &ldquo;No daddy, I&rsquo;m awake&rdquo;. DAD: &ldquo;I&rsquo;ve been thinking, maybe I was too hard on you earlier. It&rsquo;s been a long day and I took out my aggravation on you. Here&rsquo;s the $50 you asked for.&rdquo;</p> <p>The little boy sat straight up, smiling. SON: &ldquo;Oh, thank you daddy!&rdquo; Then, reaching under his pillow he pulled out some crumpled up bills. The man saw that the boy already had money, started to get angry again. The little boy slowly counted out his money, and then looked up at his father.</p> <p>DAD: &ldquo;Why do you want more money if you already have some?&rdquo; SON: &ldquo;Because I didn&rsquo;t have enough, but now I do.</p> <p>&ldquo;Daddy, I have $100 now. Can I buy an hour of your time? Please come home early tomorrow. I would like to have dinner with you.&rdquo;</p> <p>The father was crushed. He put his arms around his little son, and he begged for his forgiveness.</p> <p>It&rsquo;s just a short reminder to all of you working so hard in life. We should not let time slip through our fingers without having spent some time with those who really matter to us, those close to our hearts. Do remember to share that $100 worth of your time with someone you love? If we die tomorrow, the company that we are working for could easily replace us in a matter of days. But the family and friends we leave behind will feel the loss for the rest of their lives. And come to think of it, we pour ourselves more into work than to our family.</p> <p>Some things are more important.</p> <hr> <p>第二个小故事:</p> <hr> <p>在我四岁那年,父亲送了我一台Xbox。你们了解的,如果我没记错的话那是2001年的款式,一个黑色硬梆梆的盒子。我和父亲一起玩了很多游戏,非常开心,直到两年后,我的父亲去世了。</p> <p>之后的十年时光里,我再也没有碰过这台游戏机。</p> <p>然而当我再度启动它时,我发现了一些事情&hellip;&hellip;</p> <p>我和父亲曾经一起玩过一款赛车游戏叫《越野挑战赛》,在当时,这真的是款很好玩的游戏。</p> <p>就在我重新启动这款游戏时,我发现了一个真正的幽灵!</p> <p>这款游戏有个奇妙的设定,上一轮比赛中最快的选手的影子将会出现在接下来的比赛中,与选手一起参赛,就是所谓的“幽灵驾驶者”。</p> <p>我想你一定猜到了。没错,当年我父亲的幽灵至今仍然在赛道上奔驰着。</p> <p>于是我一遍又一遍的玩着,试图打败这个幽灵,慢慢的,我终于接近了它的速度,甚至直到有一天我超过了它,然后&hellip;&hellip;</p> <p>我在终点线前停了下来,这样爸爸的幽灵就不会消失了。</p> <p>(附原文)</p> <p><img src="https://gulu-dev.com/post/2015-12-18-two-stories/story.jpg" width="600" height="432" srcset="https://gulu-dev.com/post/2015-12-18-two-stories/story_hub8f61d9294676fdf092ffb2f13f362ee_77734_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-12-18-two-stories/story_hub8f61d9294676fdf092ffb2f13f362ee_77734_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="story_original" class="gallery-image" data-flex-grow="138" data-flex-basis="333px" ></p> 2015.11 关于文件摘要的引申讨论 - 资源的引用管理之二 https://gulu-dev.com/post/2015-11-14-ref-management-2/ Sat, 14 Nov 2015 08:55:00 +0000 https://gulu-dev.com/post/2015-11-14-ref-management-2/ <p>前段时间发了一篇<a class="link" href="http://gulu-dev.com/post/2015-11-01-ref-management" target="_blank" rel="noopener" >利用文件摘要简化游戏资源的引用管理</a>,后来在知乎专栏的评论里与 Jare Guo 同学在这个话题上有一些后续的讨论 (见<a class="link" href="http://zhuanlan.zhihu.com/gu-lu/20311224" target="_blank" rel="noopener" >此页面</a>),俺觉得这个讨论挺有收获。对这方面感兴趣的同学来说,也许是一个参考,所以放到一起发上来。如果大家觉得有错误,补充,以及更好的想法,欢迎指正。</p> <hr> <p><strong>Jare Guo [2015-11-09 18:05 星期一]</strong></p> <p>非常棒的一篇文章,但我心里有些不同的看法,欢迎指正: path:md5 方案,经不起移动路径的同时修改资源。这造成的后果是修改资源的人,一定要在每一步操作后,回编辑器中让编辑器刷新下,再进行下一步操作。如果编辑器未启动,或未监听改动,提交上去后 reference 就会失效,所有人更新下来都会失效。</p> <p>而一旦发生这种事情,在 git 中是查不到的,因为没有明显的出错的特征。(如果用 uuid,meta file 出错了一看就知道)</p> <p>这也注定了 path:md5 的方案无法适应大型项目的团队配合,它要求所有人必须在一个大工程里,因为&quot;修改资源的人&quot;每次有操作必须帮助所有&quot;使用资源的人&quot;来提交新的 reference。(这个人可能仅仅有一个子 repo 的提交权限,也不关心其它团队把资源用在了什么地方。)</p> <p>同理,path:md5 也无法满足资源包导出的需求。</p> <p>所以我觉得 uuid 虽然坑,但还是有不可取代的地方……</p> <hr> <p><strong>Gu Lu [2015-11-09 22:00 星期一]</strong></p> <p>嗯,多谢分享你的思考。首先肯定一下你的基调,文件摘要肯定不是包治百病的万灵丹,只是在特定场合可以发挥作用的一个工具,存在局限也是必然的,比如说循环引用就会出问题——导致循环地刷摘要。当然也可以通过策略或机制去回避,而这里的重点在于某种启发而不是限制——如何去运用这个工具。</p> <p>我们一条一条来看:</p> <blockquote> <p>“修改资源的人,一定要在每一步操作后,回编辑器中让编辑器刷新下,再进行下一步操作。”</p> </blockquote> <p>这是不必的。更准确的描述是,“普通的原地更新,无需顾忌引用者;只有当对资源 rename/move 后 <strong>紧接着要继续更新该资源</strong> 时,才需要立即刷新。” 换句话说,路径一致的话可以不考虑 md5,而平常制作阶段,普通的原地更新是 80% 的情况。</p> <p>有人可能会说,“只要路径一致就认为是同一个资源,这不严谨啊,如果把资源挪走,用另一个替换掉,还用这个名字,不就引用错了吗” 想想文件系统吧,你在桌面建了个快捷方式指向 C:\foo.txt,现在你把这个文件挪到 D 盘,而把另一个 bar.txt 重命名成 foo.txt 放到 C:\ 冒充,这时候你双击桌面的快捷方式,还指望它能自动找到已经到了 D 盘的那个文件,这就不科学了吧,如果一定要实现这个需求,就需要更高级的工具或机制。</p> <blockquote> <p>“一旦发生这种事情,在 git 中是查不到的,因为没有明显的出错的特征”</p> </blockquote> <p>不太明白你说的 “引用失效在 git 里查不到” 是什么意思,引用者内含的明文路径天然可以帮助定位有问题的资源,到那个资源所在的路径看一下历史记录就知道谁动过了这个资源。当然,也可能是我误解了你到意思。</p> <p>明确地讲,当资源 rename/move 后,由于引用者含有明文的路径,当引用失效时,你可以在编辑器中弹框提示 &ldquo;C:\foo\bar.txt&rdquo; 找不到 (就如同操作系统中找不到文件时发生的那样) 当然也可以借助摘要去全库范围搜一下,以一些时间开销来换取可能的自动更新。</p> <blockquote> <p>“path:md5 的方案无法适应大型项目的团队配合,它要求所有人必须在一个大工程里,因为&quot;修改资源的人&quot;每次有操作必须帮助所有&quot;使用资源的人&quot;来提交新的 reference”</p> </blockquote> <p>这是很典型的“把一整套解决方案 (此例中的编辑器) 中的单个机制提出来,批评它解决不了一般性的工作流问题”的观点。需要注意,工作流问题千变万化,需要不同的策略,机制和约束在一起发挥作用。我只点明一点,所有的资源引用的基础仍是路径,md5 的作用是当路径失效时“潜在的”自动发现和重定位可能的目标资源,目的是在大多数情况下,能辅助开发者去自动地批量地解决这类失效。并不是说有了这个就可以乱搞了,这个逻辑我认为不难理解——你的 foo.exe 依赖同目录下的 foo.config 才能正常启动,现在你一声不吭把 foo.config 删了,还要求 foo.exe 完全正常,臣妾做不到啊 ( foo.config :“怪我咯” )。</p> <blockquote> <p>“path:md5 也无法满足资源包导出的需求”</p> </blockquote> <p>chunked md5 恰恰在文件包内单个或多个资源更新时非常有用,可以有效地与增量更新配合,把引用自动更新到增量更新包上去。所以说到这一句就可以明白地看出我们思路的不同,我更关注的是“这个技术在特定的情况下能帮到我们什么”,而不是一个笼统的“这个技术无法满足需求”</p> <p>技术是为程序员服务的,而程序员才是为需求服务的,不要跳过中间的步骤哦 O(∩_∩)O~~</p> <hr> <p><strong>Jare Guo [2015-11-09 23:23 星期一]</strong></p> <p>我又来了;)</p> <blockquote> <p>路径一致的话可以不考虑 md5,而平常制作阶段,普通的原地更新是 80% 的情况。</p> </blockquote> <p>md5 改变后,是没什么大问题。但如果不到编辑器里把所有原先引用的 md5 也同步刷新的话,下次如果有人在不知情的情况下直接移动这个文件,那么原来的引用就会面临 md5 和 path 同时失效的情况。</p> <p>因此就会鼓励资源生产者在每次修改后,养成刷新 path/md5 的习惯。</p> <p>但如果这样一来,就又会面临另一个囧况,美术一旦修改了一张图片,可能好几个场景都要刷新 md5,一方面容易往场景的版本管理中加入了无用的历史提交,另一方面很容易产生冲突!</p> <p>PS: 正如你说的,如果这几个场景还有别人也引用到了,那么它们的 md5 也得跟着刷新……</p> <blockquote> <p>一旦发生这种事情,在 git 中是查不到的,因为没有明显的出错的特征</p> </blockquote> <p>(这个问题是我碰巧想到,太钻牛角尖了。) path:md5 出错时确实可以在编辑器里面给出各种提示,但如果单纯 review 他人的工作,是没办法像 meta file 那么简单的能够发现问题的。</p> <ul> <li>meta file 只要保证和源文件一起移动,并且 uuid 保持不变就行。</li> <li>而 path:md5,哪怕你看到一个美术修改了若干资源,并且顺带改了一堆别的资源,但你无从判断那里面有哪些 md5 是失效的。</li> </ul> <blockquote> <p>我只点明一点,所有的资源引用的基础仍是路径,md5 的作用是当路径失效时“潜在的”自动发现和重定位可能的目标资源</p> </blockquote> <p>没错,但这个机制赖以生存的前提是 md5 的及时更新,如果不按照我说的,资源&quot;生产者&quot;负责任(越权?)的帮资源&quot;使用者&quot;去更新 md5,那么这个机制就不太靠谱。</p> <blockquote> <p>path:md5 也无法满足资源包导出的需求</p> </blockquote> <p>抱歉我的意思并不是为了做增量更新,我指的是要做资源的动态载入。</p> <p>举例:一旦我从网上购买了一个资源包,当这个包的作者很良心的发布更新时,我会发现我的场景也要跟着升级。</p> <ul> <li>如果这时我没有把场景的升级也提交到 git,下回同事就会帮我把这件事情给做了,结果两个人的 git 就会冲突……</li> <li>如果我开心的把新场景提交到了 git,但策划正好在这个场景,他就可能会揍我……</li> <li>如果这个包的作者发布了两次更新,我跳过中间的一次 rename,直接升级到了最新版(改 md5),那么就有可能丢失资源……</li> <li>如果这个包的作者只发布了一次更新,但这个更新用了他不少心血,不但做了 rename,还改了 md5,那么他的更新会害死很多人,而他自己可能不会意识到……</li> </ul> <p>说白了,资源包就是很典型的资源生产者,和资源消费者互不关心的使用场景,这个场景中使用 path:md5 不太合适,我也没说非要用这个不可,只是想到就说了一下。</p> <blockquote> <p>我更关注的是“这个技术在特定的情况下能帮到我们什么”,而不是一个笼统的“这个技术无法满足需求”</p> </blockquote> <p>双手赞成~ 我只是提出了一些 path:md5 不太适合的应用场景,也没说这个技术没有可取之处。事实上如果能把 path:md5 在内网服务器里做一个版本缓存,或者全项目组的人共同维护一份日志文件,那我上述的大部分问题都可以解决。</p> <hr> <p><strong>Gu Lu [2015-11-10 10:19 星期二]</strong></p> <p>嗯,看到你最后提到的方案 (&ldquo;如果能把 path:md5 在内网服务器里做一个版本缓存,或者全项目组的人共同维护一份日志文件&rdquo;),我觉得可以少打不少字就能说清楚了。:) 在你的 git repository 内的任何一个引用者内含的外部文件的摘要,都可以对应到该文件的某个历史版本。也就是说,只要你愿意,拿着这个 md5,你可以像拿着 uuid 一样。而比 uuid 的恒定唯一更进一步,你甚至可以定位到特定的一个时间点。</p> <p>就拿你说的第一个例子来说吧,假设 C:\foo.txt 的摘要是 12345,现在你在 C:\bar.txt 内有前者的引用 &ldquo;C:\foo.txt:12345&rdquo;。不管你对 foo.txt 做什么操作,能计算出 12345 这个摘要的 foo.txt 肯定存在于你的 git history 的这个文件的某个时间点上,这就是我为什么一直强调不必主动刷的根本原因。(因为如果你需要,你总能找到它的) 这也就谈不上后续的纯为了刷 md5 去提交了。 从本质上讲,path:md5 对应到你某个资源曾经存在过的某个快照,利用好这一点,能提供比 uuid 更大的价值和更小的负担。</p> <p>发布问题是一个有必要仔细说一下的问题。</p> <p>当你发布代码的时候,如果你的 v1.0 版有一个接口 int foo() { return 0; } 那么 v1.1 版你是不会随意删掉或者重命名这个接口的,因为这是你跟客户的协议,就算这个更新用了你再多的心血,你也不能随意地破坏这种约定,对吧。</p> <p>现在你发布了一个资源包,内含 pack/a, pack/b, pack/c 三个资源,客户已经用在自己的项目里了。如果你出于某种非做不可的理由把 a 重命名成 d,当然需要通知所有的客户:“新版本里 a 已经被换成 d 了” 升级的人有理由注意到他们依赖的资源发生了这种变化,从而选择更新还是不更新。所以通常更好的选择是不要破坏与用户的约定 pack/a, pack/b, pack/c,如果是重大的改动,提供 pack-1.1/a 就可以了,这就是我说的增量更新。</p> <p>你当然可以说有了 uuid 我怎么挪都无所谓,只要保证跟着挪 uuid 就行,但是,“有这么大的自由度”未必意味着“在与客户的约定中随意利用这种自由度”是合理的。更何况,如果你真正能利用好 “path:md5 对应到你某个资源曾经存在过的某个快照” 这个特性,作为迷你的版本标识符,它能提供给你比 uuid 更大的灵活度,和更小的负担。</p> <hr> <p>欢迎大家再进一步讨论这个问题。文件摘要的想法,我前不久才设计和引入资源管理系统,肯定存在一些未考虑妥当之处。考虑到我目前应用的范围和规模,一些大规模团队的协同问题短期内还不太会遇到,如果有同学能指出潜在的问题,我们就可以有机会做更深入的思考和更细致的考虑,也能形成更健壮的方案。</p> 2015.11 在实际工作中评估你的工程师伙伴 - 写给非技术向小伙伴的参考 https://gulu-dev.com/post/2015-11-05-evaluating-engineer/ Thu, 05 Nov 2015 20:36:00 +0000 https://gulu-dev.com/post/2015-11-05-evaluating-engineer/ <img src="proxy.php?url=https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/evaluation.jpg" alt="Featured image of post 2015.11 在实际工作中评估你的工程师伙伴 - 写给非技术向小伙伴的参考" /><h2 id="先说两句">先说两句</h2> <p>这个易燃易爆的话题,其实俺是不敢写的。在工程师队伍里滥竽充数了几年,俺觉得自己还没被清理和淘汰出去,已经谢天谢地了——不赶紧回去提高姿势水平,居然还在这里装大尾巴狼,大言不惭地讨论如何评估程序员,其心可诛啊。</p> <p>那为何俺还要冒码农之大不讳,在这里妄议是非,大放厥词呢?是因为俺发现,在日常工作中,对于一些非技术向的小伙伴们,由于对工程师文化了解并不多,要么难以寻找到合适的技术伙伴,要么在工作中与工程师难以保持稳定的沟通节奏,对于这些小伙伴,俺愿意把俺知道的分享出来,肤浅也好,片面也罢,总之是多了一点参考,希望能有所帮助。</p> <hr> <h2 id="1-对技术的分析和判断力">1. 对技术的分析和判断力</h2> <p>技术没有先进与落后之分,只有适用与不适用。如果你经常听某个程序员对你说 “xxx 很先进,比 yyy 好多了,应该用 xxx 就不会有这些问题了”,“xxx 非常强力,BAT (此处可换为任何一个公司名) 的 yyy 项目以前用的是 zzz,现在都改用 xxx 了” 那么就要对他的判断力打个问号了。</p> <hr> <p>下面是一些包含更多价值的例子,</p> <ul> <li>“xxx 虽然在 aaa 方面不太好,但比 yyy 更适合我们的项目,因为我们眼下更需要 bbb 和 ccc,将来如果在 aaa 方面出了问题,我们可以做这样的调整: 1&hellip; 2&hellip; 3&hellip;”</li> <li>“我们之所以不用常规方案 xxx,而是用更激进一点的 yyy,是因为我们的项目在 aaa 和 bbb 等方面没有那么强的约束,虽然损失一些 ccc,但是可以获得更高的 ddd 和 eee”</li> </ul> <p>如果你发现一个程序员,行走都把某个特定的平台/工具/技能/编程语言挂在嘴边,“xxx 就是好啊就是好”,你就会知道他不仅有选择上的局限性,也会在技术判断中不可避免的因为已有的积累产生偏见,正所谓“手里握着锤子的时候,满世界长得都像钉子”,一般这种程序员,俺称之为“信徒式程序员”。</p> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/believer.jpg" width="480" height="270" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/believer_hu151ac184545019712057d4640abb9d59_22993_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/believer_hu151ac184545019712057d4640abb9d59_22993_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <h2 id="2-对待预估任务时间的态度">2. 对待预估任务时间的态度</h2> <p>诚实是一个重要的品质。相比起对他人的诚实,对自己的诚实就更难了。</p> <p>举个例子,你答应了两个礼拜后交付一个版本,可是因为某些原因,一个礼拜过去之后,你发现自己还需要两个礼拜,这时你会如何选择呢?</p> <p>诚实的态度是,承认自己的时间预估是不准确的,要么砍掉一部分计划,按期交付,要么保证功能的完整性和质量,延期交付。如果一方面拍胸脯保证能按时交货,另一方面罔顾负荷与节奏,过度地挤压,就是不诚实的表现。这样短期内能获得更好的 KPI,但工程质量,团队士气都会受到打击,长远看得不偿失。诚实余额不足的协调者,惯用这种伎俩,通过各种手段,汲取和消耗组织的能量来为自己的 KPI 充电。更糟的是,这会使组织对团队的能力做出有偏差的判断,进一步放大问题,以致走上一条不归路。</p> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/time-estimation.jpg" width="276" height="183" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/time-estimation_hu54e4c79d4ff1322a26a390bb6012c15d_6795_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/time-estimation_hu54e4c79d4ff1322a26a390bb6012c15d_6795_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="150" data-flex-basis="361px" ></p> <p>正确地预估任务时间,是一项需要经年累积才能有一点点成长和收获的技能。任何一项任务,有经验的开发者给出的不是一个拍脑袋的日期,而是包含以下诸要素的整体判断:</p> <ul> <li>完成核心功能需要的时间预估</li> <li>完成次级功能和周边工具链的时间预估</li> <li>系统集成,稳定化,性能优化的时间预估</li> <li>预留的缓冲周期</li> </ul> <p>有经验的程序员对计划中的弹性点保持敏感,计划会随着实际进展不断调整,不会刻板地拘泥于原计划。他们的计划会尽量做到诚实且忠实地反映实际的进展情况。如果突发情况造成了计划外的影响,能及时地去做出调整,并向需要的人更新状态。</p> <h2 id="3-为自己的工作建立明确的边界">3. 为自己的工作建立明确的边界</h2> <p>所谓“明确的边界”,就是能尽早地建立明确的头脑模型,尽早摸清楚 (在你负责的领域内) 什么必须做,什么可以做,什么不能做,什么做不了。</p> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/outside.jpg" width="400" height="295" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/outside_hu8d88903349cbb57f929a652fbee13049_42977_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/outside_hu8d88903349cbb57f929a652fbee13049_42977_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="135" data-flex-basis="325px" ></p> <p>有明确的边界会带来很多显性和隐性的好处。最重要的好处是,大家的需求关系明确化、协议化,不再依赖私下的潜规则,会节省后续大量的沟通成本。其次,愿意为自己的工作范围建立明确的边界的程序员,对边界内的代码质量通常会有强烈的责任感和主人翁意识,他们会比其他人敏感和熟悉得多,在他们的地盘上,他们能彻底的说了算。相关系统内如果出了问题,让其他人来可能要修两三天,换他来,可能凭着对相关代码的熟悉度,闭着眼睛就说出几个可能的问题点。</p> <p>充满各种潜规则的系统非常脆弱,如果你发现一个程序员总在焦头烂额地处理跟其他程序员的沟通事务,往往就意味着隐患已经浮现出来了。缺乏边界意识的程序员,往往伴随着对代码较低的责任心。他们不把自己工作的代码看做是“自己的”,自然也就不会尽心尽力地去好好维护。</p> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/substitution.jpg" width="444" height="270" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/substitution_hu3d03a01dcc18bc5be0e67db3d8d209a6_22851_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/substitution_hu3d03a01dcc18bc5be0e67db3d8d209a6_22851_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="164" data-flex-basis="394px" ></p> <p>有种流行的说法是,应该通过某种形式的“轮换”,把程序员打造成“可轮换的”螺丝钉和多面手,以降低人员流失带来的风险。也许这一方式对其他行业会很有效,但至少在游戏行业,我发现,被这样轮换的程序员,这样来过几次之后,确实是对彼此的系统更熟悉了没错,但他们变得不再像之前那样“精心地”照料自己原本负责的那片代码。因为被 n 个人按自己的思路修理过之后,他们已经失去了“对那片代码的爱”,他们逐渐慢慢地从“园丁”变成了“游客”。随着时间的推移,没有人对这个模块内部的所有可能状态了如指掌,曾经被精心照顾的花园慢慢地变成了废弃的垃圾场。每个人改到这里都是无奈地捂着鼻子赶紧弄完了事,只要不要弄出新的问题就谢天谢地。这种腐化通常是加速而且不可逆的,从工程角度来讲,这往往预示着这一堆代码的废弃。更残酷的是,程序员们纷纷发现在这个项目里无法积累真正的领域相关经验,成为专家,还有什么比天天围着一辆二手车的不同部位反复修理又得不到更多锻炼机会而成长更糟糕的事呢?他们唯一合情合理的选择,就是拂袖而去,或是准备以温和一点的方式拂袖而去。</p> <p>程序员对代码的爱是一个项目里最稀缺的资源之一,不要忽略它,更不要粗暴地碾碎它,要通过创造稳定的环境和氛围去创造它,培育它,这样的代码库才能成为肥沃的土壤,带来源源不断的回报;否则就会变成一个摇摇欲坠、四处冒烟的,靠巧合来得以运行的庞然大物。</p> <h2 id="4-对异端的相容度">4. 对“异端”的相容度</h2> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/dispute_resolution.jpg" width="380" height="304" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/dispute_resolution_hu2c8d7d9d9978705dee8a6f346caa11db_13860_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/dispute_resolution_hu2c8d7d9d9978705dee8a6f346caa11db_13860_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="125" data-flex-basis="300px" ></p> <blockquote> <p>「托利得定理:测验一个人的智力是否属于上乘,只看脑子里能否同时容纳两种相反的思想而无碍于其处世行事。」</p> </blockquote> <p>这句话有三层递进的意思:</p> <ol> <li>具有包容性。能够理解矛盾各方的情势,对盲点敏感,不容易被蒙蔽。</li> <li>具有批判性思维。能用理性和逻辑等工具去分析问题,形成自己的判断,不盲从。</li> <li>具有人格独立性。依本性从事,不存偏见。</li> </ol> <p>这三样对于程序员来说,都是比较重要的软素质。寸步不让,主导意识强烈的程序员,往往有着惊人的自信——要注意这种自信有时会成为双刃剑。在讨论中展现出不必要的自信,往往在令人屈服的同时,促使对方从“奉献者”转变为“打卡者”,降低整个团队的战斗力。</p> <h2 id="5-对每一项提交记录-日常工作成果-的态度">5. 对每一项提交记录 (日常工作成果) 的态度</h2> <p>有经验的开发者从一个程序员的提交记录里,能读出太多的东西:</p> <ul> <li>对于高素质的开发者,你能不费太大力气就能顺畅地读出他近期做了哪些方面的工作;在哪些点上曾遇到了困难,选择的解决方案是什么;当他权衡和折衷时,考虑的最多的是什么因素;在一段时间内,贡献相对均匀 (DPS 能保持在相对稳定的节奏)</li> <li>而相对的,如果你在提交记录里看到不少 “临时方案”,“暂时先注掉”,“先跑起来晚点再改” 这样的句式,或者提交信息简短到像发电报,或者提交一大堆代码却不写提交信息,那么这样的程序员的实际贡献意愿和实际产出结果就要打个问号了。</li> </ul> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/commit-history.png" width="480" height="309" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/commit-history_hu80d0984b14e29ef6234c6693252881ae_53338_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/commit-history_hu80d0984b14e29ef6234c6693252881ae_53338_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="155" data-flex-basis="372px" ></p> <p>查看一个程序员的 commit history,一眼扫过去,如果除了 “完成了xx功能” “修复了 xx bug” 等信息之外,还有一些 “改善了 xxx 的内部结构,方便后期维护”,“简化了 yyy 的接口,降低了跟 aaa 和 bbb 的沟通负担”,“自动化或移除了 zzz 的操作,省去了跟 ccc 的额外沟通和确认的步骤” 你就可以知道,这个程序员不仅业务能力过硬 (勉强能完成任务的程序员,是很难有余力去操心这些事的),而且对项目也有足够的责任心和爱心,愿意把自己的真正心血奉献给项目,而不是想方设法总是凑合和应付,“先弄出来再说,哪管之后洪水滔天”。</p> <p>其实,这也是最快的考察工程师团队的一个方法——一个团队的做事方式,效率,甚至是士气和状态,从 commit message 上能读出大量的细节,而不少信息是从平常的交流里无法提取和判断的。</p> <h2 id="6-对待交付的态度">6. 对待交付的态度</h2> <p>有经验的程序员明白,<strong>提交功能的那一刻,只是交付的开始</strong>。这一点具体请详见这一篇:<a class="link" href="http://gulu-dev.com/post/2014-03-22-dont-lie" target="_blank" rel="noopener" >不要说谎 (Don&rsquo;t lie.)</a>,这里就不多说了。</p> <h2 id="7-对待技术欠债的态度">7. 对待技术欠债的态度</h2> <p>一个快速前进的团队不可避免地会留下或多或少的技术欠债,这是敏捷所要付出的最大成本之一。并非所有的技术欠债都需要偿还,它们中相当一部分实际上会随着开发风向的变化被直接风干和丢弃。有经验的程序员会时常为自己的日常工作留出处理技术欠债的时间,他们会及时查遗补缺,趁着手头任务的余温,补上因为快速开发而遗漏的东西,同时果断抛弃那些越积越重的包袱,尽量轻装上阵。</p> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/tech-debt.jpg" width="500" height="328" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/tech-debt_hu3e44db650a40e9324ebfaf3cab4650c3_78982_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/tech-debt_hu3e44db650a40e9324ebfaf3cab4650c3_78982_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="152" data-flex-basis="365px" ></p> <p>如果你观察到一个程序员总是乐于做新东西,他的提交记录里缺乏对已有系统的梳理和重构,那说明他更适合作为突击队员,还不能适应作为一个组织者的角色。</p> <h2 id="8-对待人员流失的态度和处理方式">8. 对待人员流失的态度和处理方式</h2> <p>有经验的程序员永远不会故作惊讶,对同事或下属的离职假装出惊讶的样子。他们总是“Work for the best, prepare for the worst”。他们在过去的经历里吃过亏,明白“靠山山会倒,靠人人会跑”,所以早在一个生力军加入自己负责的团队之前,他们就会计划好此人一旦离开时的善后处置工作。他们不会轻易让自己的团队没有原则地进人,也就不会让自己负责的业务因为某一个人的离职变得失控。</p> <p><img src="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/handover.jpg" width="333" height="160" srcset="https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/handover_hu6c585de50b65874c6f3c96f8788030f5_6170_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-11-05-evaluating-engineer/images/handover_hu6c585de50b65874c6f3c96f8788030f5_6170_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="208" data-flex-basis="499px" ></p> <p>但这绝不是在说他们对人员流动无所谓,不会主动捍卫自己团队成员的利益。正相反,他们会重视每一个做出贡献的成员,竭尽所能地为他们争取应得的尊重;他们会把哪怕只贡献过一行代码的离职同事都仔细地整理出来,放在 Credits 里的合适位置,以确保他们的工作得到认可;即使某些成员在自己负责的团队内表现不佳,当这些成员离开时,他们尊重个体的选择,并仍愿意给予力所能及的最大的帮助,因为他们清楚地知道,同一个人在不同的环境下,也有可能激发出完全不同的能量。</p> <h2 id="进阶阅读">进阶阅读</h2> <p>读完这一篇以后,如果觉得俺写的水分太多没什么卵用的话,可以移步这里,读一下这篇进阶读物,“<a class="link" href="http://zhuanlan.zhihu.com/gu-yu/20232566" target="_blank" rel="noopener" >What makes a good lead programmer</a>”。</p> <hr> <p>嗯,差不多就是这些吧。对于还在寻觅中的小伙伴,希望这些文字能够帮助你更好地判断,锁定和捕捉适合自己的野生程序猿 O(∩_∩)O~~ 而对想多了解一下工程师的小伙伴,也希望能促进几分彼此的理解和包容 &lt;( ̄︶ ̄)&gt;</p> <p>[注]</p> <ul> <li>本文同时发在我的知乎专栏 - 游戏人间 (<a class="link" href="http://zhuanlan.zhihu.com/gu-lu/20321980" target="_blank" rel="noopener" >链接</a>)</li> <li>本文遵循 <a class="link" href="http://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener" >Creative Commons BY-NC-ND 4.0</a> 许可协议。</li> </ul> <hr> <p>[2015-11-07] 补:</p> <p>有同学提到,</p> <blockquote> <p>有些地方不敢苟同,技术还是分先进和落后的,例如百度推送和个推</p> </blockquote> <p>这里俺说明一下,新的技术总会逐渐取代老的技术,这个是发展的必然,所谓“技术没有先进落后”,着眼点在于“平等而不含偏见地”对待各种技术,以“是否适用具体情况”来择而用之。</p> 2015.10 利用文件摘要简化游戏资源的引用管理 https://gulu-dev.com/post/2015-10-31-ref-management/ Sun, 01 Nov 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-10-31-ref-management/ <p>由于微信会对外部页面重新排版,在微信内置浏览器中,本文会出现下面的异常:</p> <ul> <li>文中的链接无法正常访问</li> <li>文中的代码段落多出了许多 html tag,变得不可读</li> </ul> <p>请选择右上角菜单“在浏览器中打开”即可正常阅读。(本文的<a class="link" href="http://gulu-dev.com/post/2015-11-01-ref-management" target="_blank" rel="noopener" >永久链接</a>)</p> <hr> <p><img src="https://gulu-dev.com/content/post/2015/2015-10-31-ref-management/images/title.jpg" loading="lazy" alt="p" ></p> <p><em>(此图引自 <a class="link" href="https://walledcity.com/supermighty/understanding-go-dependency-management" target="_blank" rel="noopener" >Understanding Go Dependency Management</a>)</em></p> <p>资源的引用管理是个有趣的话题,最近我在代码里实践了一种做法,可以在某些方面简化资源的管理,完成之后简单记录在这里。这篇文章先介绍传统的各种方式,然后简单说明一下,这个实践在传统方式的基础上做了哪些改善,解决了什么问题。</p> <hr> <h2 id="引子">引子</h2> <p>游戏开发中的资源管理,通常是指针对游戏中的各类资源数据 (模型,贴图,脚本,数据表等等),通过合理安排布局来提高资源访问的效率,进而改善游戏体验的过程。在布局方面的一些实践,譬如“如何区分对待不同的资源类型,如何做到更新友好”等等,这里就不详细讨论了。今天主要谈一下在大量资源已合理布局的情况下,如何有效地处置它们相互之间巨量的依赖和引用关系的问题。</p> <p>简单地说,<strong>如果 A 引用了 B,那么应该如何简洁有效地表达这种引用呢?</strong></p> <p>有经验的开发者知道,这个问题并不像看上去这么简单。随着资源量的剧增,以及牵扯到的工作流程的细碎化,如果处置不善,资源引用问题会成为影响整个架构的根本性问题。</p> <hr> <h2 id="传统实践">传统实践</h2> <h3 id="方式-i---基于偏移-指针-的引用">方式 I - 基于偏移 (指针) 的引用</h3> <p>文件偏移 (file-offset) 应该是最基本最原始的引用方式了。在一个运行着的 C/C++ 程序中,通常我们通过在对象 A 中存储指针来引用对象 B。如果在序列化时,把这种指针引用以文件偏移的形式直接写入文件,就是最原始的资源引用管理。</p> <p><img src="https://gulu-dev.com/addr_in_file.jpg" loading="lazy" alt="p" ></p> <p><em>(此图引自 <a class="link" href="https://root.cern.ch/root/html534/guides/users-guide/InputOutput.html#pointers-and-references-in-persistency" target="_blank" rel="noopener" >ROOT User’s Guide - 13.4 Pointers and References in Persistency</a>)</em></p> <p>这种最原始的依赖管理,细分一下还有两种形式:</p> <ol> <li>每个对象的地址 (&amp;object) 被一并存下来用作该对象的 ID (顺便保证了全局唯一),将引用者写入文件时,如果出现被引用者的指针,就直接写入其地址。这么做的好处是简单直接,速度快,与运行时地址空间一一对应,有时候甚至非常有利于调试。但缺点和限制是每个对象需要额外的4个字节 (64位就是8个字节),而且必须保证在序列化的过程中不发生相关内存的释放和重新分配 (因为可能导致同一地址被不同的对象“复用”了)。</li> <li>每个对象在被写入文件时,使用当时的文件偏移作为该对象的 ID (通过每个偏移在文件中的唯一性来保证全局唯一),将引用者写入文件时,如果出现被引用者的指针,就写入其文件偏移。这么做省去了指针的存储开销,但由于文件写入是有先后次序的,先写入的对象如果引用了后写入的对象,此时还不知道文件偏移,就只有在第一遍写完所有对象之后,再写第二遍填上引用的空缺(或者是预先在内存中把偏移算好)。</li> </ol> <p>为什么说这种方案很原始呢,因为__一个地址所能携带的信息太少了__。在载入时,我们必须在整个过程中都非常清楚自己在操作什么类型的数据,这样就需要大量额外的代码来在不同的情况下创建不同类型的对象,这是非常繁琐和易错的。究其原因,就是引用的信息量不够,做不到某种程度的自描述。</p> <hr> <h4 id="关于打包的单独讨论">关于打包的单独讨论</h4> <p>由于这种方案足够的快,在一些游戏引擎的二进制数据文件中有非常普遍的应用。为了保证读取效率,游戏引擎通常会把逻辑上相关的资源打包在一起,避免反复读取零散的文件。由于__在包内的文件仍保持着与文件系统相一致的树状存储结构__,所以“物理包文件 + 虚拟的内部文件结构”,本质上跟典型的OS树状文件系统并无不同。提供这种打包机制的引擎通常会把这一层给抽象掉,大多数情况下,游戏代码仍像访问普通文件一样去访问内部的一个资源。这也就是在说,理想情况下,一个考虑周详的打包机制,应做到保留 OS 文件系统的基本语意,将其自身透明化,不破坏和干扰已有的文件访问方式。</p> <p>出于简化讨论的目的 (不影响讨论的内容和结果),我们将只讨论基于传统的 OS 文件系统下的资源相互引用问题,而把“是否应该打包,如何打包”等问题正交地拆分出去,视作另一个维度的考虑。</p> <hr> <h3 id="方式-ii---基于路径的引用">方式 II - 基于路径的引用</h3> <h4 id="形如-foobarmiraclepng">(形如 &lsquo;/foo/bar/miracle.png&rsquo;)</h4> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">texture</span> <span class="o">=</span> <span class="s">&#34;/foo/bar/miracle.png&#34;</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>正如标题里的例子那样,按照路径来索引资源,应该是__最自然和直观的引用方式__了。事实上,互联网上的资源和服务,大部分都是通过 URL,以路径方式来提供的。</p> <p><img src="https://gulu-dev.com/ref_by_path.jpg" loading="lazy" alt="p" ></p> <p><em>(此图引自 <a class="link" href="http://slideplayer.com/slide/4451960/" target="_blank" rel="noopener" >(SlidePlayer) A+ Guide to Software</a>)</em></p> <p>使用路径来索引资源时,如有可能,应当尽量使用相同格式的归一化的平台无关的路径。混用 &lsquo;\\&rsquo; 和 &lsquo;/&rsquo;,使用 &ldquo;/../&rdquo; 或 &ldquo;/./&quot;,等等,都会造成无法直接比较两个引用是否指向同一份资源,而且对同一资源的引用字符串 hash 的结果会不一致。</p> <p>当需要移动或重命名资源的时候,路径就失效了。这时候,简单的做法是,总是在编辑器提供的资源管理工具中进行 move/rename 的操作,这样可以自动更新所有对该资源的引用。涉及到全库范围的扫描和修改,当资源量大时可能会非常慢。</p> <p>一个常见的实践是使用所谓的 &ldquo;<strong>Redirector</strong>&quot;,当 move/rename 发生时,在原来资源的位置放置一个跳转,指向新的位置,这样所有的相关资源都可以保持对原资源的引用,无需被动更新。在全库范围内,可以定期地运行自动化工具来清理这些跳转,更新引用以直接指向真正的资源。除了把操作的影响局部化以外,这种做法还有一个好处是,如果团队内一个人在 move/rename 时,另一个人创建了对老资源的引用,这个机制可以确保两个人的工作被合并时能够正常工作,而上面的“扫描并更新”的实践则会导致后者的引用失效。</p> <p><img src="https://gulu-dev.com/redirect.png" loading="lazy" alt="p" ></p> <p><em>(此图引自 <a class="link" href="http://www.criticone.com.au/learning-hub/the-how-and-why-of-301-302-redirects" target="_blank" rel="noopener" >The how and why of 301-302 redirects. </a>)</em></p> <hr> <h3 id="方式-iii---基于-guid-的引用">方式 III - 基于 GUID 的引用</h3> <h4 id="形如-77ba2b2b-3ea5-4c49-a3d2-0da6a03d2b44">(形如 &lsquo;{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}&rsquo;)</h4> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">texture</span> <span class="o">=</span> <span class="s">&#34;{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}&#34;</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>使用 GUID 的优点非常明显——由于不依赖在磁盘上的具体位置,不管路径和命名怎么变,只要 GUID 保持不变,就能保证总是索引到对应的资源。</p> <p>但问题也非常明显:</p> <ol> <li>首先是__可读性问题__,给定任意一个 GUID 必须依赖工具查找才知道对应的资源是什么,对工作效率的影响是很大的。考虑到有时会无意中删除或者忘了提交某个资源,仅凭一个 GUID 没有任何可能的途径来知道缺失了什么,而如果是路径的话我们至少有机会知道是哪个文件的问题。(是的我们可以通过版本管理软件来 blame 可是如果该文件被多人修改过就很被动了)</li> <li>其次是__额外信息的存储和同步的问题__,由于很多文件格式本身是找不到位置存 GUID 的,这就需要单独建一个同名的 .metadata 文件并与原文件一同管理,这进一步增大了负担,降低了工作效率。更重的实践使用一个中央数据库来把所有资源的 GUID 收拢到一处统一管理,这就需要提供各种工具去处理更新,合并,与版本管理软件协作等问题。</li> </ol> <hr> <h4 id="确定性的-guid-生成">确定性的 GUID 生成</h4> <p>由于工作关系,我曾在一个商业引擎的资源管理相关代码上工作过一段时间。不幸的是,该引擎使用了 GUID 来管理资源的标识和引用。更为不幸的是,该引擎通过“在打包时动态地为资源生成 GUID ”来成功地把打包问题和资源管理问题深深地耦合在了一起。由于在开发过程中,代码和资源会持续地迭代变化,打包的环境总是处于或微小或剧烈的干扰之中,所有这些带来的直接后果就是,打出的资源包内大部分资源的 GUID 几乎总是随着版本在持续地变化,而前后两次打包出的资源也无法兼容和重用。可以想见,对于一个需要联网并时常热更新的游戏来说,这是一个多么不幸的设计。</p> <p>为了解决这个问题,经过我跟另一位同事的先后努力,这个引擎中,涉及资源管理方面的所有的 GUID 生成都被我们改为了确定性的 (deterministic guid generation)。也就是尽量保证,在任何一个给定的上下文中,生成的 GUID 总是确定一致,并与该上下文基本对应。这个确定性的 GUID 生成实践,本质上是一个通过使用互不干扰的多个随机序列 (<a class="link" href="http://en.cppreference.com/w/cpp/numeric/random/mersenne_twister_engine" target="_blank" rel="noopener" >std::mt19937</a> &amp; <a class="link" href="http://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution" target="_blank" rel="noopener" >std::uniform_int_distribution</a> ) ,抓取并嵌入上下文相关的信息,来把 GUID 的生成尽可能局部化的过程。关于此问题的更详细的记录信息可参阅[此文档 (PDF)](<a class="link" href="https://github.com/mc-gulu/old-bits/blob/master/%282013%29%20Deterministic%20Guid%20Generation.pdf%29" target="_blank" rel="noopener" >https://github.com/mc-gulu/old-bits/blob/master/(2013%29%20Deterministic%20Guid%20Generation.pdf)</a>,这里就不再细说了。</p> <p>经过这次折腾,俺对 GUID 用于折腾所能产生的巨大能量有了充分而深刻的认识。此事的一个后遗症是,从那以后听到用 GUID 管理引用和依赖的方案,俺就不由自主想呵呵了。</p> <hr> <h3 id="方式-iv---unique-name-全局唯一命名">方式 IV - Unique Name 全局唯一命名</h3> <h4 id="形如-v1_ui_mainframe_miracle_png_hd">(形如 &lsquo;v1_ui_mainframe_miracle_png_hd&rsquo;)</h4> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">texture</span> <span class="o">=</span> <span class="s">&#34;v1_ui_mainframe_miracle_png_hd&#34;</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>简单来说,Unique Name 本质上是一个改良版的 (具有一定可读性的) GUID。它兼具了路径引用和 GUID 引用的优点 (可读性好,可随意修改物理路径) 但除了改良的可读性这一点之外,上面所有的 GUID 相关讨论也同样适用于 Unique Name。</p> <p>当资源量大到一定的体量并仍在持续增长时,(为了避免冲突) Unique Name 将变得越来越臃肿。过长的描述不仅容易造成额外的管理和沟通负担,也会加大运行时的内存开销,实践中在需要时可以 hash 一下。</p> <hr> <h2 id="改进的实践---路径--摘要-path--digest">改进的实践 - 路径 + 摘要 (&ldquo;Path + Digest&rdquo;)</h2> <h3 id="形如-foobarmiraclepngdigest-string">(形如 &lsquo;/foo/bar/miracle.png:(digest-string)&rsquo;)</h3> <p>呼~~终于说到这一次的实践了。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">texture</span> <span class="o">=</span> <span class="s">&#34;/foo/bar/miracle.png:bd37de66ffdcfd5bf544502a1fae1e99&#34;</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>还好一句话就能说清楚:<strong>在路径后面加一个该资源的内容摘要</strong> (算法随意不影响,目前使用 MD5) 就是我目前采取的方案。</p> <hr> <h3 id="关键点">关键点</h3> <p>那么与上面的方案相比,这个方案有何不同呢?</p> <ol> <li> <p><strong>资源重命名或移动时,能够做到自动检测和修改</strong></p> <ul> <li>一般情况下,如果仅仅是重命名或移动,根据内容算出来的摘要是不变的,当通过路径找不到资源时,通过比较摘要,就可以提示用户 (或自动重定向到) 重命名或移动后的资源。</li> <li>检测和修改是可惰性的,可延迟至对应的资源打开时再转换,不必立即一次性扫描和更新所有引用</li> <li>重命名和更新可以在 OS 的文件系统内完成,无需在特定工具内</li> </ul> </li> <li> <p><strong>资源更新时自动识别和更新摘要</strong></p> <ul> <li>当资源发生变化时 (通常是美术/策划保存了一个新版本) 编辑器会在加载此资源的引用者时为其生成新的摘要。</li> <li>这个也是可惰性的,也就是加载了哪个资源,哪个资源才需要重新生成</li> </ul> </li> <li> <p><strong>不像 GUID 那样需要单独存储,无需额外的 metadata 文件管理负担</strong></p> <ul> <li>由于摘要没有产生资源以外的额外信息,随时可以根据资源本身生成,所以无需额外的 metadata 文件</li> </ul> </li> <li> <p><strong>简化全库范围的操作</strong></p> <ul> <li>方便检查重复资源 (全库比较摘要即可)</li> <li>全库范围自动修复所有的重命名和移动 (完全应用 1.)</li> <li>全库范围自动重算 (完全应用 2.)</li> </ul> </li> </ol> <hr> <h3 id="实现逻辑">实现逻辑</h3> <p>有同学可能会问:“<strong>如果移动,重命名,更新等各种操作混杂在一起,我怎么知道什么时候该自动重定向,什么时候该更新摘要呢?</strong>”</p> <p>嗯,这就是路径 (Path) 和摘要结合 (Digest) 的精髓所在了。我们根据引用去查找资源时,是按照下面伪码的逻辑进行的:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span><span class="lnt">32 </span><span class="lnt">33 </span><span class="lnt">34 </span><span class="lnt">35 </span><span class="lnt">36 </span><span class="lnt">37 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">Resource</span><span class="o">*</span> <span class="nf">getResource</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">refString</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 分解为路径和摘要两部分 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">path</span> <span class="o">=</span> <span class="n">GetPathPart</span><span class="p">(</span><span class="n">refString</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">digest</span> <span class="o">=</span> <span class="n">GetDigestPart</span><span class="p">(</span><span class="n">refString</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// 尝试访问位于此路径的文件 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">Resource</span><span class="o">*</span> <span class="n">res</span> <span class="o">=</span> <span class="n">GetActualFile</span><span class="p">(</span><span class="n">path</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">res</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 文件存在的情况,检查摘要是否一致 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="n">digest</span> <span class="o">==</span> <span class="n">GetActualDigest</span><span class="p">(</span><span class="n">path</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">GetActualFile</span><span class="p">(</span><span class="n">path</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 文件存在,摘要不一致,则认为是资源更新,重算摘要 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">RefreshDigest</span><span class="p">(</span><span class="n">path</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 文件如果不存在,符合重命名/移动的条件,提示用户资源未找到,是否进行全库范围搜索 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="err">在另一个地点找到了摘要符合的资源</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 提示用户 (或自动) 更新引用路径 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">RefreshPath</span><span class="p">(</span><span class="n">newPath</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 提示资源缺失 (in-editor) 或使用 err-placeholder (in-runtime) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>也就是说,<strong>路径的判定优先级高于摘要</strong>。在认定当下属于何种情况时,路径为主导,摘要为辅助。如果路径吻合但摘要不符,则认为属于资源更新的情况;如果路径失效,则使用摘要去全库匹配。两种行为分别针对两种不同情况的处理,泾渭分明,各司其职。</p> <hr> <h3 id="批量处置">批量处置</h3> <p>上面的代码是单个资源获取的流程,实际上在编辑器中打开一张地图 (或一个 UI 界面) 时,如果一个资源一个资源地单独汇报和处置,效率就太低了,可以在全部加载完毕后,统一批量地进行一次全库范围的匹配,然后弹出一个汇报和处置的对话框。在这个处置对话框中,重命名/移动/更新都是黄色叹号,而无法识别/找不到资源则是红色叹号,通常如果都是黄色叹号的话直接全部更新就可以了。</p> <hr> <h3 id="代码中的引用">代码中的引用</h3> <p>在代码中为了简便,可以__仅使用路径__即可。在运行游戏的过程中,会自动生成一个 <code>digest_cache.txt</code> 文件,每一行是一个资源的完整引用,可以把这个文件提交到版本管理的库中。这样,很容易通过程序手段在资源发生重命名,移动和更新等事件时,检测并更新这个文件,必要时,可提示用户代码内的路径需要更新。</p> <hr> <h2 id="小结">小结</h2> <p>总得来说,这个方案具有以下的特征:</p> <ul> <li>良好的可读性</li> <li>无需额外的 metadata 文件存储</li> <li>对资源的重命名/移动无需在编辑器等专有工具内完成,没有潜在的破坏其他资源引用的心理负担</li> <li>唯一需要保证的是,重命名和移动资源的时候,不要同时更新其内容即可。</li> </ul> <hr> <p>好了,关于这个资源引用管理的实践,到这里就讲完了。在资源管理方面,你有什么心得呢?欢迎跟我一起讨论:)</p> <p>[注]</p> <ul> <li>本文在讲述几种传统方式时,部分内容引自<a class="link" href="http://bitsquid.blogspot.jp/2014/06/what-is-in-name.html" target="_blank" rel="noopener" >这个页面 (需翻墙)</a>”。</li> <li>本文的进一步讨论见<a class="link" href="http://gulu-dev.com/post/2015-11-14-ref-management-2" target="_blank" rel="noopener" >这里</a></li> <li>本文同时发在我的知乎专栏 (<a class="link" href="http://zhuanlan.zhihu.com/gu-lu/20311224" target="_blank" rel="noopener" >链接</a>)</li> <li>本文已授权 GameRes 的<a class="link" href="http://www.gameres.com/467402.html" target="_blank" rel="noopener" >网站转载</a>和<a class="link" href="http://mp.weixin.qq.com/s?__biz=MjM5NTMxNTU0MQ==&amp;mid=400336563&amp;idx=1&amp;sn=7c72739b8048c61dff7132e2920c54d8&amp;scene=0#wechat_redirect" target="_blank" rel="noopener" >11月2日公众号推送</a>。</li> </ul> <hr> <p>[2015-11-02] 补:昨晚有两点遗漏,</p> <blockquote> <ul> <li>没有说明“使用新的实践后无需在编辑器内做重命名和移动等操作”</li> <li>没有说明“无需像 GUID 那样需要额外的单独文件存储和管理”</li> </ul> </blockquote> <p>已补入“<a class="link" href="http://gulu-dev.com/post/2015-11-01-ref-management#toc_13" target="_blank" rel="noopener" >改进的实践 - 关键点</a>”一节。</p> 2015.10 Oculus Connect 2 首席科学家 Michael Abrash 发言实录 https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/ Sun, 18 Oct 2015 14:42:00 +0000 https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/ <p>去年三月份,传奇图形程序员 Michael Abrash <a class="link" href="https://www.oculus.com/en-us/blog/introducing-michael-abrash-oculus-chief-scientist/" target="_blank" rel="noopener" >加入 Oculus</a>,以首席科学家 (Chief Scientist) 的身份再次跟 John Carmack 站在了一起,而此时距卡神 <a class="link" href="https://www.oculus.com/en-us/blog/john-carmack-joins-oculus-as-cto/" target="_blank" rel="noopener" >成为 Oculus 的 CTO</a> 已经快一年了。</p> <p>半个月前的 Oculus Connect 2 大会上,Michael Abrash 和 John Carmack 分别做了精彩的发言 (视频在<a class="link" href="https://www.youtube.com/watch?v=tYwKZDpsjgg" target="_blank" rel="noopener" >这里</a>和<a class="link" href="https://www.youtube.com/watch?v=Ti_3SqavXjk" target="_blank" rel="noopener" >这里</a>)。其中卡神的发言技术和工程细节较多,更适合已经在 VR 一线的开发人员。而 Abrash 则在半小时的 keynote 中,集中展示了 Oculus 的研究机构 Oculus Research 在 VR 的研究和探索中遇到的各方面的难题,关键的挑战和已经取得的一些进展。相较卡神一开口就根本停不下来的意识流,Abrash 的发言更加概括和完整,很适合从全景上了解 VR/AR 技术,对一般的开发人员也有一些启发性,所以俺择要记录了一下,以备日后参考。</p> <hr> <p>Abrash 上来时先煽情了一下,告诉大家他对 VR 近年的进展感到十分吃惊和兴奋,“Just a few years ago, all of this would have been totally inconceivable and that excitement is richly deserved&hellip;The truly amazing part is that we&rsquo;ve barely started down toward what VR capable of. Decades of innovations and new experiences lie before us&hellip;” 和那些一窝蜂跑到这个行业里来淘金的人不同,对 VR 这一可能深刻地影响和改变人们生活方式的技术,Abrash 言语间洋溢着技术人员所特有的巨大热情。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/01.jpg" width="700" height="388" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/01_hu161acba6ef11e11aa7d100bdbc1772b6_33091_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/01_hu161acba6ef11e11aa7d100bdbc1772b6_33091_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="180" data-flex-basis="432px" ></p> <hr> <p>紧接着,Abrash 很有感情地讲了一个 &ldquo;Good-old-days&rdquo; 的小故事。</p> <p>当他 92 年左右还在西雅图为微软做第一代 Windows NT (这货后来居上,淘汰了 Win95/98/ME 的实现,成为后来所有 Windows 的内核基础) 的开发时,有一次开会,他跟行业传奇 <a class="link" href="https://en.wikipedia.org/wiki/Dave_Cutler" target="_blank" rel="noopener" >Dave Cutler</a> 正好走在一起,两个人没说话默默地走了一段之后,Dave 突然转过来对 Abrash 说:“You know, these are the good old days” 把他吓了一跳,“I don&rsquo;t think I would have been more startled if Dave announced he was a martian.” (就算 Dave 说他是火星人我都没法更震惊了) 当时 Abrash 很不以为然,哥你别开玩笑了好不,天天加班干到吐血,品控严得让人发指,压力这么大你居然好意思说这是“Good old days”,该吃药了吧。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/02.jpg" width="700" height="297" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/02_hu4155ce48796c68e5e5efca9f1e49823c_33997_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/02_hu4155ce48796c68e5e5efca9f1e49823c_33997_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="235" data-flex-basis="565px" ></p> <p>后来呢,Abrash 动情地说到,Dave 居然是对的,很多年后他回过头去,对 Windows NT 的那段工作经历,他印象最深的就是亲密无间的协作氛围 (teamwork),战友之间的满满基情 (camaraderie),和作为一个整体取得的巨大成就 (accomplishment)。每当想到他曾参与设计和实现了一个 OS 的核心部分,而这个 OS 在过去的15年里成为数以亿计的人几乎每天都需要使用的工具,他就觉得这段经历非常的可贵 (a rare opportunity to really make a difference),没有什么比这更符合 “Good old days” 了,而身处局中的人往往没有意识到自己正在参与什么和改变什么。Abrash 说到,他希望来参加 Connect 的各路英雄能意识到,“how unbelievably fortunate we all are, have the opportunity to be VR pioneers.”,</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/03.jpg" width="846" height="350" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/03_huecd148ccf7dcaad69a8cf5c45453c432_21977_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/03_huecd148ccf7dcaad69a8cf5c45453c432_21977_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="241" data-flex-basis="580px" ></p> <p>“We are creating a whole new way for people to interact with technology, one which has the potential to redefine almost everything about the way we work, play, and interact with each other. Opportunities like that come along once or twice in a lifetime at best&hellip;” 满满的使命感,和终于能在有生之年有机会去推动这场深刻变革的幸运感,让 Michael Abrash 看起来完全不像是一个写了一辈子代码,快到退休年纪的程序员,在他和卡马克的眼神中,你能随时看到一种压倒性的纯粹的热情,这是他们最大的共同之处。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/04.jpg" width="700" height="293" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/04_hud90fe88be9dfc075ae702217cedccd7f_57181_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/04_hud90fe88be9dfc075ae702217cedccd7f_57181_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="238" data-flex-basis="573px" ></p> <p>VR 技术允许每一个人在虚拟空间里去真切地感受和创造,这种造物主般的感觉不再被程序和美术独享,每个人都有机会去创造和利用属于自己的独一无二的虚拟世界 (正如 Star Trek 里著名的全息甲板 (Holodeck) 和 Matrix 里的母体)。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/05.jpg" width="851" height="357" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/05_hu333223cb3bb1dc3edb5deca00aaf9d90_48513_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/05_hu333223cb3bb1dc3edb5deca00aaf9d90_48513_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="238" data-flex-basis="572px" ></p> <hr> <p>在畅想了一番 VR 对日常生活的巨大变革之后,Abrash 切入正题,“the future of VR will be built on three pillars”,分别是__对(人类)生理感知系统的驱动__,和__对(真实或虚拟世界的)重建__和__(主体与客体之间的)交互__。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/06.jpg" width="847" height="359" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/06_hu2a2a3db6a0463d507cea5135dac11937_43476_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/06_hu2a2a3db6a0463d507cea5135dac11937_43476_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="235" data-flex-basis="566px" ></p> <hr> <p>先从感知系统说起,Abrash 展示了一个例子,向我们说明了人类的感知系统在百万年的进化后,是如何通过获取极为有限的外界信息,再加上了无数先验的经验假设,来在脑海中重建世界的状态的。在这个过程中,经验假设往往比我们意识到的要重要。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/07.jpg" width="537" height="237" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/07_huc0e3aabd9793e330513afd1b4be7b8b1_21240_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/07_huc0e3aabd9793e330513afd1b4be7b8b1_21240_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="226" data-flex-basis="543px" ></p> <p>注意看,这看上去是一个有两条平行屋脊的顶棚。</p> <p>但是,当后面镜子上的布被拿掉时,你可以看到镜中的映像:</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/08.jpg" width="628" height="356" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/08_huf5ab86e20e1a099599cc6f44cd458b0d_31374_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/08_huf5ab86e20e1a099599cc6f44cd458b0d_31374_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="176" data-flex-basis="423px" ></p> <p>居然是一个拱顶。(转动的视频请见 6'40&quot;)下面是不同角度时的状况:</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/09.jpg" width="636" height="356" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/09_hua9c35d2d253fbd852fadb85b3f9c844e_45170_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/09_hua9c35d2d253fbd852fadb85b3f9c844e_45170_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="178" data-flex-basis="428px" ></p> <p>这里的要点是,理解了视觉系统是如何令你的感知系统下意识地做出符合经验的判断之后,就能玩些花样,让你“看”到实际上并不存在的东西。这个例子生动地展示了“<strong>我们体验到的“真实”情境,实际上是我们的感知和大脑让我们“感觉”真实的东西</strong> (并不一定与客观世界相符)” (the reality we experience is whatever the perceptual system and brain say it is) (所谓“眼见”不一定“为实”)</p> <hr> <p>这给了 VR 一个独特的机会 (以恰当的方式) 去驱动着我们的感知系统来诱使我们“感觉上真实” (feel real)。对感知系统控制得越好,VR 的体验就会越好。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/10.jpg" width="824" height="354" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/10_hu40e4111615f4cf7ab13b77243551da39_51507_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/10_hu40e4111615f4cf7ab13b77243551da39_51507_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="232" data-flex-basis="558px" ></p> <p>正如上图,感知系统主要是经常说到的五感:视觉,听觉,触觉,嗅觉和味觉,再加上 (用于感知速度,加速度,空间位置和控制平衡) 的前庭系统 (vestibular)。紧接着 Abrash 分别谈了一下在这五个方面的研究分别处于什么阶段。</p> <p>先说说味觉 (Taste),Abrash 打趣地说,即使有能产生适当味觉的系统,也很难想象14个人用起来是啥感受 (一个手柄可以大家轮流玩,一个冰激凌轮流吃就有点……)。而且咀嚼和吞咽也是味觉 (taste) 的一部分,这方面的研究目前短期内还看不到什么起色。考虑到味觉对整体的虚拟体验影响并不大,就留给以后的研究人员吧。</p> <p>接下来是嗅觉 (Smell)。嗅觉往往与记忆和情绪有着强烈的关联 (has powerful memory and emotional associations) 但非常复杂 (surprisingly complicated)。可能你会觉得在鼻子附近按某种预设的顺序和剂量释放一些气体分子就能很好地模拟了,但实际上气味的传播方式差异很大 (并非均匀散射传播),而我们的鼻子是很敏感的;而且跟三原色不同,并没有那种嗅觉元素 (primary smells) 可以混合成各种我们能感受到的气味 (所以恰当的模拟可能需要几千种不同的分子);由于液体分子的持续性和粘性,用于结束当前气味的中央清除器 (central erasers) 本身可能会产生各种气味。所以总得来说嗅觉的模拟潜力很大,但需要进一步突破性的进展。</p> <hr> <p>然后是前庭系统 (Vestibular System)。前庭系统相当于我们人体内置的加速传感器 (accelerometer) 和陀螺仪 (gyroscope) 通过感知人体速度和朝向的变化,来协助大脑持续地判断空间位置和维持平衡感。对于 VR 来说前庭具有特殊的重要性,因为前庭感知视觉感知的冲突是 VR 造成不适的关键性因素 (比如你看到了自己正从空中猛冲向地面,但前庭缺乏对应的感知,就会产生不适)。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/11.jpg" width="700" height="326" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/11_hub192eeb61957c663d07b4df7e28bb0bb_53550_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/11_hub192eeb61957c663d07b4df7e28bb0bb_53550_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="214" data-flex-basis="515px" ></p> <p>很多人玩第一人称视角射击 (FPS) 会晕也是同样的原因:视觉上的旋转和加减速缺乏来自前庭的协同反馈。说到这儿,Abrash 无奈地把 Carmack 的照片放出来,“&hellip;but annoyingly, some people are completely unaffected” (全场哄堂大笑)。</p> <p>可以使用表面电极来刺激前庭,但由于颅骨的隔离,实际感受糙了点。要想做到精细的控制,只有把电极透过颅骨植入内部,Abrash 说,要真得这样就算是最硬核的玩家也不见得双手支持吧。哎,又是脑后插管的节奏啊。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/12.jpg" width="815" height="348" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/12_hufdf54c385e4dd4c3bc24ee8c4df2bad4_40056_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/12_hufdf54c385e4dd4c3bc24ee8c4df2bad4_40056_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="234" data-flex-basis="562px" ></p> <hr> <p>然后是听觉 (Hearing)。Abrash 和 Carmack 都觉得,听觉对好的 VR 体验非常重要 (Carmack 在随后的 keynote 里也强调了这一点),而且现有技术已经比较成熟。但这并不意味着实现好的视听效果很简单。听觉的模拟有三个基本的元素:Synthesis, Propagation, Spatialization。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/13.jpg" width="839" height="353" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/13_hue8ad3b744e47126db929ee87d6760614_44935_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/13_hue8ad3b744e47126db929ee87d6760614_44935_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="237" data-flex-basis="570px" ></p> <p>Synthesis (合成) 是源音效的产生过程 (the creation of source sounds)。目前的做法是用预先录制的波形来混合重放,但最终应该是由对物理过程的正确模拟来产生声音的 (比如表面振动)。可以想见的是,这个运算量将会是怪兽级的 (unbelievably computationally intensive)。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/14.jpg" width="814" height="356" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/14_hu7acf73b7bc1e0f473bb7f74c5a72ca72_50829_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/14_hu7acf73b7bc1e0f473bb7f74c5a72ca72_50829_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="228" data-flex-basis="548px" ></p> <p>Propagation (传播) 是音效在空间中的传播过程 (how sound moves around the space)。Abrash 原先认为传播的处理相对容易一些,运算量会小一些,但后来发现并非如此。两个原因,其一,跟光线不一样的是,不同频率的声波相互之间的折射,反射和干涉的程度非常不同;其二,(同样是) 跟光波不同,声音的传播慢到人们能显著体验到这种延迟 (闪电和雷声)。“&hellip;this means sound has to be simulated as a 3D time series across many frequency bands, which is much more expensive than generating a single instantaneous global solution per frame.” 总得来说这是一个运算量的问题,而在考虑空间上的复杂度的情况下得出一个通用的方案,目前仍然是一个未解决的问题。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/15.jpg" width="843" height="383" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/15_hu4e78234a41f7b10067820dd3152308b1_50996_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/15_hu4e78234a41f7b10067820dd3152308b1_50996_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="220" data-flex-basis="528px" ></p> <p>Spatialization (空间化) 是 (相对于接收者而言的) 声音在空间中的方向 (the direction of incoming sound)。理想情况下这应该是 Sound Propagation 的一部分,但现在可以用 HRTF (<a class="link" href="https://en.wikipedia.org/wiki/Head-related_transfer_function" target="_blank" rel="noopener" >head-related transfer function (HRTF)</a>) 来比较好地模拟声音如何到达空间内一个特定的接收者,并转化为声波穿过耳道到达鼓膜。但 HRTF 所需的硬件还很庞大,目前还无法装备到消费级的设备上。</p> <p>总得来说,我们对声音的原理和公式已经非常熟悉了,但谈到真实且实时的模拟,即使是一个小房间内的几个移动的发声体在目前都还很遥远 (运算量上几个数量级的差距)。成熟的真实声音模拟,还需要若干年的技术积累。</p> <p>(听到这里,我明白过来,目前音效的技术能力如果与图形类比的话,差不多相当于 2D 图像处理,而真实感的声音模拟对应的就是 3D 图形学)</p> <hr> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/16.jpg" width="845" height="354" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/16_hu0dc17562a945e09f310baf6f44f633f5_52727_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/16_hu0dc17562a945e09f310baf6f44f633f5_52727_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="238" data-flex-basis="572px" ></p> <p>接下来是对 VR 最为重要的视觉 (Vision),这是我们最熟悉也是研究最充分的一种物理现象。在 VR 的体验中,主要关心的是下面这五种属性的结合:</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/17.jpg" width="700" height="291" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/17_hubbc6be9f30cce43dfd28e8b011fd0fdc_48428_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/17_hubbc6be9f30cce43dfd28e8b011fd0fdc_48428_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="240" data-flex-basis="577px" ></p> <p>足够宽的视野,足够好的图像质量,任意变焦,HDR,更好的人体工程学。这些因素都需要改进,但不少情况下它们是彼此冲突的,现在实践上是各种 tradoff 来平衡之。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/18.jpg" width="846" height="354" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/18_hudfee7a4352b382c78bc4ebfb6b68354d_55525_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/18_hudfee7a4352b382c78bc4ebfb6b68354d_55525_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="238" data-flex-basis="573px" ></p> <p>这张图里列出了一些期望作为对比。细节可以看图就不多说了。</p> <hr> <p>最后是触觉 (Haptics),目前还没有任何科技,能有效地模拟真实世界的触摸感受。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/19.jpg" width="700" height="350" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/19_huad684293f222938229bd5b0f8df402a6_50390_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/19_huad684293f222938229bd5b0f8df402a6_50390_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="200" data-flex-basis="480px" ></p> <p>在感知系统这一节的结尾,Abrash 做了一个两个小球相交的小实验 (需要看视频 20'20&quot;),来说明我们在认知心理学上的巨大挑战。</p> <hr> <p>说完了感知系统,接下来是第二部分 Sense &amp; Reconstructing Reality</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/20.jpg" width="842" height="415" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/20_hu6b3498052a60ace2f21806361e9b2ad9_38626_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/20_hu6b3498052a60ace2f21806361e9b2ad9_38626_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="202" data-flex-basis="486px" ></p> <p>Abrash 放了一段视频,这是他们 Surreal Vision Team 的新同事 Richard NewComb 的成果。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/21.jpg" width="846" height="353" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/21_hu0f3b43b37bd6938c3eb2bbc21b6367b5_49831_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/21_hu0f3b43b37bd6938c3eb2bbc21b6367b5_49831_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="239" data-flex-basis="575px" ></p> <p>这对 VR 来说还不够好,“making all this work is going to require rethinking the entire sensing in reconstruction stack, both hardware and software from the ground up.”</p> <p>这是另一段同样来自 Surreal Vision Team 的视频,整个真实世界重建的过程是自动化和无缝实时的,注意其中材质,光照和阴影的模拟。(推荐观看)</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/22.jpg" width="843" height="356" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/22_hue94ec64f7233471110824c90635690eb_52679_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/22_hue94ec64f7233471110824c90635690eb_52679_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="236" data-flex-basis="568px" ></p> <hr> <p>最后是第三部分 Interaction</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/23.jpg" width="841" height="354" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/23_hu7b1a4b5d10370d2230d85fab942e9586_30425_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/23_hu7b1a4b5d10370d2230d85fab942e9586_30425_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="237" data-flex-basis="570px" ></p> <p>这里的研究要点在于“for the hands being able to act as a dextrous virtual manipulators.” 不知为啥,看到这儿我想起了 Quake III 里的电锯。这里的难点在于 &ldquo;fully reproduce real-world kinematics&rdquo;。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/24.jpg" width="847" height="357" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/24_hu763fc5a3a838937b2382d42d6a52552b_41944_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/24_hu763fc5a3a838937b2382d42d6a52552b_41944_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="237" data-flex-basis="569px" ></p> <p>比如上面这张桌子,目前没有有效的办法来让你 (在虚拟世界中) 的手被一张 (虚拟的) 桌子挡住。这需要新的触感科技,以及配套的交互语言 (就好像刚刚发明鼠标时那样)。</p> <hr> <p>把上面三个领域的这些挑战列一下,就得到了这张表 (可以看做是现在 Oculus Research 的研究课题列表)。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/25.jpg" width="1261" height="525" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/25_hu7a6218046e4117ee28f268f29a60b8cf_81331_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/25_hu7a6218046e4117ee28f268f29a60b8cf_81331_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="240" data-flex-basis="576px" ></p> <p>Abrash 放了一段视频,头戴设备内的面部表情感知 (与南加州大学的合作),这是其中一个交叉学科研究的例子。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/26.jpg" width="700" height="354" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/26_hu256a58fc2f4c5a502b73f87ad293b117_31686_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/26_hu256a58fc2f4c5a502b73f87ad293b117_31686_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="197" data-flex-basis="474px" ></p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/27.jpg" width="700" height="395" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/27_huc552beaec973a32e7351ac6b563800b0_43826_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/27_huc552beaec973a32e7351ac6b563800b0_43826_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="177" data-flex-basis="425px" ></p> <p>面对这些世界级的课题,尤其是一些交叉合作的课题,需要非常大的直面困难的勇气。说到这里,Abrash 提起了曾在 id software 与卡神共事的经历,又开始讲小故事了。</p> <p>那时卡神才刚刚能让 Quake 实时地跑起来,在那个时间点上,在地图内来回跑动已经比较流畅,但偶尔会非常卡,问题出在过度绘制 (Overdrawn) 上。那时的 Quake 画下了视锥内所有的多边形,当往一个复杂度很高的角度看过去时,很多被挡住的物体被一层层地刷在 framebuffer 上,而那时候还没有有效的可见性检测算法。卡神那时一直在想各种办法,来剔除那些不可见的物体。</p> <p>他试了各种不同的算法 (见下图左半边部分),看起来都很挺有潜力,但都各有缺点。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/28.jpg" width="700" height="293" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/28_hu5a85b436c5d4ff878a12ed6faf51b6ba_65203_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/28_hu5a85b436c5d4ff878a12ed6faf51b6ba_65203_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="238" data-flex-basis="573px" ></p> <p>后来他尝试了直接从场景 BSP 中提取可见信息 (右半部分),忙活了一个周末,在周一的凌晨 3:30 John 理出了 PVS 的头绪,彻底地解决了这个问题。</p> <p>这里 Abrash 复述了他在著名的黑皮书 (Michael Abrash&rsquo;s Graphics Programming Black Book) 里<a class="link" href="https://github.com/jagregory/abrash-black-book/blob/master/src/chapter-64.md" target="_blank" rel="noopener" >提到过的一段话</a>,值得记录一下:</p> <blockquote> <p>John says <strong>precalculating the PVS was a logical evolution of the approaches he had been considering, that there was no moment when he said &ldquo;Eureka!&rdquo;</strong> Nonetheless, it was clearly a breakthrough to a brand-new, superior design, a design that, together with a still-in-development sorted-edge rasterizer that completely eliminates overdraw, comes remarkably close to meeting the &ldquo;perfect-world&rdquo; specifications we laid out at the start. &hellip; <strong>All really great designs seem simple and even obvious—once they&rsquo;ve been designed.</strong> But the process of getting there requires incredible persistence and a willingness to try lots of different ideas until the right one falls into place, as happened here. &hellip; My friend Terje Mathisen likes to say that <strong>&ldquo;almost all programming can be viewed as an exercise in caching,&rdquo;</strong> and that&rsquo;s exactly what John did. No matter how fast he made his VSD calculations, they could never be as fast as precalculating and looking up the visibility, and his most inspired move was to yank himself out of the &ldquo;faster code&rdquo; mindset and realize that it was in fact possible to precalculate (in effect, cache) and look up the PVS. &hellip; <strong>The hardest thing in the world is to step outside a familiar, pretty good solution to a difficult problem and look for a different, better solution.</strong> The best ways I know to do that are to keep trying new, wacky things, and always, always, always try to simplify. One of John&rsquo;s goals is to have fewer lines of code in each 3-D game than in the previous game, on the assumption that as he learns more, he should be able to do things better with less code.</p> </blockquote> <p>最后,Abrash 提到这本具有传奇意义的,影响和激励了无数人 (包括他和 Carmack) 的书。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/29.jpg" width="700" height="298" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/29_hu3233b3956d037edac60fed89ca9bacea_58174_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/29_hu3233b3956d037edac60fed89ca9bacea_58174_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="234" data-flex-basis="563px" ></p> <p>是的,“These are the good old days”。</p> <p><img src="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/30.jpg" width="700" height="333" srcset="https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/30_hu456e8d98c2f9e07967e46221f465bb0a_40697_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-17-oculus-connect-2-michael-abrash/images/30_hu456e8d98c2f9e07967e46221f465bb0a_40697_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="210" data-flex-basis="504px" ></p> <hr> <p>下面的评论里有两条很有意思 (后一条是前一条的回复),摘录一下:</p> <p><em>HowAbout NoSon</em></p> <blockquote> <p>Really, what a load of shit. All this amounts to is &ldquo;please support our platform that has no software&rdquo; and &ldquo;we have no idea how to solve any of the non obvious problems yet.&rdquo;</p> </blockquote> <blockquote> <p>Not sold on Oculus at all.</p> </blockquote> <p><em>Tim Robb</em></p> <blockquote> <p>&ldquo;Please support our platform that continues to pioneer a space that is fiendishly difficult to break into, that somehow, despite all these challenges, we&rsquo;ve produced a result which people are still finding amazing. Please support our platform that is only the beginning. That is still under incredibly heavy research and development to make a great experience&rdquo;. An experience that isn&rsquo;t even released yet. Of course they don&rsquo;t have all their software built yet, lol. How many games are completely supported on a new generation console months before they release? None. They all adapt after launch. Oculus Connect are massive events. Developers across the world are trying to make experiences. Will we see much on launch date? Maybe a couple. But you can bet your ass and your money that there will be a lot more where they come from.</p> </blockquote> <blockquote> <p>Or don&rsquo;t. That&rsquo;s fine too. This is a presentation for early adopters. Most &gt;50% of consumer don&rsquo;t buy a product until well, well after this phase, probably not until late next year. That&rsquo;s fine! Do that then. It&rsquo;ll be a safe bet by then, rather than the risk now. Us early adopters and cutting edge developers will be the people ensuring you have a good experience down the line. With our feedback, and our creations, the platform will have a little of us in it. That&rsquo;s why we do what we do, and why we watch things like this.</p> </blockquote> <p>是的,很多很多困难。其中不少困难,不要说工程实践,甚至从理论上都还没有答案。越是深刻的影响和改变,越是需要时间去成熟,消化和沉淀。正是因为我能感受到 VR 将多么深刻地改变我们的生活,才明白这绝不是短期就会成熟落地的科技(正如互联网从兴起到真正地改变生活)。而这正是我对先行者的钦佩之处,也是我记录此文的初衷。</p> <p>[注]</p> <ul> <li>[2016-05-06] 本文已授权极客视界 <a class="link" href="http://geekview.cn/g06/15916.html" target="_blank" rel="noopener" >全文转载</a></li> <li>[2016-07-07] 本文已授权 indienova <a class="link" href="http://indienova.com/indie-game-development/oculus-connect-2-michael-abrash-keynote/" target="_blank" rel="noopener" >全文转载</a></li> </ul> 2015.10 CppCon2015 Memory and C++ debugging at EA https://gulu-dev.com/post/2015-10-11-memory-debugging/ Sun, 11 Oct 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-10-11-memory-debugging/ <p>由于微信会对外部页面重新排版,若文中的链接无法访问,请选择右上角菜单“在浏览器中打开”即可。 (本文的<a class="link" href="http://gulu-dev.com/post/2015-10-11-memory-debugging" target="_blank" rel="noopener" >永久链接</a>)</p> <hr> <p>Scott Wardle 在 CppCon 2015 上的分享题为《Memory and C++ debugging at EA》,是关于内存和调试方面的一些心得。这里是<a class="link" href="https://www.youtube.com/watch?v=8KIvWJUYbDA" target="_blank" rel="noopener" >视频链接</a>(需翻墙),和<a class="link" href="https://github.com/CppCon/CppCon2015/tree/master/Presentations/Memory%20and%20C%2B%2B%20debugging%20at%20EA" target="_blank" rel="noopener" >演讲稿链接</a>。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/scott.jpg" width="1278" height="650" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/scott_hu63e65cbc7fce903a4fa3e91c4d77ddbc_76404_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/scott_hu63e65cbc7fce903a4fa3e91c4d77ddbc_76404_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="196" data-flex-basis="471px" ></p> <p>Scott 同学有 20+ 年游戏开发的经验,这个分享包括了在不同时期 (2000 年的 PS2 时期,2005 年左右的 XBox360/PS3 时期,当前的 PS4/XBox One 时期) 的技术演进情况,在此分享中,我收获颇多,这里简单记录一下。</p> <p>以 [GL_Note] 开头的是我夹带的私货,见谅。</p> <hr> <h2 id="2000-年左右时的一些原始工具和策略">2000 年左右时的一些原始工具和策略</h2> <p>2000 年左右时,不少程序员都是从 C 转过来没多久。那时的常见做法是像下面这样重载 new:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/01.png" width="436" height="219" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/01_hu3b0dbe3153c0fd5eb33e0846bb8df962_42102_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/01_hu3b0dbe3153c0fd5eb33e0846bb8df962_42102_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="199" data-flex-basis="477px" ></p> <p>和这样:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/02.png" width="576" height="248" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/02_hub49e77ccc1c45997ca4440c543c0ef6c_119399_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/02_hub49e77ccc1c45997ca4440c543c0ef6c_119399_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="232" data-flex-basis="557px" ></p> <p>debug_name 用于标示用途,flag 标示分配方向等选项。相信大家都这么干过吧。</p> <p>内存中的布局是这样的:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/03.png" width="576" height="142" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/03_hu8c4c6b6734e840f551025d842920afab_39833_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/03_hu8c4c6b6734e840f551025d842920afab_39833_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="405" data-flex-basis="973px" ></p> <p>除了一块专用的“小块内存分配器”外,其他一整块地址空间,从两端开始往中间用。</p> <p>对待内存碎片化的处理主要是按照生命期,把临时的短暂的内存放前面,较长的放后面。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/04.png" width="579" height="155" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/04_hueece1ee762c94dbd88716fc7cf8460a2_67984_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/04_hueece1ee762c94dbd88716fc7cf8460a2_67984_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="373" data-flex-basis="896px" ></p> <p>上图中的典型例子就是在 Low 这边把贴图读入内存,再解压缩到 High 那头,保证短期的和长期的互不干扰,就不易形成空洞。</p> <h2 id="2005-年左右的进展情况">2005 年左右的进展情况</h2> <p>到了 2005 年也就是 360/PS3 的年代,开始支持多分配器:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/05.png" width="515" height="270" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/05_hu2fb54498f70e3358e9ef73e7f74812a4_77325_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/05_hu2fb54498f70e3358e9ef73e7f74812a4_77325_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="190" data-flex-basis="457px" ></p> <p>C++ 里面重载 delete 是不能有参数的,所以析构时还要手动传分配器,比较痛苦。</p> <p>[GL_Note] 这个问题实践中可通过直接在分配出的 block header 里存放 allocator 的指针来解决。但这样会为每块内存浪费 4 个字节,当然通常 allocator 数量很少,一个 byte 也许就够了。</p> <p>[GL_Note] 还有一个见过的做法是,规定凡是自定义的 allocator 都划分自己专属的一段内存,拿任意一个指针/地址过来,通过对某些区间位做一下位运算,就能判定是哪个定制的 allocator,如果都不是的话,就是默认的 allocator。这种做法用地址空间上的限制消除了额外记录的需求。</p> <hr> <p>更好的利用地址空间的策略 (以时间,尺寸和不同的团队边界等作为标准):</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/06.png" width="451" height="136" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/06_hu0c00f1f927da5949537528da351bbc64_48577_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/06_hu0c00f1f927da5949537528da351bbc64_48577_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="331" data-flex-basis="795px" ></p> <p>[GL_Note] 简单解释一下,</p> <blockquote> <ul> <li>第一行时间因素:时间条上从左向右的每一项的生命期都显著不同,把它们彼此标记和隔离,有助于从根本上避免产生碎片。</li> <li>第二行尺寸因素:按照尺寸尽量把不同量级的内存分开,可以让新的内存请求更有效地 fit 进已有的空洞,从而提高利用率,降低极限情况下的最大尺寸开销。</li> <li>第三行团队因素:按照团队切分,能有效地快速定位问题到不同的组 (就能快速找到负责的人) 这里的 SBA 是 Small Block Allocator。 这三种特征通常需要综合起来考虑。</li> </ul> </blockquote> <p>不管以何种方式分块,块与块的边界处的 corruption 都是比较难以处理的,如下图:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/07.png" width="450" height="92" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/07_huf6138a02b9e08f61e9de3c520564e9ad_30481_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/07_huf6138a02b9e08f61e9de3c520564e9ad_30481_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="489" data-flex-basis="1173px" ></p> <p>他们意识到,如果像之前那样把一些调试信息放在新分配的内存的尾部,当发生 corruption 时十有八九就会被写坏,妨碍查错。于是就单独开了个调试堆,把地址尺寸分类标示等调试信息 hash 后存在这里。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/08.png" width="459" height="203" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/08_hua6f6799f931e0afe2d47ad19a5ef232d_99553_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/08_hua6f6799f931e0afe2d47ad19a5ef232d_99553_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="226" data-flex-basis="542px" ></p> <p>这样当 corruption 发生时,可以精确地找到当时的时间和空间的上下文,看看发生了什么。</p> <hr> <p>所有这些信息同时被记录在硬盘上,如下图。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/09.png" width="453" height="200" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/09_hudc74c6ac80c78e65a871ec9a43b36810_74105_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/09_hudc74c6ac80c78e65a871ec9a43b36810_74105_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="226" data-flex-basis="543px" ></p> <p>可以选择和查看任意一个时间点上的分配情况,也可以选择一段时间区间,查看在那一段时间里变化的部分。每一个分配都可以查看对应的堆栈信息。</p> <hr> <p>这是 BlockView,可以从空间上直观地看到不同类型内存的分配情况,以及空间上不同区域的利用率和碎片化的信息。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/10.png" width="430" height="227" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/10_hu43d99923ee2615ace58bfd14794db04e_91313_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/10_hu43d99923ee2615ace58bfd14794db04e_91313_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="189" data-flex-basis="454px" ></p> <p>当选中一个 block 时,可以看到那个 block 相关的详细信息 (左下角)</p> <hr> <p>另一个强力工具是 Stomp Allocator:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/11.png" width="402" height="209" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/11_huea0b176b8d2660459ea3bf57866703dc_48751_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/11_huea0b176b8d2660459ea3bf57866703dc_48751_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="192" data-flex-basis="461px" ></p> <p>这个专门用来查 corruption 的。当内存请求发生时,它利用虚拟内存分配 4k 的可读写内存并返回尾部的可用空间,并在后面追加一个 4k 的只读内存,这样一旦发生越界写立刻就会 crash。这个工具因为内存开销大,所以总是在__已经定位到较小范围内的怀疑对象__时使用。</p> <hr> <p>关于智能指针的循环依赖问题,</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/12.png" width="447" height="131" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/12_hu9d0f3df2b96503b9e3f46738bc2cf7a5_45824_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/12_hu9d0f3df2b96503b9e3f46738bc2cf7a5_45824_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="341" data-flex-basis="818px" ></p> <p>Scott 说如果加上循环依赖的检测就开始变得像垃圾回收了。所以明确使用规则,避免滥用即可。</p> <p>[GL_Note] 简单解释一下,</p> <blockquote> <p>智能指针的使用规则很简单,一句话就可以概括:当生命期明确的时候,使用 unique_ptr;只有当需要共享对象/数据的所有权导致生命期不确定的时候,才使用 shared_ptr。 这条规则隐含着一个认识:在绝大多数情况下,<strong>相互依赖的双方,必有一方生命期是相对确定的</strong>,否则常常说明有隐含的设计问题。</p> </blockquote> <hr> <p>接着 Scott 说到了 <a class="link" href="https://github.com/paulhodge/EASTL" target="_blank" rel="noopener" >EASTL</a>,</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/13.png" width="379" height="201" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/13_hu6709ebd618aff30b36936042f2a37476_52354_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/13_hu6709ebd618aff30b36936042f2a37476_52354_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="188" data-flex-basis="452px" ></p> <p>在 188 个单独的测试中,大部分比最新的 VS2015 自带的快,debug 版更是快上两个数量级。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/14.png" width="377" height="214" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/14_huba60777545cf05338345791db7b78413_50811_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/14_huba60777545cf05338345791db7b78413_50811_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="176" data-flex-basis="422px" ></p> <p>[GL_Note] 除了运行速度,一直以来我惊讶的是 EASTL 的良好的可读性,不得不说这是诸多 STL 版本里,最接近写给人看的版本。试举一例,摘自<a class="link" href="https://github.com/paulhodge/EASTL/blob/community/include/EASTL/heap.h" target="_blank" rel="noopener" >这里</a></p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/15.png" width="740" height="388" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/15_huf4d7c34bcfb739337c316034e8e842bf_44112_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/15_huf4d7c34bcfb739337c316034e8e842bf_44112_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="190" data-flex-basis="457px" ></p> <p>接下来 Scott 讲了一些 EASTLICA (EASTL ICoreAllocator) 的实现细节和一些传参和 type erasure 的问题处理。这些问题都属于 stl 定制 allocator 相关的问题,在网络上讨论也很普遍,实际上因为 EASTL 是一个专属版本,在这个专属环境下问题更容易协调和解决,这里就不多说了,感兴趣的可以直接看视频。</p> <h2 id="目前-2015-的系统">目前 (2015) 的系统</h2> <p>他们对逐渐开发出来的各种调试工具进行了强力的整合,下面逐一介绍。</p> <hr> <h3 id="内存调试工具改进">内存调试工具改进</h3> <p>首先是内存分配的接口逐渐不再使用一个单一的 debug_name (因为这种单个的字符串标签提供的信息量太小了),而是使用了 scope 这个上下文相关的概念,来把更多的信息关联到这次内存分配,比如跟对应的资源名及子系统名挂钩。</p> <p>其次,现在任意一个 allocator 都可以方便地找到自己所在的上一级内存区域 (parent arena),可以根据这个调整自己的行为。</p> <p>比如下面这个类 (其中的 eastl 使用了上面提到的 EASTLICA)</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/16.png" width="352" height="176" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/16_hu0d3ba1de877f0f688437aec574686be2_25112_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/16_hu0d3ba1de877f0f688437aec574686be2_25112_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="200" data-flex-basis="480px" ></p> <p>由于可以利用这些额外的信息来定制分配策略,逻辑上相关联的对象在物理上也会分配在一起,最终在内存中的布局可能是下面这样:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/17.jpg" width="612" height="343" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/17_hu08db1e55cf00986b0bf89ecf9f5e169a_44750_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/17_hu08db1e55cf00986b0bf89ecf9f5e169a_44750_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="178" data-flex-basis="428px" ></p> <h3 id="调试工具-deltaviewer">调试工具 DeltaViewer</h3> <p>DeltaViewer 会记录游戏运行从头到尾的整个 session (one run of the game),上传到一个 http server,并存在数据库里。</p> <h4 id="日志-trace-log">日志 (Trace Log)</h4> <p>首先是日志 (Trace Log) 的记录和查看:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/18.jpg" width="523" height="253" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/18_hu8e90929f3025630ce7b526a2f6fb706d_36393_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/18_hu8e90929f3025630ce7b526a2f6fb706d_36393_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="206" data-flex-basis="496px" ></p> <h4 id="io-负载剖析器-turbo-tuner">IO 负载剖析器 (Turbo Tuner)</h4> <p>IO 负载剖析器 (Turbo Tuner) 是一个查看任意时刻 IO 负载的工具,用这个可以很直观地看出系统性能受到 IO 影响的情况。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/19.jpg" width="591" height="251" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/19_huf949ba7c40148c2fdf3fae52fcc1e031_43893_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/19_huf949ba7c40148c2fdf3fae52fcc1e031_43893_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="235" data-flex-basis="565px" ></p> <p>注意这里的 Bundles 是需要同步加载的完整资源,Chunks 是可异步加载的碎片资源。</p> <p>仔细地看可以看到,上面第一行的 http log 可以看出任意时刻的 Log 量的大小和频繁程度;bundle states / chunk states 这两栏可以看到 IO 在不同状态间切换的时间点。</p> <hr> <h4 id="关联使用">关联使用</h4> <p>Trace Log 跟 Turbo Tuner 这两个是关联的 (实际上后续介绍的这些工具相互之间都是相关联的),也就是说对于一些关键的时间点,如果在日志中选择了对应的一条记录,可以精确地看到那个时间点上发生了什么,如下图:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/20.jpg" width="493" height="282" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/20_hu80db2724bb30a55c79f8c4ce5c801e8d_43418_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/20_hu80db2724bb30a55c79f8c4ce5c801e8d_43418_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="174" data-flex-basis="419px" ></p> <p>可以看到不同的游戏阶段,以及系统资源随时间流逝的变化情况,从而得到宏观的运行状况。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/21.png" width="483" height="264" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/21_hu84b86c2a9e2444fab9f5d8678d30ff57_148665_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/21_hu84b86c2a9e2444fab9f5d8678d30ff57_148665_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="182" data-flex-basis="439px" ></p> <p>当鼠标悬停在任意一次 bundle request 上时,可以得到那一次请求的所有相关的细节,如下图:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/22.png" width="469" height="232" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/22_huda9c83bbaaa1c269350d9cd57137819b_136231_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/22_huda9c83bbaaa1c269350d9cd57137819b_136231_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="202" data-flex-basis="485px" ></p> <p>可以看到有请求 ID (Sequence Number) / 序列 ID (可用来查前后时序相关的问题),StartTime/EndTime/Duration (起始,终止和持续时间),Priority (优先级),Size / Patch Size 尺寸相关信息,所在的资源包名 (bundle name),等等。</p> <hr> <h4 id="performance-timer">Performance Timer</h4> <p>接下来是性能剖析器 Frame rate and Job thread profiler (Performance Timer)</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/23.jpg" width="800" height="424" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/23_hu80cc581a4fe45fa205268d59af5fe496_93956_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/23_hu80cc581a4fe45fa205268d59af5fe496_93956_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="188" data-flex-basis="452px" ></p> <p>最上面一栏是帧率,每个蓝色条纹就是一帧。用鼠标选中就可以高亮那一帧及相邻的几帧。下面则依次是几个 CPU 上的负载情况,可以看到栈调用的层次关系和时间开销,很像 Telemetry 这一类工具,就不多说了。</p> <p>这个工具跟前面的工具结合起来使用,看起来是下面这样子的:</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/24.jpg" width="616" height="346" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/24_hu0b6d0eeb59baaa35f058ccfe7b5663c1_66416_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/24_hu0b6d0eeb59baaa35f058ccfe7b5663c1_66416_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="178" data-flex-basis="427px" ></p> <hr> <h4 id="memory-investigator">Memory Investigator</h4> <p>接下来是使用 Memory Investigator 查找内存泄漏。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/25.jpg" width="800" height="376" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/25_huf3e3957865477fb48326151b6b9e7c60_56101_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/25_huf3e3957865477fb48326151b6b9e7c60_56101_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="212" data-flex-basis="510px" ></p> <p>传统意义上的内存泄漏是一个宽泛的概念,new 了之后只要最终 delete 了就不算内存泄漏。而在游戏里这个概念要严格得多,在关卡与关卡之间严格来讲不允许有累积的未释放内存,当第二关的加载结束时,理论上第一关范围内分配的内存都应已被释放。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/26.jpg" width="615" height="345" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/26_hu0de5ce890deecf430ed6609cc132ec28_58512_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/26_hu0de5ce890deecf430ed6609cc132ec28_58512_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="178" data-flex-basis="427px" ></p> <p>用这个工具可以选择一个时间段 (A-B) 和一个时间点 C,然后列出在 (A-B) 这段时间内所有到了点 C 仍未被释放的内存分配,并查看它们的各种相关信息。</p> <hr> <p>也可以查看不同的时间点上,内存的分类对比情况</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/27.jpg" width="614" height="330" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/27_hubf51b193816e54a1d96edde9be06a119_56431_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/27_hubf51b193816e54a1d96edde9be06a119_56431_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="186" data-flex-basis="446px" ></p> <p>可以看到不同尺寸 (512B/64K/2M/Large) 的内存被分类统计,其中一百多次大分配占据了 1.7G 左右,而两百多万的小分配占据了 100M 左右,这有助于我们更细致地了解内存的使用状况。</p> <p>这是按照资源模块分类的情况</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/28.jpg" width="613" height="312" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/28_hu3b95a20936a9406de0da816ca71cc472_60477_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/28_hu3b95a20936a9406de0da816ca71cc472_60477_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="196" data-flex-basis="471px" ></p> <h2 id="小结和问答">小结和问答</h2> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/30.png" width="470" height="171" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/30_hudf467b520be79345b2518ed4cec0458e_33180_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/30_hudf467b520be79345b2518ed4cec0458e_33180_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="274" data-flex-basis="659px" > <img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/29.png" width="581" height="294" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/29_hu03e8edd6cc6363dbc949903fb330d6ec_109272_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/29_hu03e8edd6cc6363dbc949903fb330d6ec_109272_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="197" data-flex-basis="474px" ></p> <p>在后面的问答中,有人问这个工具会不会开源,Scott 说目前不会,但 EASTLICA 可能会随着 EASTL 一起开源,所以日后也不排除这个可能性。关于 EASTL,有同学问性能提升主要来自哪里,Scott 回答说主要是 1) 用指针做 iterator 和 2) 不依赖 inline 把很深的嵌套调用拍扁。有人问获取这么多数据会影响游戏的运行性能吗,Scott 说他一直都很惊讶于这个工具的运行性能,游戏实时运行没有问题,基本上只会损失 10%-20% (3-4ms)。</p> <p><img src="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/31.jpg" width="718" height="412" srcset="https://gulu-dev.com/post/2015-10-11-memory-debugging/images/31_hu436ded695d9ee2b793cc4fe2d33b33f8_21847_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-10-11-memory-debugging/images/31_hu436ded695d9ee2b793cc4fe2d33b33f8_21847_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p" class="gallery-image" data-flex-grow="174" data-flex-basis="418px" ></p> <p>这个分享的信息量挺大,很多思路都非常有价值。受益匪浅,简单记录,以备日后参考。最后再提一下,如果我的细节描述不够,请移步前往<a class="link" href="https://www.youtube.com/watch?v=8KIvWJUYbDA" target="_blank" rel="noopener" >视频</a>以获得完整的内容。</p> <hr> <p>[2015-10-13] 补:修正了几个错误。因为每篇文章的配图会自动使用第一张,所以开头添加了一张现场的图,应该比代码更合适:)</p> 2015.10 Evernote 你还好吗? https://gulu-dev.com/post/2015-10-04-evernote-are-you-ok/ Sun, 04 Oct 2015 13:16:00 +0000 https://gulu-dev.com/post/2015-10-04-evernote-are-you-ok/ <p>[按] 由于微信对外部 web 页面重新排版,所以从微信访问的同学无法直接访问文中的红字链接,请选择右上角菜单“在浏览器中打开”,即可正常访问。</p> <hr> <p>关心 Evernote 的同学 (尤其是深度依赖的 Premium 用户),可能对 Evernote 这段时间的动向格外关注。</p> <hr> <p>两个月前,Evernote 发生了一次 <a class="link" href="http://www.businessinsider.com/evernote-ceo-phil-libin-steps-down-2015-7" target="_blank" rel="noopener" >CEO 的人事变动</a>,Phil Libin 离开了 CEO 的位置,而由具有 Google 背景的 Chris O&rsquo;Neill 上任接替。几天前,Evernote 宣布了<a class="link" href="http://www.businessinsider.com/evernote-layoffs-2015-9" target="_blank" rel="noopener" >关闭办公室(台北,新加坡和莫斯科)和裁员(47人,13%)</a>的信息。虽然有了这些信息的铺垫,但是看到今天早上 Hacker News 的标题 <a class="link" href="https://news.ycombinator.com/item?id=10324960" target="_blank" rel="noopener" >&ldquo;Evernote is in deep trouble&rdquo;</a> (<a class="link" href="http://www.businessinsider.com/evernote-is-in-deep-trouble-2015-10" target="_blank" rel="noopener" >原文地址</a>),心里还是不由得一凛。</p> <p>的确,如果说三年前 Evernote 是在正确的时间 (个人信息过载却缺乏有效的组织工具) 推出了一个正确的愿景(<a class="link" href="http://jasoneverett.info/2014/02/06/evernote-my-second-brain/" target="_blank" rel="noopener" >成为大脑的延伸</a>),成了够潮够酷的硅谷明星,那么三年后,努力拓展外延的 Evernote 却逐渐变得臃肿而缓慢 (<a class="link" href="https://medium.com/@Jamesbedell/why-i-stopped-using-evernote-e5c0d83c606c" target="_blank" rel="noopener" >Why I Stopped Using Evernote (@medium.com)</a>),平台支持参差不齐 (<a class="link" href="https://discussion.evernote.com/topic/31999-evernote-not-truly-a-cross-platform-app/" target="_blank" rel="noopener" >Evernote - NOT truly a cross platform app.</a>),核心稳定性和可靠性也大不如前 (<a class="link" href="http://jasonkincaid.net/2014/01/evernote-the-bug-ridden-elephant/" target="_blank" rel="noopener" >Evernote, the bug-ridden elephant</a>),是到了需要改变的时候了。</p> <hr> <p>虽然一直是 Evernote 的长期付费用户,几年下来存有几千条各类信息记录,其中若干条是较高价值的高度有序汇总条目,但我对 Evernote (以及OneNote/有道云笔记等,后略)其实一直是比较保留和克制的使用。因为对我而言,Evernote 这一类笔记同步工具的最大问题在于,你对这些信息的完整性 (Integrity) 缺乏有效的手段去控制。在部分情况下,由于遗忘,你甚至不知道你安心交给它去保管的信息实际上已经丢失。打个比方,如果在两个设备之间对拷一个较大的文件时,担心拷贝过程是否完整有效,可以比较一下文件尺寸和 md5 digest;若是在不同设备上同步 Evernote,则缺乏手段去验证同步是否完整有效。任何一个测试不充分导致新引入的 bug 或兼容性问题,都有可能导致数据丢失或损坏,正如前面的<a class="link" href="http://jasonkincaid.net/2014/01/evernote-the-bug-ridden-elephant/" target="_blank" rel="noopener" >“Evernote, the bug-ridden elephant”</a>一文中描述的那样。</p> <p>必须承认,在不同设备不同平台的同步过程中,数据的损坏,丢失,<a class="link" href="http://techcrunch.com/2013/03/02/evernote-saw-first-signs-of-hacking-on-feb-28-emails-passwords-and-usernames-accessed-but-not-your-data-or-payment-details/" target="_blank" rel="noopener" >甚至是被窃取</a>是有很高几率发生的。这本应是 Evernote 的根本命脉和诉求,却因为公司的极高速发展和上市的迫切盈利需要,从来没有被真正坚决而彻底地重视。(反观 Dropbox,没有什么花哨的功能,但是 Integrity 足够好足够可靠,就够了)</p> <hr> <p>在 <a class="link" href="https://news.ycombinator.com/item?id=10324960" target="_blank" rel="noopener" >Hacker News 的热议</a> 中,除了大量的各种姿势吐槽 Evernote 以外,有几条评论挺有意思,摘录于下:</p> <p>谈收获的:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">From this article, I have some of my software business beliefs validated: </span></span><span class="line"><span class="cl">- don&#39;t be afraid to charge money </span></span><span class="line"><span class="cl">- release frequent, small updates. Users tolerate this much better than infrequent big updates with major changes </span></span><span class="line"><span class="cl">- focus on quality. Every update should include several bug fixes. As much as possible, fix bugs before adding new features </span></span><span class="line"><span class="cl">- focus. Put all your eggs in one basket, to some extent. </span></span><span class="line"><span class="cl">- watch costs like a hawk. Operating in 10 locations sounds like Evernote let their costs get out of control. </span></span></code></pre></td></tr></table> </div> </div><p>评论商业模式的:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Evernote is only a weird bloated SaaS company (with hundreds of employees) because they followed the wrong business model. </span></span><span class="line"><span class="cl">Evernote should be more like Minecraft (Mojang). A cool piece of software that a handful of people develop but a billion people use. They would be wildly profitable and worth many billions to Microsoft and others. </span></span></code></pre></td></tr></table> </div> </div><p>吐槽同步问题,顺便不小心跟我想到一起去了的:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Evernote still can&#39;t handle even simple non-overlapping changes on two devices. Every time I close my laptop I have to wonder if changes were synched. Especially embarrassing because superb open source code is already available for conflict resolution (git, for example). </span></span><span class="line"><span class="cl">EDIT: someone should write a notekeeping app built on github. </span></span></code></pre></td></tr></table> </div> </div><p>这一条来自<a class="link" href="https://news.ycombinator.com/item?id=10299642" target="_blank" rel="noopener" >这个讨论</a>:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">The problem with Evernote is they lack vision. Or maybe focus. Not sure which one. I understand selling socks makes money but ... </span></span><span class="line"><span class="cl">I was under impression that Evernote will evolve into something like RealTime+Word+Powerpoint - but no :( </span></span><span class="line"><span class="cl">I think Evernote is done. They lost their mojo and there is no way to get it back :( </span></span></code></pre></td></tr></table> </div> </div><hr> <p>另一个我觉得 (Evernote 等同类软件) 不太理想之处在于,它们的导出机制普遍不是很好。导出笔记时,要么导出为它们自己的私有格式,要么导出为格式全部丢失的纯文本,要么导出为格式完整但无法编辑的 PDF 档,而存在大量的“这些选项都难以满足需要”的情况,尤其是在需要批量地导出的时候。这不独是笔记类软件的通病,而是所有私有格式都面临的困难,只是对于笔记类软件,由于各类信息格式被汇总在一起,从而加剧了这个问题。</p> <p>反正是吐槽,也不嫌多一句了。为了更快的同步速度,我在去年切换到了国内版的「印象笔记」,结果惊奇地发现,竟然无法有效地把国际版账户里的所有笔记无痛迁移到国内的账户 (我知道有第三方小工具不过它们会丢失各种信息所以还是算了),所以直到现在我都还是国际/国内账号共用状态 Orz。</p> <hr> <p>所以后来我自制了一套笔记系统 ProNote,基于最基本的文件系统,以__可编辑的 Markdown 格式__ (用于日常记录) 和__保证完整性的 PDF 格式__ (用于存档备案) 为主,必要时使用 Git 管理部分文件的版本 (可回溯编辑历史)。现在我的效率组合是 <a class="link" href="https://todoist.com/" target="_blank" rel="noopener" >Todoist</a> + ProNote + Git。(偶尔会使用 Evernote/OneNote 等)</p> <p>正如报道中所言,如果未来的一到两年之内 Evernote 无法把精力聚焦到真正创造价值的地方,那我只好被迫更彻底地“去 Evernote 化”了——不过,说了那么多,还是衷心希望 Evernote 能好起来呢。</p> <hr> <p>最后两句话,顺便勉励一下自己吧。</p> <pre><code>The whole fun of living is trying to make something better. 生活的所有乐趣,在于使某样东西变得更好。 </code></pre> <blockquote> <ul> <li>(Charles Kettering)</li> </ul> </blockquote> <pre><code>If you pay too much attention to efficiency, you might actually become less effective. 过于重视效率,就会变得没有效率。(欲速则不达) </code></pre> <blockquote> <ul> <li>&ldquo;<a class="link" href="http://www.pickthebrain.com/blog/why-efficiency-is-overrated-%E2%80%93-and-what-to-do-about-it/" target="_blank" rel="noopener" >Why Efficiency is Overrated</a>&rdquo;, Pick the Brain</li> </ul> </blockquote> 2015.08 UMetaLod - 一个通用的增强版 LOD 方案 https://gulu-dev.com/post/2015-08-01-u3d-umetalod/ Sat, 01 Aug 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-08-01-u3d-umetalod/ <p>这篇 blog 是这个系列的第三篇,主题依然是 Unity 相关的实践。这一次我们来聊一聊,如何在游戏中实现一个通用的增强版 LOD (Level-Of-Detail) 方案。</p> <p>先解释一下为什么搞出一个很难念的名字 UMetaLod 吧——这实际上是前缀 u- 和 meta-lod 的组合。所谓 meta-lod 实际上是针对传统 LOD 而言的,用来表示一种更通用的广义的 LOD。</p> <hr> <h2 id="基本思路">基本思路</h2> <p>我们知道,不管是 Unity 还是 Unreal,都有着内建的基于与摄像机距离的 LOD 机制。如果正确地设置了 LOD 的每个层级对应的模型,当摄像机移动时,引擎会以一定频率计算 LOD,并把目标切换为对应层级精度的模型。</p> <p>那么为什么我们还要手动实现一个所谓的增强版本呢?</p> <p>这主要有以下几个方面的考虑:</p> <hr> <p>其一,手动定制的 LOD 系统,除了以该物体与摄像机的距离为基础,还会考虑</p> <ul> <li><strong>影响因子 1 - Bounding Box Factor</strong> - 该物体的包围盒尺寸</li> <li><strong>影响因子 2 - Geometry Complexity Factor</strong> - 该物体的顶点数量</li> <li><strong>影响因子 3 - ParticleSys Complexity Factor</strong> - 该物体是否为粒子系统,如果是的话考虑粒子数量等参数</li> <li><strong>影响因子 4 - Visual Impact Factor</strong> - 每个子物体的视觉影响,可由美术手动设置</li> </ul> <p>这些影响以不同的可定制权重 (weight) 对整个 LOD 系统发挥作用,这样全面而综合地考虑后,呈现出来的渲染结果对实际画面的影响更小,优化也就会更有效。</p> <p>除了这些内建的影响因子以外,用户还可以通过 AddUserFactor() 添加若干个定制的影响因子,参与到 LOD 系统的运算和评估中来。</p> <hr> <p>其二,对当前系统的性能进行评估,并把结果以参数形式传入系统,可以有效地形成负反馈,提高系统的伸缩性和健壮性。这里主要可以考虑两个因素:</p> <ul> <li>一个是当前系统性能等级的评估,目前用一个枚举 Highend / Medium / Lowend 分别代表高中低档的目标机器</li> <li>一个是当前 5 秒内的平均 FPS 状况,用于表示当前游戏的运行时性能状况 这两个值健康程度越高,整个 LOD 系统就会调整至允许容纳更多的视觉元素;如果情况越恶劣,系统则倾向于使用更严格的约束,从而降低视觉元素的总量。</li> </ul> <hr> <p>其三,传统的狭义 LOD 仅会在若干个不同精度的模型之间切换,而 UMetaLod 则是相对广义一些。UMetaLod 通过上面多因素的综合考虑和计算,得到一个针对当前物体的活跃度 (Liveness) 的概念,其值域为 [0, 1]。有了这个值,游戏内不同的系统,可以有针对性地对自己的对象做多种粒度,多个角度的不同处理,下面是一些常见的例子:</p> <ul> <li>对于常见的包含多个面片和粒子系统的技能特效,可以通过美术设置的权重 (即上面的 Visual Impact Factor),在活跃度发生变化时有选择地隐藏那些相对次要的部分,或者让其较早地淡出</li> <li>如果一个角色包含高中低的 shader 实现,可以在需要时,根据活跃度在不同复杂度的 shader 实现间切换</li> <li>可以开启/关闭对应的物理模拟,或更细粒度的调整 (调高/调低物理更新的频率)</li> <li>在需要时,根据活跃度使用更低面数的模型,更低骨骼数的骨骼动画,更低分辨率的贴图</li> <li>在需要时,根据活跃度简化或关闭动态的光照运算,调整和精简 shadow caster 的列表</li> </ul> <hr> <p>把__对多种影响因子的综合评估__,<strong>负反馈的性能调节</strong>,和__多层次细粒度的调整__这三者结合起来,就构成了一个广义的 LOD 系统。UMetaLod 能够从整体上根据系统的负载能力和运行情况,自主地去调节和优化系统的性能表现。当然,如果需要的话,也可以通过暴露出来的大量参数去调整它的行为,是激进还是保守,还是每个子系统使用不同的策略,还是针对特定的游戏类型做定制,都是可以考虑的。</p> <hr> <p>上个图吧,看上去跟传统的 LOD 区别不大。</p> <p><img src="https://gulu-dev.com/post/2015-08-01-u3d-umetalod/metalod_0.png" width="853" height="524" srcset="https://gulu-dev.com/post/2015-08-01-u3d-umetalod/metalod_0_hu568ba51e0d3633fec693cb1f7770bd3f_88224_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-08-01-u3d-umetalod/metalod_0_hu568ba51e0d3633fec693cb1f7770bd3f_88224_1024x0_resize_box_3.png 1024w" loading="lazy" alt="metalod_0" class="gallery-image" data-flex-grow="162" data-flex-basis="390px" ></p> <p>图中为了清晰起见,我隐藏了实际的物体,仅显示表示活跃度的调试线框,黑色表示活跃度为 0 而红色表示活跃度为 1,中间的过渡色则为环状的过渡区域。过渡区域的宽度直接关系到 popping 现象的多寡,也就是视觉跳跃感的强弱。</p> <hr> <h2 id="工程实现">工程实现</h2> <p>代码简单说一下吧,先说一下伪码的运算流程。为了简明起见,我们把影响因子称为 FOI (factor of impact)</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="err">计算目标物体的活跃度</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ==== 第一阶段 ==== </span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="err">获取目标物体与热点</span><span class="p">(</span><span class="err">摄像机或玩家的位置</span><span class="p">)</span><span class="err">的距离</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="err">分别计算四种内建</span> <span class="n">FOI</span> <span class="err">在不同权重下的影响度,并累加</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="err">分别计算所有用户添加的</span> <span class="n">FOI</span> <span class="err">在不同权重下的影响度,并累加</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="err">计算经过所有</span> <span class="n">FOI</span> <span class="err">修正过的距离</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ==== 第二阶段 ==== </span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="err">使用当前系统的性能评级和</span> <span class="n">FPS</span> <span class="err">来修正活跃度区域的上下限(也即热力环的热力衰减运算)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ==== 第三阶段 ==== </span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="err">使用上面两个阶段的计算结果得出该物体的活跃度</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个计算流程的实际代码在类 <code>UMetaLod</code> 的这个函数里:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">private</span> <span class="k">void</span> <span class="m">_</span><span class="n">updateLiveness</span><span class="p">(</span><span class="n">IMetaLodTarget</span> <span class="n">target</span><span class="p">)</span> </span></span></code></pre></td></tr></table> </div> </div><p>下面是系统中内建的四个影响因子,均定义有各自的取值范围和权重。正如上面提到的,用户还可以通过 <code>void AddUserFactor(UImpactFactor userFactor)</code> 来添加定制的影响因子。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">UMetaLodConst</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// the bounding volume of the target</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Factor_Bounds</span> <span class="p">=</span> <span class="s">&#34;Bounds&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// currently corresponds to vertex count of the target mesh, would be 0 for particle system</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Factor_GeomComplexity</span> <span class="p">=</span> <span class="s">&#34;GeomComplexity&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// currently correspends to particle count of the target particle system, would be 0 for ordinary mesh</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Factor_PSysComplexity</span> <span class="p">=</span> <span class="s">&#34;PSysComplexity&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// a subjective factor which reveals the visual importance of the target in some degrees</span> </span></span><span class="line"><span class="cl"> <span class="c1">// for instance, skill effects casted by player would generally has a </span> </span></span><span class="line"><span class="cl"> <span class="c1">// pretty much higher visual impact than a static stone on the ground</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Factor_VisualImpact</span> <span class="p">=</span> <span class="s">&#34;VisualImpact&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这些影响因子还可以设置不同的 Normalizer 去归一化传进来的值</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">delegate</span> <span class="kt">float</span> <span class="n">fnFactorNormalize</span><span class="p">(</span><span class="kt">float</span> <span class="k">value</span><span class="p">,</span> <span class="kt">float</span> <span class="n">upper</span><span class="p">,</span> <span class="kt">float</span> <span class="n">lower</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">public</span> <span class="k">struct</span> <span class="nc">UImpactFactor</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// customized Normalizer for different Impact Factor </span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="n">fnFactorNormalize</span> <span class="n">Normalizer</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// use methods like InverseLerp() to transform the parameter value into a valid FOI </span> </span></span><span class="line"><span class="cl"><span class="n">Normalizer</span> <span class="p">=</span> <span class="p">(</span><span class="k">value</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="n">lower</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="n">UMetaLodUtil</span><span class="p">.</span><span class="n">Percent</span><span class="p">(</span><span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="k">value</span><span class="p">);</span> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>正如之前的 <code>UQtConfig</code>,<code>UMetaLod</code> 也提供了一些可配置参数来调整行为</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">UMetaLodConfig</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// the time interval of an update (could be done discretedly)</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">UpdateInterval</span> <span class="p">=</span> <span class="m">0.5f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// the time interval of an FPS update (could be done discretedly)</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">FPSUpdateInterval</span> <span class="p">=</span> <span class="m">5.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// debug option (would output debugging strings to lod target if enabled)</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">bool</span> <span class="n">EnableDebuggingOutput</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// performance level (target platform horsepower indication)</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">UPerfLevel</span> <span class="n">PerformanceLevel</span> <span class="p">=</span> <span class="n">UPerfLevel</span><span class="p">.</span><span class="n">Medium</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// performance level magnifier</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="n">UPerfLevel</span><span class="p">,</span> <span class="kt">float</span><span class="p">&gt;</span> <span class="n">PerfLevelScaleLut</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="n">UPerfLevel</span><span class="p">,</span> <span class="kt">float</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="n">UPerfLevel</span><span class="p">.</span><span class="n">Highend</span><span class="p">,</span> <span class="m">0.2f</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="n">UPerfLevel</span><span class="p">.</span><span class="n">Medium</span><span class="p">,</span> <span class="m">0.0f</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="n">UPerfLevel</span><span class="p">.</span><span class="n">Lowend</span><span class="p">,</span> <span class="p">-</span><span class="m">0.2f</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// heat attenuation parameters overriding (including the formula)</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">DistInnerBound</span> <span class="p">=</span> <span class="m">80.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">DistOuterBound</span> <span class="p">=</span> <span class="m">180.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">FpsLowerBound</span> <span class="p">=</span> <span class="m">15.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">FpsStandard</span> <span class="p">=</span> <span class="m">30.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">FpsUpperBound</span> <span class="p">=</span> <span class="m">60.0f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">FpsMinifyFactor</span> <span class="p">=</span> <span class="p">-</span><span class="m">0.2f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="kt">float</span> <span class="n">FpsMagnifyFactor</span> <span class="p">=</span> <span class="m">0.2f</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">fnHeatAttenuate</span> <span class="n">HeatAttenuationFormula</span> <span class="p">=</span> <span class="n">UMetaLodDefaults</span><span class="p">.</span><span class="n">HeatAttenuation</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>可以看到末尾的 <code>HeatAttenuationFormula</code> 允许用户使用自定义的公式替换掉默认的热力衰减运算。</p> <p>其他的代码就不一一说明了,感兴趣可自行查看,文末附有 GitHub 链接。</p> <hr> <h2 id="优化和扩展">优化和扩展</h2> <p>这里先简单地提两点吧。</p> <ol> <li> <p>一个是可以与<a class="link" href="http://gulu-dev.com/post/2015-07-11-uquadtree" target="_blank" rel="noopener" >之前的 <code>UQuadtree</code></a> 结合使用,把每个叶节点上的数据集作为一个 <code>UMetaLod</code> 的 Lod Target,这样的好处是可以以区域为单位批量化运算,避免以单个对象为粒度所产生的大量近似的冗余运算。</p> </li> <li> <p>另一个是如果单帧的运算量过大,更新时可以划分为四个象限,逐象限计算和更新,也就是分拆到不同的帧去做增量更新。由于整个系统更新频率较低 (默认为 0.2s 更新一次),相邻的不同帧之前可以看做是等同的。即使万一由于玩家的移动漏更了一两个对象,也会在下一个 0.2s 周期就会处理,问题不大。</p> </li> </ol> <hr> <p>正如你可能已经发觉的那样,本文中一些细节并未充分地展开说明,如果你对背后的思路感兴趣,希望了解更多的实现细节,可以阅读<a class="link" href="https://github.com/SeaSunOpenSource/umetalod/blob/master/docs/%E6%80%A7%E8%83%BD%E7%AE%A1%E7%90%86%E5%99%A8%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97%E6%95%B4%E7%90%86.pdf" target="_blank" rel="noopener" >此文</a>,这是我此前实现的一个类似系统的一些开发日志的整理,也是此文中一些概念的来源。</p> <hr> <p>代码及对应的测试工程在<a class="link" href="https://github.com/SeaSunOpenSource/umetalod" target="_blank" rel="noopener" >这里</a>,在 Unity-5.0.1f1 下编译和运行通过。<br> 需要说明一下,这几期的代码都在西山居于 GitHub 上的 <a class="link" href="https://github.com/SeaSunOpenSource" target="_blank" rel="noopener" >SeaSunOpenSource</a> 组织的工程页面内维护。如未明确说明,均以 MIT License 发布。</p> 2015.07 《中国文化概论》所有相关资源 (附 GitHub 地址) https://gulu-dev.com/post/2015-07-25-chinese-culture-completed/ Sat, 25 Jul 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-07-25-chinese-culture-completed/ <p>[注] 微信内进入此页面的同学会发现,文内的 GitHub 链接无法打开。这是因为基于所谓安全原因,微信内置浏览器对原文重新做了排版的缘故,在右上角选择“在浏览器中打开”即可。</p> <hr> <p>前后三个多月的《中国文化概论》终于结课了,俺的结课分数也有惊无险地过了 80 分的线,获得了“优秀”的评价 O(∩_∩)O~~ 不过有个小遗憾,第五周的政治文化作业本来得了 8.2 的高分,结果忘了互评直接减半,变成了 4.1 分 (ಥ _ ಥ),影响了最后的结课成绩。</p> <hr> <p>所有的相关资源都已经整理在<a class="link" href="https://github.com/gl-notes/gln-public/tree/master/L-Learning/L-001-1507-mooc_chinese_culture" target="_blank" rel="noopener" >这里(GitHub 地址)</a>,里面包括所有的课件,笔记,讨论,作业,测验,期末考试。</p> <hr> <p>这是一个前所未有的时代。只需要一根网线,那些也许一辈子都没有机会走出大山的孩子,能够不花一分钱,有机会接触到这个国家最高水平的授课。某种意义上,这个世界变得更公平了——也许有的人拥有万贯家财,能把自己的孩子早早送入耶鲁剑桥,但若说起对知识的渴望,对梦想的执着,对改变自己命运的决心,可未必及得上那些每天要走十里山路去上学的孩子。每一个梦想都值得鼓励,每一双翅膀都渴望翱翔。虽然很难想象,在这样的时代,知识如此易得对无数的求知者意味着什么,但是——</p> <p>我很庆幸,自己可以成为他们中的一个。</p> <hr> <p>[2015-07-25] 补: 看来不少同学对这种授课模式很感兴趣,想了解一下的同学可以搜一下 “慕课” 或 “翻转课堂”,具体的网站就不贴了,免得被说成是广告帖 :)</p> <hr> <p><img src="https://gulu-dev.com/post/2015-07-25-chinese-culture-completed/2015-07-25-chinese-culture-cert-gu-lu.jpg" width="1680" height="2411" srcset="https://gulu-dev.com/post/2015-07-25-chinese-culture-completed/2015-07-25-chinese-culture-cert-gu-lu_huded8c4e173a6c64fd7aa700ea254cdd7_298763_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-07-25-chinese-culture-completed/2015-07-25-chinese-culture-cert-gu-lu_huded8c4e173a6c64fd7aa700ea254cdd7_298763_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="culture" class="gallery-image" data-flex-grow="69" data-flex-basis="167px" ></p> 2015.07 (C++) 一个可注销的通用多路回调列表 https://gulu-dev.com/post/2015-07-22-cpp-multicast/ Wed, 22 Jul 2015 21:01:00 +0000 https://gulu-dev.com/post/2015-07-22-cpp-multicast/ <h2 id="背景">背景</h2> <p>回调列表是个很常见的东东,经常被用在 Observer 这样的<a class="link" href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern" target="_blank" rel="noopener" >订阅/发布模式</a>里。当系统触发一个事件时,会遍历所有已经注册的回调列表,挨个调用,通知到相关的对象。</p> <p>我们知道,为了保持对 C 尽可能的兼容,一直以来,C++ 中的函数并非是所谓的“一级对象” (first-class objects)。而在函数指针的帮助下,我们可以在 C/C++ 中模拟一些 <a class="link" href="https://en.wikipedia.org/wiki/First-class_function" target="_blank" rel="noopener" >First-class function</a> 才有的特性,比如把函数像值一样以参数传递和保存。到了 C++11 的出现,有了语言和标准库级别的 <code>lambda</code> / <code>closure</code> / <code>std::function</code> 之后,对函数的操作才变得真正灵活和丰富起来。</p> <hr> <p>常见的 C/C++ 回调列表有以下这几种实现方式:</p> <ol> <li><strong>基类指针 (形如 <code>std::vector&lt;IListener*&gt;</code>)</strong>,当回调发生时,以虚函数的形式通知到不同的派生类的对象。这个方案的问题在于,凡是想加入这个列表,必须从 IListener 派生,而且所有的虚函数要求签名严格一致,耦合太高,灵活性较差。</li> <li><strong>函数指针 (形如 <code>std::vector&lt;fnCallback&gt;</code>)</strong>,当回调发生时,挨个调用容器中的函数指针。这个方案避免了继承的强耦合,但仍需要保证所有的响应函数签名一致,而且每一种类型的响应函数都要定义不同的回调列表,多了之后非常啰嗦,再一个函数指针本身可读性也欠佳。</li> <li><strong>函数对象 (形如<code>std::vector&lt;std::function&lt; ... &gt;&gt;</code>)</strong>,这种回调列表相对于上面两个更加灵活一些,不仅不需要继承,在 <code>std::bind</code> 的帮助下,连函数签名也不需要一致。但问题是,由于 <code>std::function</code> 无法使用 <code>==</code> 和 <code>!=</code> 来比较(见<a class="link" href="http://www.boost.org/doc/libs/1_58_0/doc/html/function/faq.html#idp205088688" target="_blank" rel="noopener" >参考一(第1条)</a>和<a class="link" href="http://stackoverflow.com/questions/3629835/why-is-stdfunction-not-equality-comparable" target="_blank" rel="noopener" >参考二</a>),注销比较麻烦,不像上面两个可以直接指针比较。</li> </ol> <hr> <h2 id="好处">好处</h2> <p>那么这里介绍的所谓通用回调列表有何好处呢?</p> <ol> <li>(以所谓“完美转发”的形式)支持任意个数和类型的参数调用</li> <li>在上面第三点 <code>std::function&lt;&gt;</code> 的基础上,可以使用 <code>std::string</code> 作为 tag, 标记那些后面需要被注销的函数,也同时支持不打 tag 的函数</li> <li>在需要时,支持批量地收集这些回调函数的返回值</li> </ol> <hr> <h2 id="接口">接口</h2> <p>说完了好处,接下来看一下 <code>BtMulticast</code> 这个类的对外接口和基本的使用吧:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="n">TRet</span><span class="p">,</span> <span class="k">class</span><span class="err">... </span><span class="nc">TArgs</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">BtMulticast</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="k">public</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="k">using</span> <span class="n">TFunc</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">function</span> <span class="o">&lt;</span> <span class="n">TRet</span><span class="p">(</span><span class="n">TArgs</span><span class="p">...)</span> <span class="o">&gt;</span> <span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">using</span> <span class="n">TElem</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">pair</span> <span class="o">&lt;</span> <span class="n">TFunc</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="o">&gt;</span> <span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">using</span> <span class="n">TRetVect</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span> <span class="o">&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">pair</span> <span class="o">&lt;</span> <span class="n">TRet</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="o">&gt;</span> <span class="o">&gt;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="nf">AddFunc</span><span class="p">(</span><span class="n">TFunc</span> <span class="n">func</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="nf">AddFunc</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">tag</span><span class="p">,</span> <span class="n">TFunc</span> <span class="n">func</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="kt">void</span> <span class="nf">RemoveFunc</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">tag</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">template</span><span class="o">&lt;</span><span class="k">class</span><span class="err">... </span><span class="nc">U</span><span class="o">&gt;</span> <span class="kt">void</span> <span class="n">Invoke</span><span class="p">(</span><span class="n">U</span><span class="o">&amp;&amp;</span><span class="p">...</span> <span class="n">u</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">template</span><span class="o">&lt;</span><span class="k">class</span><span class="err">... </span><span class="nc">U</span><span class="o">&gt;</span> <span class="n">TRetVect</span> <span class="n">InvokeR</span><span class="p">(</span><span class="n">U</span><span class="o">&amp;&amp;</span><span class="p">...</span> <span class="n">u</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">private</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">vector</span> <span class="o">&lt;</span> <span class="n">TElem</span> <span class="o">&gt;</span> <span class="n">m_funcList</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个类很简短,</p> <ul> <li><code>AddFunc</code> / <code>RemoveFunc</code> 是添加和删除回调函数</li> <li><code>Invoke</code> / <code>InvokeR</code> 分别触发无返回值和普通返回值的回调。</li> </ul> <p>需要注意的是,</p> <ol> <li><code>AddFunc()</code> 可以选择指明 tag, 在这种情况下可通过指明 tag 来 <code>RemoveFunc</code></li> <li><code>InvokeR()</code> 实际上返回的是一个返回值列表,采集了每一个回调的结果</li> <li><code>TFunc</code> 这个类型定义了最终存储在 <code>BtMulticast</code> 类中的回调函数对象,利用了 C++11 的所谓“完美转发”来把任意类型和个数的参数转发给回调函数</li> <li>考虑到 add/remove 通常只发生一次,而每次触发事件都会遍历,内部的存储选择 <code>std::vector</code>,牺牲了一点 add/remove 时的查找速度,换得更快更紧凑的遍历。而看一下实现代码就可以知道,牺牲的那点 add/remove 速度也只有在有 tag 的情况下会发生。</li> </ol> <hr> <h2 id="用法">用法</h2> <p>使用方面,基本用法如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="c1">// testing multicast: simplest </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">BtMulticast</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span> <span class="n">test</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">([]()</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (simplest): func 1 called. &#34;</span><span class="p">);</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">([]()</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (simplest): func 2 called. &#34;</span><span class="p">);</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">([]()</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (simplest): func 3 called. &#34;</span><span class="p">);</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">Invoke</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>三个匿名函数被添加进 <code>test</code> 对象,然后在 <code>test.Invoke()</code> 的时候被依次调用。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span><span class="lnt">9 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="c1">// testing multicast: tagged &amp; single parameter </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">BtMulticast</span><span class="o">&lt;</span><span class="kt">void</span><span class="p">,</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="n">test</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">(</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="p">[](</span><span class="kt">int</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (tagged): func a called (param: %d). &#34;</span><span class="p">,</span> <span class="n">p</span><span class="p">);</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">(</span><span class="s">&#34;b&#34;</span><span class="p">,</span> <span class="p">[](</span><span class="kt">int</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (tagged): func b called (param: %d). &#34;</span><span class="p">,</span> <span class="n">p</span><span class="p">);</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">(</span><span class="s">&#34;c&#34;</span><span class="p">,</span> <span class="p">[](</span><span class="kt">int</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (tagged): func c called (param: %d). &#34;</span><span class="p">,</span> <span class="n">p</span><span class="p">);</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">RemoveFunc</span><span class="p">(</span><span class="s">&#34;b&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="n">test</span><span class="p">.</span><span class="n">Invoke</span><span class="p">(</span><span class="mi">15</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>三个 tag 分别为 &ldquo;a&rdquo;, &ldquo;b&rdquo;, &ldquo;c&rdquo; 的匿名函数 (参数为 <code>int</code>,注意实例化 <code>BtMulticast</code> 时的类型参数列表变化) 被注册进来,然后 tag 为 &ldquo;b&rdquo; 的匿名函数被移除,最后以 15 作为参数依次调用剩下的回调函数 (&ldquo;a&rdquo; 和 &ldquo;c&rdquo;)。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="c1">// testing multicast with multiple parameters and return value list </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">BtMulticast</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="n">testRet</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">testRet</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">(</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="p">[](</span><span class="kt">int</span> <span class="n">p1</span><span class="p">,</span> <span class="kt">int</span> <span class="n">p2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">int</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (with RetVal): func a called (p1: %d, p2: %d). &#34;</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">);</span> <span class="k">return</span> <span class="n">p1</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">*</span> <span class="n">p2</span><span class="p">;</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">testRet</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">(</span><span class="s">&#34;b&#34;</span><span class="p">,</span> <span class="p">[](</span><span class="kt">int</span> <span class="n">p1</span><span class="p">,</span> <span class="kt">int</span> <span class="n">p2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">int</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (with RetVal): func b called (p1: %d, p2: %d). &#34;</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">);</span> <span class="k">return</span> <span class="n">p1</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">p2</span><span class="p">;</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">testRet</span><span class="p">.</span><span class="n">AddFunc</span><span class="p">(</span><span class="s">&#34;c&#34;</span><span class="p">,</span> <span class="p">[](</span><span class="kt">int</span> <span class="n">p1</span><span class="p">,</span> <span class="kt">int</span> <span class="n">p2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">int</span> <span class="p">{</span> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (with RetVal): func c called (p1: %d, p2: %d). &#34;</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">);</span> <span class="k">return</span> <span class="n">p1</span> <span class="o">+</span> <span class="mi">3</span> <span class="o">*</span> <span class="n">p2</span><span class="p">;</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="n">testRet</span><span class="p">.</span><span class="n">RemoveFunc</span><span class="p">(</span><span class="s">&#34;b&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;</span> <span class="nl">p</span> <span class="p">:</span> <span class="n">testRet</span><span class="p">.</span><span class="n">InvokeR</span><span class="p">(</span><span class="mi">20</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">BT_LOG</span><span class="p">(</span><span class="s">&#34;Multicast (with RetVal): func %s returned %d. &#34;</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">second</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">first</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>最后这个用例测试了多个参数和返回值的情况,可以看到 &ldquo;a&rdquo;, &ldquo;b&rdquo;, &ldquo;c&rdquo; 做了不同的操作后,返回的值被采集到了一个返回值列表里面,这个列表被就地 (即所谓的 <code>move</code> 语意) 遍历,内部的值可以根据需要再进行处理。这个用例的运行结果如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Multicast (with RetVal): func a called (p1: 20, p2: 2). </span></span><span class="line"><span class="cl">Multicast (with RetVal): func c called (p1: 20, p2: 2). </span></span><span class="line"><span class="cl">Multicast (with RetVal): func a returned 22. </span></span><span class="line"><span class="cl">Multicast (with RetVal): func c returned 26. </span></span></code></pre></td></tr></table> </div> </div><p>可以看到 <code>BtMulticast</code> 能够适配任意个数和类型的参数,因此可认为具有一定的通用性。</p> <hr> <h2 id="实现">实现</h2> <p>最后我们简单看一下实现。先看看 <code>BtMulticast::AddFunc()</code>,</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="n">TRet</span><span class="p">,</span> <span class="k">class</span><span class="err">... </span><span class="nc">TArgs</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="kt">bool</span> <span class="n">BtMulticast</span><span class="o">&lt;</span><span class="n">TRet</span><span class="p">,</span> <span class="n">TArgs</span><span class="p">...</span><span class="o">&gt;::</span><span class="n">AddFunc</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">tag</span><span class="p">,</span> <span class="n">TFunc</span> <span class="n">func</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// check if this tag has been used </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="n">tag</span><span class="p">.</span><span class="n">size</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">find_if</span><span class="p">(</span><span class="n">m_funcList</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">m_funcList</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="o">&amp;</span><span class="n">tag</span><span class="p">](</span><span class="k">const</span> <span class="n">TElem</span><span class="o">&amp;</span> <span class="n">elem</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">elem</span><span class="p">.</span><span class="n">second</span> <span class="o">==</span> <span class="n">tag</span><span class="p">;</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">it</span> <span class="o">!=</span> <span class="n">m_funcList</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">m_funcList</span><span class="p">.</span><span class="n">emplace_back</span><span class="p">(</span><span class="n">func</span><span class="p">,</span> <span class="n">tag</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>当 tag 有效时,先判定是否有 tag 冲突,然后注册一下回调,过程很直白就不多说了。</p> <hr> <p>再看一下具体的调用过程 <code>BtMulticast::InvokeR()</code>,</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="cm">/* ----- Note ----- </span></span></span><span class="line"><span class="cl"><span class="cm"> `BtMulticastRetVect` is an extra alias especially for the returning type for the signature of InvokeR() below, </span></span></span><span class="line"><span class="cl"><span class="cm"> since `TRetVect` defined inside `BtMulticast` cannot be used in the signature (outside the function body) </span></span></span><span class="line"><span class="cl"><span class="cm"> although `BtMulticastRetVect` is defined separately, it literally equals to `typename BtMulticast::TRetVect` </span></span></span><span class="line"><span class="cl"><span class="cm">*/</span> </span></span><span class="line"><span class="cl"><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="n">TRet</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="n">BtMulticastRetVect</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span> <span class="o">&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">pair</span> <span class="o">&lt;</span> <span class="n">TRet</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="o">&gt;</span> <span class="o">&gt;</span> <span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="n">TRet</span><span class="p">,</span> <span class="k">class</span><span class="err">... </span><span class="nc">TArgs</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="k">template</span> <span class="o">&lt;</span><span class="k">class</span><span class="err">... </span><span class="nc">U</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="n">BtMulticastRetVect</span><span class="o">&lt;</span><span class="n">TRet</span><span class="o">&gt;</span> <span class="n">BtMulticast</span><span class="o">&lt;</span><span class="n">TRet</span><span class="p">,</span> <span class="n">TArgs</span><span class="p">...</span><span class="o">&gt;::</span><span class="n">InvokeR</span><span class="p">(</span><span class="n">U</span><span class="o">&amp;&amp;</span><span class="p">...</span> <span class="n">u</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">BtMulticastRetVect</span><span class="o">&lt;</span><span class="n">TRet</span><span class="o">&gt;</span> <span class="n">ret</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;</span> <span class="nl">p</span> <span class="p">:</span> <span class="n">m_funcList</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">ret</span><span class="p">.</span><span class="n">emplace_back</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">first</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">forward</span><span class="o">&lt;</span><span class="n">U</span><span class="o">&gt;</span><span class="p">(</span><span class="n">u</span><span class="p">)...),</span> <span class="n">p</span><span class="p">.</span><span class="n">second</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">ret</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这里可以看到我单独定义了一下返回值的类型,具体原因见注释,大体上是说类内定义的类型 <code>TRetVect</code> 只能在类内使用 (包括类定义及相关的成员函数体的定义,成员函数的签名不算在内)。另外这函数前面的两个 <code>template</code> 声明分别是类的模板和函数的模板。</p> <hr> <p>俺一直觉得 C++ 的模板声明挺啰嗦,很有孔乙己范儿,看了上面这个函数声明,你也一定深有同感罢。应该跟 D 学一下,简化一下。</p> <p>C++ 的 <code>typedef</code> 和 <code>class template</code>,</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="kt">double</span> <span class="n">A</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">template</span><span class="o">&lt;</span><span class="k">class</span> <span class="nc">T</span><span class="o">&gt;</span> <span class="k">struct</span> <span class="nc">B</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">typedef</span> <span class="kt">int</span> <span class="n">A</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span></code></pre></td></tr></table> </div> </div><p>D 的对应语法 <code>alias</code> 和模板的 <code>(T)</code> 语法,简洁到没朋友。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-d" data-lang="d"><span class="line"><span class="cl"><span class="kd">alias</span> <span class="n">A</span> <span class="o">=</span> <span class="kt">double</span><span class="o">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nf">B</span><span class="o">(</span><span class="n">T</span><span class="o">)</span> </span></span><span class="line"><span class="cl"><span class="o">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">alias</span> <span class="n">A</span> <span class="o">=</span> <span class="kt">int</span><span class="o">;</span> </span></span><span class="line"><span class="cl"><span class="o">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>不过 C++ 已经把 D 的 <code>alias</code> 关键字的用法学来了,翻到前面可以看到 <code>class BtMulticast</code> 的定义中的那一组 <code>using</code>,把 <code>alias</code> 抄了个十足十,啧啧,借鉴得不错。</p> <hr> <p><code>BtMulticast</code> 类的实现和测试用例代码见<a class="link" href="https://gist.github.com/mc-gulu/ae73eb94e005fcf720c6" target="_blank" rel="noopener" >这里</a>。</p> <hr> <p>[2015-07-23] 补:代码中的 <code>.push_back(std::make_pair(func, tag))</code> 已替换为更紧凑的 <code>.emplace_back(func, tag)</code>。段落标题也为了清晰起见重新做了划分。</p> <hr> <p>[2015-07-24] 补:有同学反映</p> <pre><code>函数名不够 fasion,至少把 Invoke 调用弄成 operator() 也行吧。 </code></pre> <p>这个原因很简单,就像 C++ 把 C 语言的强制转型的语法 <code>(Foo *)pObject</code> 给扩展成 <code>dynamic_cast&lt;Foo *&gt;(pObject)</code> 一样,仅仅是为了可读性和易搜索性,<code>Invoke</code> 和 <code>InvokeR</code> 不仅给了这两个函数独特的符号,而且也很容易在批量查找时区分。嗯,朴素一点也挺好。</p> 2015.07 UQuadtree - 在 Unity 下实现场景资源的动态管理 https://gulu-dev.com/post/2015-07-11-u3d-uquadtree/ Sat, 11 Jul 2015 22:28:00 +0000 https://gulu-dev.com/post/2015-07-11-u3d-uquadtree/ <p>当游戏场景偏大时,由于目标平台资源一般比较有限,通常我们不会把该场景的所有资源一次性展开到内存里。常见做法是,把大大小小的各种资源用某种空间分割的数据结构组织起来,当玩家在场景中移动的时候,可以有效地获取即将出现的资源列表,触发对应的异步加载,并及时释放玩家离开区域的资源。这样的话,无论场景规模多大,同一时刻出现在内存中的数据量总是相对可控的。</p> <hr> <p>为了缓解场景的资源压力,昨天实现了一个基于四叉树的动态资源管理,代码在<a class="link" href="https://github.com/SeaSunOpenSource/uquadtree" target="_blank" rel="noopener" >这里</a>。顺便把设计和实现时的考虑简单地记录一下,以节省沟通的成本。</p> <hr> <p>四叉树是常用的空间分割数据结构。跟 BSP 和 Octree 相比,它非常清晰和规律,在平面上展开时,观察和调试又足够简单。使用四叉树实现的空间管理,可以较好地兼顾开发效率和运行效率。在 <a class="link" href="http://gulu-dev.com/post/2014-11-16-open-world" target="_blank" rel="noopener" >开放世界游戏中的大地图背后有哪些实现技术?</a> 一文中我曾提到 “当尺度大到一定规模之后,地形通常退化为相对扁平的2D空间”,在实际的 3D 游戏项目里,水平方向上的场景复杂度一般也会远大于垂直方向上的,因此四叉树比八叉树往往更适合实际项目的需要。</p> <p><img src="https://gulu-dev.com/qt1.png" loading="lazy" alt="qt1" ></p> <p>这是一个测试用的模拟场景,内含 5000 个形状和变换各异的测试模型。左边的 Move 按钮和 &ldquo;Always Move&rdquo; 复选框用于模拟玩家的移动, &ldquo;Debug Lines&rdquo; 用于画出场景中调试线条。</p> <p><img src="https://gulu-dev.com/qt2.png" loading="lazy" alt="qt2" ></p> <p>这是动态加载开启后的负载情况。可以见到,在玩家(白色柱子)向目标(紫红柱子) 移动的过程中,任意时刻,只有玩家所在区域附近的物件在内存中持有。这张图上还可以看到,玩家远离的方向比面向的方向物件多,这是 swap-out 比 swap-in 的判定距离远一些的缘故,这样主要是为了避免了玩家来回移动时的内存颠簸,也就是对象的反复加载 (降低了 IO 总量)。</p> <p><img src="https://gulu-dev.com/qt3.png" loading="lazy" alt="qt3" ></p> <p>这是在 Scene View 下开启了 Debug Lines 选项后看到的调试视角。可以看到,场景被切分成了均匀四叉树,每一个小的 cell 都是四叉树的一个叶节点 (UQtLeaf) ,其中:</p> <ul> <li>灰色 cells 是非活跃叶节点,这些叶节点上的所有物件都被释放,不占用内存</li> <li>白色 cells 是玩家当前持有的所有叶节点 (对应代码中的 _holdingLeaves)</li> <li>绿色 cells 是正在被交换进来 (swap-in) 的叶节点 (这些节点上的所有物件异步加载完成后,这个 cell 会变为白色)</li> <li>红色 cells 是正在被交换出去 (swap-out) 的叶节点 (这些节点上的所有物件销毁后,这个 cell 会变为灰色)</li> </ul> <hr> <p>上面三张图基本上已经把功能说得差不多了,接下来我们简单过一下代码,略作补充。</p> <p><img src="https://gulu-dev.com/code1.png" loading="lazy" alt="code1" ></p> <p><code>UQtConfig</code> 这个类内含一些参数,用于按需配置和控制 <code>UQuadtree</code> 内部的一些行为。请注意,对这些参数的调整会直接影响性能表现。目前页交换的触发 (SwapTriggerInterval) 是 0.5s 一次,页状态更新 (SwapProcessInterval) 是 0.2s 一次,这些是为了在保持较低的 CPU 开销下,能够有较高的反应速度。</p> <p><img src="https://gulu-dev.com/code2.png" loading="lazy" alt="code2" ></p> <p><code>IQtUserData</code> 是每个叶节点上挂载的用户对象所需实现的接口,使用接口而不是独立委托,可以更明确和清晰地定义 <code>UQuadtree</code> 与用户数据之间的约定。一个典型的用户实现 <code>QtTypicalUserData</code> 应当至少包括一个资源路径字符串 (ResourcePath) 和一个 GameObject——当对应的叶节点被交换进内存时,由 ResourcePath 发起异步请求,载入 GameObject;当对应的叶节点被交换出去时, GameObject 被释放,ResourcePath 被保留用于下一次的载入。</p> <hr> <p>代码及对应的测试工程在 Unity-5.0.1f1 下编译和运行通过,还没有来得及做针对 4.6 的向后移植工作,先这样吧。</p> 2015.06 Unity 项目实践点滴 https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips/ Sun, 28 Jun 2015 18:47:00 +0000 https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips/ <img src="proxy.php?url=https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips/Unity_3D_logo.png" alt="Featured image of post 2015.06 Unity 项目实践点滴" /><p>[2015-06-29] 补:有同学问手机上代码横向显示不下的问题。可以向左滑动对应的代码块,就能看到右边的部分。不过的确不如 PC 上方便 :)</p> <hr> <p>这两年,游戏行业的重心逐渐迁移到了手游,引擎开发工具大有被 Unity 席卷之势,其掀起的声浪可谓是横扫了当年曾在国内风光一时的 Unreal / Gamebryo / OGRE 等诸前辈。好了,废话不多说,今天我们简单地聊一下 Unity 项目中的一些具体的实践。</p> <pre><code>&quot;Unity can only be manifested by the Binary. Unity itself and the idea of Unity are already two.&quot; - Buddha </code></pre> <hr> <h2 id="高精度计时器">高精度计时器</h2> <p>在日常开发中,出于测试或优化的需要,我们常常需要使用比毫秒数更精确的计时器。因此在深入之前,我们先了解一下如何在 Unity 平台上获取足够的计时精度。</p> <hr> <p>我们知道,在传统的 Win32 平台上,游戏开发者通常使用 QueryPerformanceFrequency 和 QueryPerformanceCounter 来计时,这两个函数依赖 rdtsc 指令来获取时钟周期数,能得到相对比较精确的计时结果。而在 .Net 平台上,<a class="link" href="https://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx" target="_blank" rel="noopener" >System.Diagnostics.StopWatch</a> 的功能与这两个函数类似,在 MSDN 文档中有提到:</p> <pre><code>The Stopwatch class assists the manipulation of timing-related performance counters within managed code. Specifically, the Frequency field and GetTimestamp method can be used in place of the unmanaged Win32 APIs QueryPerformanceFrequency and QueryPerformanceCounter. </code></pre> <p>也就是说 StopWatch 在可能的情况下,会使用与 QueryPerformanceFrequency/QueryPerformanceCounter 类似的机制来达到与其相当的精度。</p> <p>这里同时还提到 StopWatch 的几个有趣的接口:</p> <ul> <li>可以通过 <code>Stopwatch.IsHighResolution</code> 来判断当前是否为高精度</li> <li>可以通过 <code>StopWatch.Frequency</code> 来获取当前的时钟频率</li> <li>可以通过 <code>StopWatch.GetTimestamp()</code> 来获取当前的时间戳</li> </ul> <p>我们在 Unity 中简单验证一下这些接口在 Mono 2.x 上的可用性:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="n">UnityEngine</span><span class="p">.</span><span class="n">Debug</span><span class="p">.</span><span class="n">LogFormat</span><span class="p">(</span><span class="s">&#34;high-res: {0}, freq: {1}, timestamp: {2}&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">Stopwatch</span><span class="p">.</span><span class="n">IsHighResolution</span><span class="p">,</span> <span class="n">Stopwatch</span><span class="p">.</span><span class="n">Frequency</span><span class="p">,</span> <span class="n">Stopwatch</span><span class="p">.</span><span class="n">GetTimestamp</span><span class="p">());</span> </span></span></code></pre></td></tr></table> </div> </div><p>输出的结果是</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> high-res: True, freq: 10000000, timestamp: 63121800066 </span></span></code></pre></td></tr></table> </div> </div><p>这里可看出几个有趣的现象:</p> <ol> <li>高精度计时是开启的,在 Mono 2.x 上是可用的</li> <li>频率被归一化为 1e7 了,而非返回实际的 CPU 频率,这个的好处是 ElapsedTicks 从周期数变为了一个有逻辑意义的时间计量,这也就是在说,StopWatch 提供的计时服务__最高精度为 0.1 微秒(也即 100 纳秒)__</li> <li>时间戳 <code>GetTimestamp()</code> 可用,而且返回的值是一个有效的 64 bits long (大于 2^32),也就是基本不用担心溢出回绕的问题</li> </ol> <hr> <p>我在 StopWatch 上封装了一个简单的 Timer 类,用法如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">using</span> <span class="p">(</span><span class="n">SSTimer</span> <span class="n">t</span> <span class="p">=</span> <span class="k">new</span> <span class="n">SSTimer</span><span class="p">(</span><span class="s">&#34;_name_tag_&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// test code </span> </span></span><span class="line"><span class="cl"> <span class="n">foo</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>输出如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> &#39;_name_tag_&#39; exec time: 33.332 (ms) </span></span></code></pre></td></tr></table> </div> </div><p>这个工具 SSTimer 的代码在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/SSTimer.cs" target="_blank" rel="noopener" >这里</a>。后面的性能相关的对比数据,均由此计时器统计而来。</p> <hr> <h2 id="monoc-代码实践">Mono/C# 代码实践</h2> <h3 id="for--foreach-问题">for / foreach 问题</h3> <p>这个问题之前有一些争论,不过<a class="link" href="http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php" target="_blank" rel="noopener" >这里 (&ldquo;Should you avoid foreach loops?&rdquo; 一节的最后的 [EDIT] 补充部分)</a>和<a class="link" href="http://www.zhihu.com/question/30334270" target="_blank" rel="noopener" >这里 (见 @王建飞 和 @权然 的回答)</a> 已经说得很清楚了。我自己也分别看了不同情况下 VS 和 Mono 编译出来的 il 代码,肯定了他们的观点,在这里简单归纳一下结论吧:</p> <ol> <li>直到 Unity 5.0.1 (说好的 5.x 修复呢?) 为止,如果你的代码用 Unity 自带的 Mono 编译器,无论使用的是标准容器 (自带 struct-enumerator 优化) 还是自定义容器 (手动 struct-enumerator 优化),都无法避免 foreach 展开后经由 GetEnumerator() 所获取出的 struct-enumerator 产生的一个额外的 boxing 动作 (及对应的内存分配)。简单地说,<strong>由 Unity 自带编译器编译的代码,建议不要使用 foreach</strong>。</li> <li>然而,如果你的代码使用 VS 的 C# 编译器以目标为 &ldquo;Unity 3.5 .net Subset Base Class Libraries&rdquo; 编译出 dll,并把此 dll 放在项目的 Assets 目录下供 Unity 直接使用的话,就可以得到 struct-enumerator 优化所带来的好处,无需担心额外开销。也就是说,<strong>以 dll (由 VS 编译) 方式使用的代码,可以放心用 foreach</strong>。</li> <li>再补充一点我实测的,如果是数组的遍历,在这两种方式下都不会产生额外的开销,而且在 il 中不创建 enumerator,也就没有对应的 MoveNext() / Dispose() 调用,这样连 try / finally block 也不再生成,生成的代码短了不少。也就是说,<strong>数组使用 foreach 没有任何限制,而且遍历效率较容器要高</strong>。(使用上面的高精度计时器测得:遍历百万元素的数组 (int[]),列表 (List<int>) 和字典 (Dictionary&lt;int, int&gt;) 分别耗时 6.263ms / 32.65ms / 32.385ms,后两者耗时是数组的 5 倍多,所以能用数组就尽量用数组吧)</li> </ol> <hr> <p>相关的对比测试的代码在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/T01_foreach.cs" target="_blank" rel="noopener" >这里</a>。</p> <hr> <h3 id="lambda-表达式-vs-闭包-closure">lambda 表达式 vs. 闭包 (closure)</h3> <p>我们知道,闭包 (closure) 本质上是包含了&quot;外部&quot;变量状态的 lambda 表达式。从<a class="link" href="http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php" target="_blank" rel="noopener" >这里 (见 “Should you avoid closures and LINQ?” 一段)</a>可以知道,lambda 表达式能够被编译和替换为对应类的一个静态字段,而 closure 由于储存了额外的修改状态,编译器需要创建一个新类来表示和引用,由于C#有比较丰富的类型信息,不仅创建时开销比前者高,而且也隐含着 100 字节以上的内存开销。</p> <p>简单地说,跟闭包相比,<strong>lambda 表达式要轻量得多,可以放心使用</strong>。</p> <hr> <p>但是这里才是真正的问题——由于普通的 lambda 表达式和闭包长得非常相似,实践中如果不小心,是非常容易弄混淆的。不信的话,试试判断一下,下面这几个函数中的 <code>func</code>,有几个会被 Unity 认为是闭包?</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span><span class="lnt">32 </span><span class="lnt">33 </span><span class="lnt">34 </span><span class="lnt">35 </span><span class="lnt">36 </span><span class="lnt">37 </span><span class="lnt">38 </span><span class="lnt">39 </span><span class="lnt">40 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">static</span> <span class="k">void</span> <span class="n">Test_lambda_01</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">System</span><span class="p">.</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">&gt;</span> <span class="n">func</span> <span class="p">=</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">e</span> <span class="p">*</span> <span class="n">e</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">result</span> <span class="p">=</span> <span class="n">func</span><span class="p">(</span><span class="m">6</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">++</span><span class="n">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">static</span> <span class="kt">int</span> <span class="m">_f</span><span class="n">oo2</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">static</span> <span class="k">void</span> <span class="n">Test_lambda_02</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">System</span><span class="p">.</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">&gt;</span> <span class="n">func</span> <span class="p">=</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">e</span> <span class="p">*</span> <span class="n">e</span> <span class="p">+</span> <span class="m">_f</span><span class="n">oo2</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">result</span> <span class="p">=</span> <span class="n">func</span><span class="p">(</span><span class="m">6</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">++</span><span class="n">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">static</span> <span class="kt">int</span> <span class="m">_f</span><span class="n">oo3</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">static</span> <span class="k">void</span> <span class="n">Test_lambda_03</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">System</span><span class="p">.</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">&gt;</span> <span class="n">func</span> <span class="p">=</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">e</span> <span class="p">*</span> <span class="n">e</span> <span class="p">+</span> <span class="p">(++</span><span class="m">_f</span><span class="n">oo3</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">result</span> <span class="p">=</span> <span class="n">func</span><span class="p">(</span><span class="m">6</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">++</span><span class="n">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">static</span> <span class="k">void</span> <span class="n">Test_lambda_04</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="m">_f</span><span class="n">oo4</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">System</span><span class="p">.</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">&gt;</span> <span class="n">func</span> <span class="p">=</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">e</span> <span class="p">*</span> <span class="n">e</span> <span class="p">+</span> <span class="m">_f</span><span class="n">oo4</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">result</span> <span class="p">=</span> <span class="n">func</span><span class="p">(</span><span class="m">6</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">++</span><span class="n">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">static</span> <span class="k">void</span> <span class="n">Test_lambda_05</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="m">_f</span><span class="n">oo5</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">System</span><span class="p">.</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">&gt;</span> <span class="n">func</span> <span class="p">=</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">e</span> <span class="p">*</span> <span class="n">e</span> <span class="p">+</span> <span class="p">(++</span><span class="m">_f</span><span class="n">oo5</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">result</span> <span class="p">=</span> <span class="n">func</span><span class="p">(</span><span class="m">6</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">++</span><span class="n">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><hr> <p>如果你能无需思考地给出答案,那么还请受俺真心的一拜,佩服佩服。</p> <p>C# 下的闭包与普通的 lambda 表达式难以区分的原因是,C# 的闭包不像 C++ 那样需要显式地指明自己引用的外部变量。C++ 通过所谓的方括号“捕获”语法 ( square brackets capturing ) 来声明一个闭包,非常清晰地指明自己需要的外部环境,比如:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">some_list</span><span class="p">{</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">auto</span> <span class="n">func</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="n">total</span><span class="p">](</span><span class="kt">int</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span> <span class="n">total</span> <span class="o">+=</span> <span class="n">x</span><span class="p">;</span> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">for_each</span><span class="p">(</span><span class="n">begin</span><span class="p">(</span><span class="n">some_list</span><span class="p">),</span> <span class="n">end</span><span class="p">(</span><span class="n">some_list</span><span class="p">),</span> <span class="n">func</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>这里的方括号显式地以引用方式“捕获”了 <code>total</code> 作为闭包的一部分,而 C# 把这个简化成由编译器推导了。这里俺觉得还是 <a class="link" href="http://typicalrunt.me/2014/06/05/ansible-tips-part-1-when-in-doubt-be-explicit/" target="_blank" rel="noopener" >&ldquo;When in Doubt, Be Explicit&rdquo;</a> 好一些。</p> <p>嗯,刚才的练习俺就卖个关子,不给出答案了。<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/T02_closure.cs" target="_blank" rel="noopener" >代码在这里</a>,感兴趣的同学运行这些代码所在的测试 Unity 工程,一试便知。</p> <hr> <p>关于 lambda 表达式和闭包,这次先讲这么多吧。要点是__保持短小__即可。对于这种嵌套的逻辑,随着代码量的增加,可读性会急剧下降,就会损失局部定义的好处。</p> <hr> <h3 id="枚举项的-tostring-vs-enumgetname">枚举项的 ToString() vs. Enum.GetName()</h3> <p>简单说一下吧。<a class="link" href="http://www.glenstevens.ca/unity3d-best-practices/" target="_blank" rel="noopener" >有种说法</a>是说不要使用枚举的 ToString() 来获取单个枚举项对应的字符串,应该使用 <code>Enum.GetName(typeof(Foo), Foo.Bar)</code> 据说后者的速度是前者的两倍。</p> <p>我实测了一下,一个含有 10 个枚举项的枚举 <code>FooTypes</code> 在遍历并转换每一项 10000 次的结果是:</p> <ul> <li>ToString() 耗时 553.817ms</li> <li>Enum.GetName() 耗时 437.2ms</li> </ul> <p>测试中的差距并不十分显著,所以结论是__直接使用枚举项的 ToString() 并无不妥,可读性亦较 Enum.GetName 更佳__。但有一点要注意的是,这种字符串化的绝对开销不低,<strong>算下来转 1000 次会花 5ms 左右</strong>,这个开销感觉对游戏来说已经比较高了。</p> <hr> <p>具体的测试代码在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/T03_enum_string.cs" target="_blank" rel="noopener" >这里</a>。</p> <hr> <h3 id="使用-as-转型-vs-使用-c-style-cast">使用 &ldquo;as&rdquo; 转型 vs. 使用 C-Style Cast</h3> <p>同上。按照<a class="link" href="http://www.glenstevens.ca/unity3d-best-practices/" target="_blank" rel="noopener" >这里的说法</a>:</p> <pre><code>“When casting a variable use the post fix “as type” instead of pre fixing with (type) as this is faster.” </code></pre> <p>直接说测试结果吧,实测百万次的有效转型的结果是:</p> <ul> <li>as 方式耗时 5.287ms</li> <li>C-Style 方式耗时 4.640ms</li> </ul> <p>同样是差距并不显著,而 as 方式甚至更慢一点。这是为什么呢?查阅 MSDN 可以在<a class="link" href="https://msdn.microsoft.com/en-us/library/cscsdfbt.aspx" target="_blank" rel="noopener" >这里</a>看到,</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> expression as type </span></span></code></pre></td></tr></table> </div> </div><p>实际上等价于</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> expression is type ? (type)expression : (type)null </span></span></code></pre></td></tr></table> </div> </div><p>也就是说,对于有效转型,<strong>&ldquo;as&rdquo; 的开销 = &ldquo;is&rdquo; 的开销 + &ldquo;c-style cast&rdquo; 的开销</strong>。这样就解释了前面的测试结果。</p> <p>结论,在 <strong>使用 &ldquo;as&rdquo; 还是 C-Style Cast 这个问题上,不要考虑性能影响</strong>,应按照它们各自提供的功能去选择。进一步说,如果你需要自动地把错误的转型以 null 为结果返回的话,使用 &ldquo;as&rdquo; 关键字;如果你认为这个转型足够重要,不希望别人由于忘记检查返回值导致没有处理该关键错误,那么就需要使用 C-Style Cast,这种方式会在转型失败时抛出 InvalidCastException 异常。多说一句,在 Unity 的框架内即使不处理,这个异常也会被 Unity 吞掉,然而我个人对这种不管三七二十一都一网打尽的方式持保留意见。</p> <hr> <p>具体的测试代码在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/T04_typecast.cs" target="_blank" rel="noopener" >这里</a>。</p> <hr> <h3 id="矩阵优化-i---矩阵乘法">矩阵优化 (I) - 矩阵乘法</h3> <p>在 <code>UnityEngine.dll</code> 的 <code>struct Matrix4x4</code> 定义 (网页版在<a class="link" href="http://docs.unity3d.com/ScriptReference/Matrix4x4-operator_multiply.html" target="_blank" rel="noopener" >这里</a>) 中,我们可以看到 Unity 的标准矩阵相乘函数的定义如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="c1">// Summary:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// A standard 4x4 transformation matrix.</span> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">struct</span> <span class="nc">Matrix4x4</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// (前略) ...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Matrix4x4</span> <span class="k">operator</span> <span class="p">*(</span><span class="n">Matrix4x4</span> <span class="n">lhs</span><span class="p">,</span> <span class="n">Matrix4x4</span> <span class="n">rhs</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// (后略) ...</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>那么它的性能具体如何呢?我简单地测试了一下,在 AMD 的 3.5G 八核处理器上,<strong>百万次矩阵相乘的时间为 339.681 (ms)</strong>。</p> <p><code>operator *</code> 是 Unity 引擎提供给用户的标准矩阵相乘的接口。对于一个典型的 3D 游戏来说,这个操作可能会被频繁地用到,因此对其进行适当的优化还是很有必要的。下面我们一起来看看,对矩阵相乘操作可以做哪些优化尝试,具体效果又能达到什么程度。</p> <p>为了方便,<code>operator *</code> 的版本,下面我们简单地称为__官方版__。</p> <hr> <h4 id="优化版本-1---brute-force-耗时-159676">优化版本 1 - brute-force (耗时 1596.76%)</h4> <p>首先,我们实现一个拿衣服版本的矩阵相乘 Mul_v1_naive:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span><span class="lnt">9 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Matrix4x4</span> <span class="n">Mul_v1_naive</span><span class="p">(</span><span class="n">Matrix4x4</span> <span class="n">m1</span><span class="p">,</span> <span class="n">Matrix4x4</span> <span class="n">m2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Matrix4x4</span> <span class="n">result</span> <span class="p">=</span> <span class="n">Matrix4x4</span><span class="p">.</span><span class="n">zero</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="m">4</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">j</span> <span class="p">&lt;</span> <span class="m">4</span><span class="p">;</span> <span class="n">j</span><span class="p">++)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">k</span> <span class="p">&lt;</span> <span class="m">4</span><span class="p">;</span> <span class="n">k</span><span class="p">++)</span> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">]</span> <span class="p">+=</span> <span class="n">m1</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">k</span><span class="p">]</span> <span class="p">*</span> <span class="n">m2</span><span class="p">[</span><span class="n">k</span><span class="p">,</span> <span class="n">j</span><span class="p">];</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个是很直接的教科书式的矩阵相乘运算。这个运算告诉了我们如下的信息:</p> <ul> <li>把拿衣服版本的运算结果与官方版做值比较,得到了一致的结果——这说明了官方版的行为与我们的期望完全一致。</li> <li>把运算的开销与官方版比较,不出所料,拿衣服版本够慢的,百万次耗时为 5413.076 (ms) 左右,是官方版的 15 倍。</li> </ul> <h4 id="优化版本-2---循环展开-耗时-10054">优化版本 2 - 循环展开 (耗时 100.54%)</h4> <p>拿衣服的 v1 版本有三重循环嵌套,光是肉眼看起来就很慢。那么我们把循环展开一下,写个 v2 测一下看看有什么变化。</p> <p>这是 Mul_v2_naive_expanded:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="n">Matrix4x4</span> <span class="n">Mul_v2_naive_expanded</span><span class="p">(</span><span class="n">Matrix4x4</span> <span class="n">m1</span><span class="p">,</span> <span class="n">Matrix4x4</span> <span class="n">m2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Matrix4x4 is a struct so &#39;new&#39; would still leave it on stack </span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">new</span> <span class="n">Matrix4x4</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">m00</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m00</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m00</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m01</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m10</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m02</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m20</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m03</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m30</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m01</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m00</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m01</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m01</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m11</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m02</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m21</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m03</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m31</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m02</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m00</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m02</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m01</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m12</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m02</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m22</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m03</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m32</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m03</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m00</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m03</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m01</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m13</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m02</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m23</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m03</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m33</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">m10</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m10</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m00</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m11</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m10</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m12</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m20</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m13</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m30</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m11</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m10</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m01</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m11</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m11</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m12</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m21</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m13</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m31</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m12</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m10</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m02</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m11</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m12</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m12</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m22</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m13</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m32</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m13</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m10</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m03</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m11</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m13</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m12</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m23</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m13</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m33</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">m20</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m20</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m00</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m21</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m10</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m22</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m20</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m23</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m30</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m21</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m20</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m01</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m21</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m11</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m22</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m21</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m23</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m31</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m22</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m20</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m02</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m21</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m12</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m22</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m22</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m23</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m32</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m23</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m20</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m03</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m21</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m13</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m22</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m23</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m23</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m33</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">m30</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m30</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m00</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m31</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m10</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m32</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m20</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m33</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m30</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m31</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m30</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m01</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m31</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m11</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m32</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m21</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m33</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m31</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m32</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m30</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m02</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m31</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m12</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m32</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m22</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m33</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m32</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">m33</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m30</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m03</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m31</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m13</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m32</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m23</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m33</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m33</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个版本跑下来已经跟官方版很接近了,上文提到官方版百万次是 339.681 (ms),而 Mul_v2_naive_expanded 的开销是 341.534 (ms),是前者耗时的 100.54%。有理由推断,官方版的 <code>operator *</code> 或多或少就是如此实现的。</p> <h4 id="优化版本-3---使用-ref-处理参数和返回值--耗时-2552">优化版本 3 - 使用 ref 处理参数和返回值 (耗时 25.52%)</h4> <p>我们注意到,由于 <code>Matrix4x4</code> 是一个 struct,参数传递时是传值而不是传引用,这意味着每次调用会产生三次矩阵的复制,算下来是 16 * 3 个 float 也就是 192 bytes 的复制。我们改为 ref 来消除这些无谓的复制,就得到了 v3 的实现。</p> <p>这是 Mul_v3_ref:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">Mul_v3_ref</span><span class="p">(</span><span class="k">ref</span> <span class="n">Matrix4x4</span> <span class="n">result</span><span class="p">,</span> <span class="k">ref</span> <span class="n">Matrix4x4</span> <span class="n">m1</span><span class="p">,</span> <span class="k">ref</span> <span class="n">Matrix4x4</span> <span class="n">m2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m00</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m00</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m00</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m01</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m10</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m02</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m20</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m03</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m30</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// (中略) ...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m33</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m30</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m03</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m31</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m13</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m32</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m23</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m33</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m33</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个版本的耗时为 86.703 (ms),缩短到了官方版的 1/4 左右。</p> <p>可见 <strong>“复制 192 字节”</strong> 所花的时间至少是 <strong>“64 次乘法 + 48 次加法”</strong> 的三倍 (&ldquo;至少&quot;二字是考虑到函数调用的开销),所以减少无谓的复制还是蛮重要的,呵呵。</p> <h4 id="优化版本-4---利用-3d-变换矩阵的特点-耗时-2097">优化版本 4 - 利用 3D 变换矩阵的特点 (耗时 20.97%)</h4> <p>我们知道,在 3D 矩阵的变换中,最后一行是 (0, 0, 0, 1) 不会变化,如下图:</p> <p><img src="https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips/mat43.png" width="414" height="155" srcset="https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips/mat43_hudf10cfd19fd41fcec32a4a750c966c26_2715_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-06-28-u3d-practices-and-tips/mat43_hudf10cfd19fd41fcec32a4a750c966c26_2715_1024x0_resize_box_3.png 1024w" loading="lazy" alt="matrix43" class="gallery-image" data-flex-grow="267" data-flex-basis="641px" ></p> <p>所以可以省掉这一部分冗余运算,得到 Mul_v4_for_3d_trans:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">Mul_v4_for_3d_trans</span><span class="p">(</span><span class="k">ref</span> <span class="n">Matrix4x4</span> <span class="n">result</span><span class="p">,</span> <span class="k">ref</span> <span class="n">Matrix4x4</span> <span class="n">m1</span><span class="p">,</span> <span class="k">ref</span> <span class="n">Matrix4x4</span> <span class="n">m2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// (前略,同上) ...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m23</span> <span class="p">=</span> <span class="n">m1</span><span class="p">.</span><span class="n">m20</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m03</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m21</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m13</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m22</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m23</span> <span class="p">+</span> <span class="n">m1</span><span class="p">.</span><span class="n">m23</span> <span class="p">*</span> <span class="n">m2</span><span class="p">.</span><span class="n">m33</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m30</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m31</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m32</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">result</span><span class="p">.</span><span class="n">m33</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个版本的耗时为 71.062 (ms),约是官方版的 1/5 。</p> <hr> <p>这里拿矩阵相乘做优化实际上是举个栗子,旨在说明在实践中__避免不必要的拷贝__的重要性。 至于何时使用 struct,何时使用 class,何时需要使用 ref 加持的 struct,相信大家能够根据具体的情况去更好地判断。</p> <hr> <p>这四个版本的实现代码和对应的测试代码可以在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/SSMatrix.cs" target="_blank" rel="noopener" >这里</a>和<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/T05_matrix.cs" target="_blank" rel="noopener" >这里</a>看到。</p> <hr> <h3 id="矩阵优化-ii---变换矩阵的缓存">矩阵优化 (II) - 变换矩阵的缓存</h3> <p>这里我们还是拿矩阵来举例子吧。下面是一个典型的变换到摄像机空间的操作 (为简化讨论仍使用官方版的乘法):</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="n">Matrix4x4</span> <span class="n">ret</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">worldToCameraMatrix</span> <span class="p">*</span> <span class="m">_</span><span class="n">inputMat</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>这里的运算本身并无不妥,但通常我们做这种变换的时候,是对一系列输入矩阵做变换,如下:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">ApplyTransform</span><span class="p">(</span><span class="n">Matrix4x4</span><span class="p">[]</span> <span class="n">outputMatrices</span><span class="p">,</span> <span class="n">Matrix4x4</span><span class="p">[]</span> <span class="n">inputMatrices</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">inputMatrices</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> </span></span><span class="line"><span class="cl"> <span class="n">outputMatrices</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">worldToCameraMatrix</span> <span class="p">*</span> <span class="n">inputMatrices</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这时问题就来了,<code>Camera.main.worldToCameraMatrix</code> 看起来只是获取对象的只读属性,但实际上是对一系列复杂表达式的求值,有时甚至还涉及到 safe/unsafe 的切换。</p> <p>那么反复对这个表达式求值,就值得被从循环中提取出来,如下所示:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">ApplyTransform</span><span class="p">(</span><span class="n">Matrix4x4</span><span class="p">[]</span> <span class="n">outputMatrices</span><span class="p">,</span> <span class="n">Matrix4x4</span><span class="p">[]</span> <span class="n">inputMatrices</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Matrix4x4</span> <span class="n">trans</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">worldToCameraMatrix</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">inputMatrices</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> </span></span><span class="line"><span class="cl"> <span class="n">outputMatrices</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">=</span> <span class="n">trans</span> <span class="p">*</span> <span class="n">inputMatrices</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>经过测试,提取前后,百万次变换的开销分别为 977.077 (ms) / 338.626 (ms)。可以看出,后者本质上就是百万次的矩阵相乘操作,与上边官方版的矩阵相乘的运算开销 (339.681 (ms)) 非常接近。而前者花了__近3倍__的运算时间,其中一大半都在对 <code>Camera.main.worldToCameraMatrix</code> 表达式求值。</p> <hr> <p>这个例子告诉我们,C# 的属性 (Property) 是非常有欺骗性的,可能内部隐藏了使用者难以预计的运算开销。这种欺骗性较 C++ 中的赋值和拷贝构造内的隐含逻辑更甚,因为后者毕竟有该类对应对象的尺寸作为参考,如果对象的尺寸偏大,我们理所当然地认为它的赋值会更费。而 C# 中的 Property 如果看不到代码,唯一可资参考的就是该 Property 的类型了,可是“某个属性的类型是什么”,与“它是怎么被计算出来的”,本质上是没啥关系的,所以欺骗性要强得多。</p> <p>补充一句,当我们使用没有代码的实现时,更要加倍留意这种隐藏的陷阱。</p> <hr> <p>这个测试的代码可以在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/blob/master/Assets/T06_matrix_caching.cs" target="_blank" rel="noopener" >这里</a>看到。</p> <hr> <h3 id="其他的一些零碎常识">其他的一些零碎常识</h3> <p>还有一些常识,简单归纳一下:</p> <ul> <li>利用 string 的 immutable 特性,在内存中单一实例 (Interning) 的特性</li> <li>利用 string 的比较性能好 (当引用方式为 object 时进行地址比较) 的特性</li> <li>在需要时使用 StringBuilder</li> <li>利用好容器的 Capacity 来优化内存访问</li> <li>利用 ref 和 struct 来把堆 (heap) 上的访问往栈 (stack) 上挪</li> <li>避免使用 LINQ 来降低零碎的内存分配</li> </ul> <p>这些常识稍有经验的 C# 程序员应该都很熟悉,就不一一赘述了。</p> <hr> <h2 id="内存相关的实践">内存相关的实践</h2> <h3 id="mono-265-的-gc-特性和应对方案">Mono 2.6.5 的 GC 特性和应对方案</h3> <p>直到目前我手头上的 Unity 5.0.1 为止,其内含 Mono 始终停留在 &ldquo;2.6.5.0&rdquo; 上 (特定 Unity 版本可查看 &ldquo;Editor\Data\Mono\lib\mono\2.0\mscorlib.dll&rdquo; 内的 <code>Consts.MonoVersion</code> 得知)。这个版本的 Mono 使用的 GC 仍是较老的 <a class="link" href="https://en.wikipedia.org/wiki/Boehm_garbage_collector" target="_blank" rel="noopener" >Boehm garbage collector</a>。</p> <hr> <p>这里先简单说一下 Boehm GC 实现的一些特点:</p> <ul> <li>基于 Mark/Sweep,无分代/并行</li> <li>执行时所有线程阻塞 (Stop-The-World)</li> <li>堆越接近满的状态,执行得越频繁</li> <li>每次标记都会扫描访问到所有可到达的对象</li> <li>标记阶段 (Mark Phase) 的性能数据 (仅作为参考) <ul> <li>在小对象的情况下,1.4GHz Itanium 能达到 500MB/Sec 的速度</li> <li>每个对象 90 个时钟周期左右 (大量时间是 cache-missing 所致)</li> <li>算下来每秒 15M 数目的对象,也就是__每毫秒标记 15000 个__左右</li> </ul> </li> <li>清除本身开销很小,但 Finalization 较耗时 (取决于对象的 finalizer)</li> </ul> <hr> <p>具体到 Mono 的实现中,有以下这些需要注意的地方:</p> <ul> <li>(Mono 实现) 无法精确地读取寄存器和栈,且无法区分一个给定值是指针还是标量,这会造成大块的内存无法正常回收,而且难以压缩空闲列表</li> <li>(Mono 实现) 碎片化会导致直接的新堆分配,即使空间仍充足(也就是说 Mono <strong>没有做 Copy Optimization</strong> 相关的 Defragmentation)</li> <li>(Mono 实现) Mono 的 Finalizer <strong>运行在独立的线程上</strong>,因此 GC.Collect() 和 obj.Dispose() 是需要线程同步的。</li> <li>(Mono 实现) 由于第一条,GC.Collect() 不会处理栈,寄存器,静态变量(这些东东被称为所谓的 <strong>&ldquo;Roots&rdquo;</strong>)</li> <li>(Mono 实现) GC 的开销与堆的尺寸是__正相关__的 (分配得越多,堆尺寸越大,新的分配和回收就会越慢)</li> </ul> <hr> <p>这里的信息来自<a class="link" href="http://www.hboehm.info/gc/04tutorial.pdf" target="_blank" rel="noopener" >The Boehm-Demers-Weiser Conservative Garbage Collector (Hans-J. Boehm, HP Labs)</a>,<a class="link" href="https://github.com/ivmai/bdwgc" target="_blank" rel="noopener" >GitHub (ivmai/bdwgc)</a> ,<a class="link" href="http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2310.pdf" target="_blank" rel="noopener" >Transparent Programmer-Directed Garbage Collection for C++ (2007)</a> 和 <a class="link" href="http://www.infoq.com/news/2011/01/SGen" target="_blank" rel="noopener" >SGen: Mono’s Generational Garbage Collector</a></p> <h3 id="实践中的-gc-控制手法">实践中的 GC 控制手法</h3> <p>从实践上看,与 GC 相关的控制手法主要是以下这些:</p> <ol> <li>避免无谓的反复分配,尤其是__隐含的每帧分配__ 典型的例子是在 Update() 函数里面拼接字符串</li> <li>在可能的时刻__主动触发 GC__,这些时刻包括: <ul> <li>“刚刚进入某张地图时”</li> <li>“刚刚打开某个(静态)界面时”</li> <li>“结束掉某一段剧情/新手引导时”</li> </ul> </li> <li>使用对象池__策略性地重用对象__ <ul> <li>把对象的引用归还到对象池,主动有计划地持有引用,而非交给 GC</li> <li>做好平衡和取舍(最小化分配/释放的行为,同时妥善考虑内存占用量的调整)</li> </ul> </li> <li>在 GC.Collect() 之前,确保__置空所有能被清理的对象__,以最大化 GC.Collect() 运行一次的性价比</li> </ol> <hr> <ul> <li>2 和 3 的意义在于,对于每次 GC 而言,如果没有需要释放的对象,速度会非常快。</li> <li>可以连续触发多帧的 GC ,就能在 Profiler 中看到,时间消耗的峰值就是第一次 GC。</li> <li>所以尽量手动 GC 的好处就是,会降低 GC 发生在你不期望的时间的几率,也能降低万一发生时的时间开销。</li> <li>考虑到很多 Unity 程序员之前有过丰富的 C++ 经验,对象池就不再展开细说了。</li> </ul> <h3 id="内存布局的效率改善-以对象为单位-vs-以类型为单位">内存布局的效率改善 (以对象为单位 vs. 以类型为单位)</h3> <p>除了对 GC 的行为有一定的理解,并加以适当的控制以外,内存方面还需要注意布局方面的因素:</p> <p>看下面的例子:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="k">struct</span> <span class="nc">Stuff</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">a</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">float</span> <span class="n">b</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span> <span class="n">c</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">string</span> <span class="n">leString</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">Stuff</span><span class="p">[]</span> <span class="n">arrayOfStuff</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>这是典型的__以对象为单位__组织数据。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c#" data-lang="c#"><span class="line"><span class="cl"> <span class="kt">int</span><span class="p">[]</span> <span class="n">As</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">float</span><span class="p">[]</span> <span class="n">Bs</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">bool</span><span class="p">[]</span> <span class="n">Cs</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">string</span><span class="p">[]</span> <span class="n">leStrings</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>这是典型的__以类型为单位__组织数据。</p> <hr> <p>按照 Unity 程序员 Marco Trivellato 同学的说法,后者对 GC 比前者友好,因为按照后者的方式组织,不同类型的数据被 GC 回收时仅需扫描自己那块;而前者(按照对象组织)被 GC 的时候,所有的数据都需要被扫描到,会花费更多的时间。</p> <h3 id="导致内存碎片化的各种常见点">导致内存碎片化的各种常见点</h3> <ul> <li>前面已经提到的,这里汇总一下 <ul> <li>foreach</li> <li>FindObject()</li> <li>LINQ</li> <li>ToString()</li> </ul> </li> <li>Unity 接口中一些导致零碎内存分配的常见点 <ul> <li><code>.tag</code></li> <li><code>GetComponents&lt;T&gt;</code> (这个据说还要调到 native code 里面去)</li> <li><code>Vector3[] Mesh.vertices</code></li> <li><code>Camera[] Camera.allCameras</code></li> </ul> </li> </ul> <h3 id="美术资源相关的运行时控制">美术资源相关的运行时控制</h3> <h4 id="unloadunusedassets-和-unloadasset">UnloadUnusedAssets() 和 UnloadAsset()</h4> <p>Resources.UnloadUnusedAssets() 的特点:</p> <ul> <li>会扫描所有的未引用资源</li> <li>发现时就会触发回收操作</li> <li>是一个异步操作</li> <li>在加载一个关卡后自动调用</li> </ul> <p>Resources.UnloadAsset() 的特点:</p> <ul> <li>由程序员主动调用</li> <li>Unity 扫描开销比前者低很多 (只考虑相关的依赖关系)</li> </ul> <p>结论:如有可能,尽可能地使用后者手动释放。</p> <h4 id="资源控制常识">资源控制常识</h4> <p>还有一些常识,可能不是很系统,这里也简单提一下:</p> <ul> <li>绝大部分 Mesh 是不需要 CPU 端的读写的,可以把 Read/Write 关掉 (少一份 copy)</li> <li>不要对 Mesh 做非标准的缩放 (少一份 copy)</li> <li>Instantiate() 内做了下面这些事 <ul> <li>克隆整个对象树 (GameObject Hierarchy)</li> <li>克隆它们的组件 (Components)</li> <li>复制它们的属性 (Properties)</li> <li>Awake() <ul> <li>清除各种状态</li> <li>内部状态缓存</li> <li>预计算</li> </ul> </li> <li>需要的话应用变换 (Apply Transform)</li> </ul> </li> </ul> <hr> <p>慢慢地引申到了图形方面,这方面实践中也有一些内容,考虑到篇幅,这一次就不展开了。代码相关的实践,这一次就先讨论这么多吧。</p> <hr> <h2 id="工程相关的实践">工程相关的实践</h2> <p>下面我们来简单聊一下工程方面的实践,这些实践我基本上都只是简单地提一下思路,仅供参考。</p> <h3 id="耗电发热问题改善">耗电发热问题改善</h3> <p>常见的手机发热问题根源有这些:</p> <ul> <li>后台运行多个任务导致CPU超载;</li> <li>系统I/O处理遇到瓶颈和阻塞;</li> <li>手机充电时导致过热;</li> <li>后台多个应用消耗一定的电量;</li> <li>手机硬件连接网络时电量损耗最多;</li> </ul> <p>降低发热可以做的事有以下这些:</p> <ul> <li>在特定的界面控制帧率,降低 CPU/GPU 的使用率</li> <li>检测后台应用并提示关闭,提示关闭 GPS 和 蓝牙 <ul> <li>或者提供一键关闭,游戏关闭或退到后台时再自动恢复)</li> </ul> </li> <li>亮度动态调整 <ul> <li>(甚至可考虑当前地图的光照风格)</li> </ul> </li> <li>提示关闭背景数据和关闭自动同步,退出时再自动恢复 <ul> <li>(但将无法及时接收到邮件)</li> </ul> </li> <li>IO 异步化,串行化,可等待化,可丢弃化,Throttling (流速控制)</li> </ul> <h3 id="新手引导防卡死">新手引导防卡死</h3> <p>正如我在 <a class="link" href="http://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue" target="_blank" rel="noopener" >“一个有趣的交互 bug ——兼谈游戏的引导系统”</a> 一文中提到的新手引导问题那样,卡新手是一类常见问题。对于这一类问题,除了把可能有冲突的系统及时修复以外,我们还应当采取一些防御性的设计,当玩家陷入卡死状态时,能借助这些机制跳出当前的卡死状态。</p> <p>由于现在的新手引导普遍傻瓜化,如果玩家停留在某个步骤超过 5 秒钟,我们就可以假设该玩家遇到状况了。这时我们可以检测玩家是否在连续 tap 屏幕,如果连续 tap 三次以上,可以弹出信息提示:“请长按屏幕 x 秒钟退出当前的引导” 如果玩家按提示操作,就 break 出当前的引导,视情况跳过或重新开始。</p> <h3 id="错误处理和异常捕获时机">错误处理和异常捕获时机</h3> <p>这一节只需讲一句话:<strong>关键逻辑路径上不要裸调</strong>。<strong>关键逻辑路径上不要裸调</strong>。<strong>关键逻辑路径上不要裸调</strong>。(是谁说重要的话要讲三遍来着?)</p> <p>不要依赖 Unity 对未捕获异常的宽容性。</p> <p>在游戏的关键逻辑路径上,如果裸调一个可能抛出异常的函数,就会冒着部分关键业务逻辑被跳过的风险。这种风险除了会造成可能的内部状态错误和运行不稳定以外,更有可能被破解者或熟练用户利用,达成各种你非常非常不希望见到的目的。</p> <h3 id="宕机信息的采集处理统计和反馈">宕机信息的采集,处理,统计和反馈</h3> <p>这里必须宣传一下,我司(西山居)质量中心部门近期出品了一个服务 <a class="link" href="http://www.crasheye.cn/" target="_blank" rel="noopener" >Crasheye</a>,可以帮助开发人员采集,分类和梳理各种宕机记录,给出项目稳定情况的分类趋势统计,并借助符号文件把堆栈转换为程序员可读的样式。第一次看到这个工具就我伙呆了(这个词过时得好快)。</p> <p>点<a class="link" href="http://www.crasheye.cn/project/81347d90/dashboard" target="_blank" rel="noopener" >这里</a>可以看到一个使用此产品来捕获宕机信息和统计的演示,简直华丽得不能直视。</p> <p>好了废话不多说了,我要跑去质量中心那边,请他们收下我的膝盖了。</p> <hr> <p>[2015-06-28] 补:</p> <p>本文中的代码都在<a class="link" href="https://github.com/SeaSunOpenSource/u3d_practice/tree/master/Assets" target="_blank" rel="noopener" >这里</a>,如有错误请不吝指出,更新和扩展都会出现在那里。</p> 2015.06 “千年故纸空读尽,恨把衣冠祭九州” - 小记《三国志11之血色衣冠》 https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/ Sun, 14 Jun 2015 21:48:00 +0000 https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/ <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/blood.jpg" width="549" height="369" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/blood_hu6543302d4407688f962ea997d91e0ecf_64210_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/blood_hu6543302d4407688f962ea997d91e0ecf_64210_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="blood" class="gallery-image" data-flex-grow="148" data-flex-basis="357px" ></p> <hr> <h1 id="千年故纸空读尽恨把衣冠祭九州---小记三国志11之血色衣冠">“千年故纸空读尽,恨把衣冠祭九州” - 小记《三国志11之血色衣冠》</h1> <p>三国志 11 是我眼中非常经典的一代,也是投入时间最多的一代。前段时间一个很偶然的机会,让我接触到了它的一个 MOD ——血色衣冠,当时就拜服于其浩大的工程和严谨的制作之下。</p> <p>这段时间玩了 50+ 小时的血色衣冠,寥寥几句介绍和心得,也算稍表对制作者的景仰之情。</p> <h2 id="介绍">介绍</h2> <pre><code>“《血色衣冠》是一款以三国志11为基础的MOD,以两千年华夏历史为背景,集合了从春秋到明末的中华英豪在同一个舞台上相互角逐,争夺最后的胜利。” ——游戏手册 </code></pre> <p>血色衣冠的最大特色是,上迄春秋战国,下至宋元明清(其中元和清在游戏中以异族的方式登场),在同一张地图内对整个华夏文明史的全景投射。如果你玩过任天堂明星大乱斗这类游戏,大概很容易明白我的意思,血色基本上可以认为是中国历史大乱斗。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/map.jpg" width="800" height="800" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/map_hu40e1032b05836941861606e1f8e63976_201310_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/map_hu40e1032b05836941861606e1f8e63976_201310_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="map" class="gallery-image" data-flex-grow="100" data-flex-basis="240px" ></p> <p>血色从 2008 年开始开发,到2014年,历时六年,四个大版本,一个血色主剧本,三个各具特色的挑战剧本。<code>“以通鉴、左传和二十四史为基础所撰写的十四万字人物列传。”(游戏手册语)</code>这个诚意已经超过了我所知的绝大多数国产游戏,考虑到这只是出于爱好者之手,我只能说收下我的膝盖了。</p> <h2 id="势力篇">势力篇</h2> <p>游戏里比较适合上手的正是历史上几个比较强盛的朝代——汉,唐,宋,明。其中除了宋稍弱(但也有岳飞,岳云,杨再兴,赵匡胤本身也是极强的)之外,汉,唐,明几个势力阵容都非常闪耀——汉有韩信,张良,卫青,霍去病;唐有李世民,李靖,李勣(他们仨的特点是统智双高,而且兵种适性上李靖似乎是综合最强);明有徐达,常遇春,朱棣,刘基,姚广孝,而且还有王守仁和张居正。总得来说,在俺眼中,这四大王朝的实力排序(对应的游戏难度从易到难)依次为: 明 &gt; 汉 &gt; 唐 &gt; 宋。考虑地图上的分布的话,明与汉地处边陲(明在东南方金陵,有长江天险可供利用,而汉居西南,易守难攻),而唐宋地处中原,皆为四战之地,与明汉相比难度增大不少。三国志11中的逐鹿中原,其实与围棋之道非常像,所谓“金角银边草肚皮”,边角意味着稳定的后方和相对清晰的战略规划。</p> <p>血色提供有一个挑战剧本叫做《四朝争统》,就是汉唐宋明这四大最强的王朝分居四方,争夺天下。这个剧本比之原作魏蜀吴三分天下,更增平衡和乐趣。必玩剧本。</p> <p>说到剧本,另外两个也顺便提一下吧。《五胡乱华》和《三军夺帅》,挑战难度更高,前者是司马氏以长江天险抵御北方五胡的情节(这个剧本难在孙武,白起等武将被设定为五胡阵营,而且为表现当时中原的被动局面,初始资源极度短缺),后者是幻想剧本,假设韩信成为齐王后,脱离刘邦阵营,借楚汉相争渔翁得利的情节。为了进一步增加挑战的难度和趣味,设定为韩信仅占临淄一城(科技大大落后于其它势力),并增加了北方莫顿单于的入侵和蒙恬的增援。</p> <p>三军夺帅的地图:</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/san.jpg" width="297" height="253" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/san_hue6c42944ab805123301a36b8de7dec81_37910_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/san_hue6c42944ab805123301a36b8de7dec81_37910_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="san" class="gallery-image" data-flex-grow="117" data-flex-basis="281px" ></p> <h2 id="人物篇">人物篇</h2> <p>中华历史上,两千年来将星璀璨,文韬武略智勇双全者层出不穷,以至于为了显示出与孙武韩信等绝顶人物在属性上的区分性,很多历史上风流一时的人物,数值也被压得偏低(如卧薪尝胆,灭吴称霸的越王勾践,统率也只有81,智武分别只有75和70,作为春秋最后一个霸主,政治更是只有63)。</p> <p>说到绝顶人物,我们先从五兵种的“兵圣”说起吧(按照血色的设定,所有人物中,每个兵种,历史上只有五个“神”,一个“圣”)。这五个“兵圣”在其对应的兵种中,均为史上最强的角色。按武侠的说法,称之为“五绝”并不为过。</p> <p>这五人分别是__“枪圣”白起__,<strong>“戟圣”韩信</strong>,<strong>“弩圣”王翦</strong>,<strong>“骑圣”霍去病</strong>,<strong>“工圣”戚继光</strong>,(以及水军之圣刘裕,不过水军的存在感很低,就不多说了)。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/p5.jpg" width="1300" height="307" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/p5_huf3b527722ab2471f0440e0abaaaa224a_91553_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/p5_huf3b527722ab2471f0440e0abaaaa224a_91553_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p5" class="gallery-image" data-flex-grow="423" data-flex-basis="1016px" ></p> <p>这里面,俺觉得白起,韩信和霍去病没什么疑问,不过王翦,戚继光和刘裕感觉稍有勉强,他们三人更多是超强的统率能力而非具体兵种的战术能力,不过我想血色也是优先考虑“圣”的称号应被顶级战神而非战术专家所持有罢。</p> <p>值得注意的是,“五绝”中的三个都是先秦和楚汉时的名将,而且史上统率最高的孙武,吴起,史上武力最强的项羽,史上智力最高的张良,也都是这一时期的人物(史上政治最高的管仲、商鞅,史上魅力最高的老聃、孔丘,则年代稍早,但也是先秦时期),可见先秦时期不仅在文化和哲学上是核心的蜕变时期,而且在政治和军事上也是巅峰的年代。(注意咱们熟悉的三国时的刘关张赵诸葛,周瑜陆逊,曹操司马懿,跟他们相比差距还是很明显的)</p> <p>上面提到的这些人物,专有特技都很独特很霸气:“兵圣”(孙武)“兵神”(吴起)“战神”(白起)“兵仙”(韩信)“霸王”(项羽),其中前两个偏防御性优势,后三个偏攻击性优势,都是全兵种的。其他的各种枪神,戟神,弓神,骑神,工神相对就稍偏科一点,只是对应兵种很强。</p> <p>除了这些神一样的存在,还有两个人物是不容忽视的,用好了甚至比绝顶人物更有优势——他们就是__李靖(唐)<strong>和__岳飞(宋)</strong>。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/p2.jpg" width="532" height="319" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/p2_hu51e9d833f4ea028628afd416acfa93d3_165464_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/p2_hu51e9d833f4ea028628afd416acfa93d3_165464_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="p2" class="gallery-image" data-flex-grow="166" data-flex-basis="400px" ></p> <p>李靖除了统智双高外,优势主要是兵种的全面性(3“神”2“极”)——他的弩兵,骑兵和水军都是“神”,而枪兵和戟兵则是“极”(对应原著的S级) ,这样的全兵种适性,再配合他的高统高智(112/95)和攻击性十足的“兵仙”特技,足以让李靖成为血色__综合素质第一人__。</p> <p>而岳飞是血色中唯一一个统武过百的角色,兵种全面性也不弱(2“神”3“极”)顺便说一句,岳飞的智力和政治(86/62)设定明显偏低了,知乎上曾有<a class="link" href="http://www.zhihu.com/question/20993491/answer/20026544" target="_blank" rel="noopener" >详尽的分析</a>告诉我们岳飞的智谋和政治水准。</p> <pre><code>“岳飞的情商和政治水平是当时大臣中最高的,非常善于协调各方面关系,包容性远比今人凭传统印象想象的高,与同僚的关系也是所有大臣中最好的。换句话说,历史上真实的岳飞是文武双全,战功卓著,军事才华突出,为人刚正,私德近乎完美;但绝不是“不容小人”,更不狂傲,政治敏感度也不低。相反他政治眼光远大,政治嗅觉也极为灵敏;善于协调各方面关系;独立领军后更是温和、恭谨而且谦逊低调到了几乎有些自我压抑的程度,所谓的“循循如书生”“时人至今号为贤将”。” - “真实的岳飞是什么情况?” - 北溟客的回答 - 知乎 </code></pre> <p>还有一些一时瑜亮,在经历或属性上也相似或相匹配的角色(或根本就是好基友),玩起来也别有兴味。如高统的卫青霍去病,李靖李勣,徐达常遇春,高武的岳飞岳云,杨业杨再兴,高智的范蠡文种,孙武孙膑,张良陈平,刘基姚广孝,张仪苏秦,孔明周瑜,包拯狄仁杰(-_-!),高政的商鞅韩非,管仲乐毅,高魅的老聃孔丘,也都值得一试。</p> <p>有的角色属性和适性均不突出,但有很有趣或很实用的特技,如俞大猷/廉颇/段纪明/李存勖的百战(战神们的好搭档),徐达等人的攻心(吸收士兵),李广等人的飞将/遁走(于陆上不考虑ZOC),朱文正等人的坚城(防御战的必备),宇文泰等人的激励(战法有机会再行动一回合),拓跋珪等人的强行(增加全兵种的移动),萧何等人的后勤(大军出征必备),诸葛亮等人的王佐(全内政提高效率)</p> <h2 id="战略篇">战略篇</h2> <p>这个游戏最大的乐趣就是尝试不同的战略和战术,一般的战术我就不多说了,论坛贴吧上有很多了,就简单说一下血色的特色吧。</p> <p>血色的大地图上,整体的战略重心是偏北的。主要是长安-洛阳-开封一线的秦(秦任好) / 唐(李渊) / 东汉(刘秀) / 宋(赵匡胤)之间的争夺。其中秦稍弱,通常会被先灭掉,剩下几个常年征战不休。李唐虽然武将很强,但由于与秦的直接对抗,难以发展,相对贫弱,在东面和南面常年处于守势;刘秀本人很强,但麾下武将的素质一般,由于周边较弱,整体扩张快;而赵宋拥有地形优势,北临黄河,对黄河以北攻守很灵活,东南西三面都偏弱,可安心发展,随时骚扰李唐和刘秀。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s1.jpg" width="947" height="507" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s1_hu1b9f2f52fb927ffd87d48aeb52a2f06a_89949_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s1_hu1b9f2f52fb927ffd87d48aeb52a2f06a_89949_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="s1" class="gallery-image" data-flex-grow="186" data-flex-basis="448px" ></p> <p>(图中的秦国在李唐的巨大压力下,开局仅过三年,已近灭国的边缘)</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s2.jpg" width="1068" height="349" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s2_hu1be6dc99809cdeb3969b53a66e471e1d_84755_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s2_hu1be6dc99809cdeb3969b53a66e471e1d_84755_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="s2" class="gallery-image" data-flex-grow="306" data-flex-basis="734px" ></p> <p>(图中岳飞领军的赵宋越过黄河,展开对晋国的包围)</p> <p>正如势力篇中已提到的那样,西南的刘邦(汉)和东南的朱元璋(明)可以很从容地发展,兵精粮足,逐个击破。</p> <p>先看刘邦,西汉的优先战略目标是宇文泰和蜀汉之一,取蜀汉则可得到刘关张赵和孔明,但偏守势,不易向江陵发展。取宇文泰则发展更快,但有被多线夹击的风险,好在刘邦兵多将广,可从容调度。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s3.jpg" width="912" height="401" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s3_hu13961582111c60ffe1d004cf2dbe4d01_85229_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s3_hu13961582111c60ffe1d004cf2dbe4d01_85229_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="s3" class="gallery-image" data-flex-grow="227" data-flex-basis="545px" ></p> <p>(此图中刘邦率卫青霍去病出征蜀汉,蜀汉精英尽出,刘关张赵,魏延黄忠,马超姜维,全部上阵,殊死一搏)</p> <p>再看朱元璋,明是大王朝中战略最清晰的势力。首要目标平定江东三郡,顺便获得兵圣孙武。此时一般战略是由庐江向中原发展,我玩的时候选择了从长江逆流而上,先取柴桑,后定荆州,长江以南很快就能全部平定,比挺进中原要顺利得多。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s4.jpg" width="1127" height="525" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s4_hue11ef6eca745222a0ff8d7c3cd4f9cf3_95615_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s4_hue11ef6eca745222a0ff8d7c3cd4f9cf3_95615_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="s4" class="gallery-image" data-flex-grow="214" data-flex-basis="515px" ></p> <p>(此图为在长江中逆流而上,绕过庐江,千里奔袭并速取柴桑的战役。图中四人组里,徐达常遇春二猛将,戚继光王守仁俩工神,堪称大明的绝妙搭配,在平定江东前就助我取得了柴桑。)</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s5.jpg" width="1134" height="496" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s5_huc90346b14fbf8e3661f7ead476c44ac9_96460_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/s5_huc90346b14fbf8e3661f7ead476c44ac9_96460_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="s5" class="gallery-image" data-flex-grow="228" data-flex-basis="548px" ></p> <p>(作为对照,这是在另一个存档里电脑控制的大明强攻庐江,遇到的司马懿和刘裕的强力阻击和杨行密的拼死抵抗)</p> <p>原三国中的曹魏和蜀汉在强敌环伺之下,都难有大的作为,只有得到了荆南四郡的孙坚,没有后患,且倚靠长江,可以持久。</p> <p><img src="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/m1.jpg" width="150" height="105" srcset="https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/m1_hu969826540aa060f2dc55d498234321d9_15576_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-sango-mod-blood-civil/images/m1_hu969826540aa060f2dc55d498234321d9_15576_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="m1" class="gallery-image" data-flex-grow="142" data-flex-basis="342px" ></p> <p>(如图中,开局三年后,猛将如云的蜀汉已被刘邦所灭,孙坚却径取荆南四郡,已率部北上争霸)</p> <h2 id="评价">评价</h2> <p>基本分:80</p> <ul> <li> <p>(+3) 多个势力和多个剧本,使得与一般的 MOD 相比,此 MOD 的可重复玩的价值大大提高。</p> </li> <li> <p>(+3) 游戏的角色设定非常严谨,数值的真实性很强。</p> </li> <li> <p>(+2) 特技和兵种适性贴合人物的特质,战斗变化在原作的基础上更为丰富。</p> </li> <li> <p>(-3) 由于原三国志11 的 AI 限制,此 MOD 的 AI 做出的决策仍不够明智,变化也不够多,后期乏味程度没啥改观。</p> </li> <li> <p>(-2) 地图的平衡性或可进一步改善,避免头重脚轻,通过一些额外的随机性来避免重复的战役进程。</p> </li> </ul> <p>修正分:83</p> <p>我个人非常喜欢这款基于三国志 11 的 MOD,虽然 83 分似乎不高,但考虑到爱好者个人的无偿作品能做到这个程度,作为一个开发者和玩家,感情上俺毫不犹豫地给此 MOD 满分。</p> 2015.06 《中国文化概论》第十一周作业 - 诗性文体书写实践 https://gulu-dev.com/post/2015-06-14-chinese-culture-homework-2/ Sun, 14 Jun 2015 12:54:00 +0000 https://gulu-dev.com/post/2015-06-14-chinese-culture-homework-2/ <p><img src="https://gulu-dev.com/post/2015-06-14-chinese-culture-homework-2/2015-06-14-chinese-landscape.jpg" width="600" height="479" srcset="https://gulu-dev.com/post/2015-06-14-chinese-culture-homework-2/2015-06-14-chinese-landscape_huc7abd41c9c29c2303bc5b60338291b5e_121242_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-06-14-chinese-culture-homework-2/2015-06-14-chinese-landscape_huc7abd41c9c29c2303bc5b60338291b5e_121242_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="culture" class="gallery-image" data-flex-grow="125" data-flex-basis="300px" ></p> <hr> <h2 id="第十一周作业-诗性文体书写实践">第十一周作业 诗性文体书写实践</h2> <p>这是我在《中国文化概论》这门课的最后一次作业。第一次在慕课的平台上学习,历时三个月左右,结课的考试已经结束,等拿到证书后,我会再做一下完整的回顾。</p> <p>现在想想,当年因为贪玩,高考分数不高,无缘得入国内一流高校学习 (其实按我那时候的尿性,去了也学不到啥),现在有机会得听武汉大学文学院的李建中教授的人文课程,也算是很幸运的了。三两年间,随着在线教育平台的普及,高考分数和专业方向已不再是求学的门槛。只要感兴趣,愿意下工夫,国内和国外的名校和名师都不再是传说中的存在。对于一心向学的同学来说,这确实是一个黄金的时代。</p> <hr> <h3 id="题目">题目</h3> <p><strong>诗性文体书写实践:请运用你所知道的任何一种中国古代文体(如骈文、赋体、诗体、词体等),写下本课程学习中的感悟。</strong></p> <p>本就对自己的文笔没啥信心,看到这个题目需要用古代文体来写作,彻底慌了……作为一个程序员,毕业后在工程实践领域一待就是十年,大脑早已经被代码化了。以前曾看过的诗词歌赋,能想到的也只是只言片语,勉强写两句吧,怕搞出“趵突泉,泉趵突,三个眼子一般粗”这种句子来。</p> <p>一直以来,不想让自己的头脑里充斥着工程和逻辑概念,希望能从传统文化中汲取养分。往实务中说,可以让自己的设计融合科技与人文,特立脱俗,清新雅致,蕴涵内在的人文气息;往远了说,甚至于能内化为自己内心力量的一部分。就可以避免无意中被物质驱动的生活所奴役,“君子役物,小人役於物”嘛。</p> <p>学了这门课之后,对传统文化有了框架性的了解,就像一座山,之前看过去雄奇俊伟,云雾缭绕,现在手中多了一份游历指南,心中也更明白了自己想要的是什么。从这个角度讲,于我而言,我希望在中国文化概论这门课中的修习,只是自己了解传统文化的一个起点。我买了老师提到的《轴心时代》一书,还有对汤因比的著作的一些解释,打算研读一下,慢慢地提高认识。</p> <h3 id="念奴娇">念奴娇</h3> <pre><code>念奴娇 · 学文化概论杂感 摩登时代,互联网,人文已然湮灭? 信息革命,弹指间,科技改变生活。 维基百科,苍茫浩瀚,转瞬呈指尖。 圣贤若在,当发千年之叹。 我欲诚心向学,奈根基浅薄。 网络拜师,键盘论道, 良师旁,或得略窥门径。 而立之年,养浩然之气,蔽而新成。 科技人文,矢志融会贯通。 </code></pre> <hr> <ul> <li>写完后看,发现“矢志”一词用得重了,有种“老都老了,还发少年狂”的感觉,可以“惟愿”二字替之。</li> <li>“蔽而新成” 四字,引自“夫唯不盈,故能蔽而新成”一句。修行道德的人,不求圆满。正因明白“月盈则亏”,所以永远抱朴守拙而充满生机,在旧的框架中不断融入新的认识。</li> <li>这是俺人生第一次填词,不仅平仄音韵完全没有考虑,而且通篇白话词汇,脱不了打油诗的味道,哈哈,管它呢。</li> </ul> 2015.05 入职后发现项目组代码异常混乱,是去是留? https://gulu-dev.com/post/2015-05-09-legacy-code/ Sat, 09 May 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-05-09-legacy-code/ <img src="proxy.php?url=https://gulu-dev.com/post/2015-05-09-legacy-code/title.jpg" alt="Featured image of post 2015.05 入职后发现项目组代码异常混乱,是去是留?" /><p>此文是俺在知乎上对这个问题的回复:</p> <p>知乎链接:<a class="link" href="http://www.zhihu.com/question/29941041" target="_blank" rel="noopener" >入职后发现项目组代码异常混乱,是去是留?</a></p> <hr> <p>楼上的@蔡磊兄分析得很清楚。对重写代码可能带来的各种风险,俺很认同他的观点,也就不再多嘴了。然而,蔡磊兄整体的论调呈相对消极的姿态,从不同角度得出这一个结论——<strong>重写代码是不可取的</strong>。这里俺更愿意换个角度,谈谈积极的一面,也发出一点不同的声音。</p> <hr> <h2 id="再谈重写">再谈重写</h2> <p>在展开讨论之前,先抛出结论:对于商业价值已在明显缩水中的项目,大动干戈意义确实不大;而对于一个急速成长或稳定运行中的项目而言,有计划有步骤地整理和翻新,是非常必要而且很有讲究的。各种在初期不会显山露水的局限和瓶颈,会随着项目规模的成长不断凸显,此时,若不能水涨船高,通过积极手段逐步改良其内部实现,随着贪一时之快的补丁越积越多,系统将日趋僵化,后果自不必说。</p> <p>对于重写本身,我们也大可不必畏之如虎。虽说正如 Joel 所说,“废弃现存可运作的代码跑去完全重写”几乎总是个错误的决定,但“中小型系统或组件的受控重写”却是很常见的改良系统的手段。比如这个:</p> <p><a class="link" href="http://matt-welsh.blogspot.jp/2013/08/rewriting-large-production-system-in-go.html" target="_blank" rel="noopener" >Rewriting a large production system in Go</a></p> <p>在这篇文章里,Matt Welsh 同学介绍了自己是__如何使用 Go 来完全重写一个 Google 的生产环境下的系统__的。推荐感兴趣的同学前往一读。此外我读过一篇讲 facebook 是如何决定和实施一个服务的重写的,还有一篇是 GitHub 如何保证重写的新系统和老系统之间无缝平滑过渡的,这两篇都蛮有趣,可现在一时之间找不到链接了,不过没关系,后面我会介绍一些我仍记得的思路。至于微软是如何(反复)重写 Windows 组件的大家可以到 <a class="link" href="http://blogs.msdn.com/b/oldnewthing/" target="_blank" rel="noopener" >The Old New Thing</a> 上去看,老的例子有 GDI, Direct3D, Visual C++, MSE,新的例子则有 Edge (IE)。</p> <p>关于从 IE 到 Edge 还有一个有趣的曲折——其实自从 IE8 之后微软就动了重写的心思了,在 IE9 中,微软尝试了所谓的“完全重写 (Rewrote From Scratch)”(可以参考这个链接:<a class="link" href="http://www.geekwire.com/2011/geeks-guide-to-ie9/" target="_blank" rel="noopener" >Inside IE9: How Microsoft rewrote its browser from scratch</a>) 这个版本里把原来的 Javascript 解释器替换为一个新写的能生成本地的 native code 的执行引擎,把图形系统内的大多数渲染工作挪到了 GPU 上,还有一个全新的布局引擎 (With IE9, we rewrote our layout engine from scratch) <strong>但是</strong>(这里才是重点),就连这种程度的重写也还是不够的,微软后来才意识到,人们的关注点变了,互联网的焦点已经在向移动端转移,于是才有了现在更彻底的重写——Windows Edge(原来的 code name 叫 Project Spartan)</p> <p>好了,在了解到在有重大商业影响的项目中,重写也不是什么偶然的事情之后,我们就会明白,已有的 Legacy Code 并非刻在石头上的神谕,尝试着去逐步改进自己维护的代码,重点不在于“能不能”,而在于“<strong>应不应该这么做</strong>” ,“<strong>如果需要,应该做到什么程度?</strong>”,还有“<strong>实践中,如何以受控的方式去实施</strong>”,这样才能在避免失控的悲剧的同时,周期性地抛下越来越沉重的负担,让系统得以轻快而长久地健康运行。</p> <hr> <h2 id="legacy-code-在不同的人眼里长什么样">Legacy Code 在不同的人眼里长什么样?</h2> <p>在新来的萌宠小师妹眼中,现有系统是这样的:</p> <p><img src="https://gulu-dev.com/post/2015-05-09-legacy-code/new_dev.png" width="811" height="476" srcset="https://gulu-dev.com/post/2015-05-09-legacy-code/new_dev_hu50d559107edd460506e9bb7a0a284205_84546_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-09-legacy-code/new_dev_hu50d559107edd460506e9bb7a0a284205_84546_1024x0_resize_box_3.png 1024w" loading="lazy" alt="new_dev" class="gallery-image" data-flex-grow="170" data-flex-basis="408px" ></p> <p>在曾经沧海的大师兄眼中,现有系统是这样的:</p> <p><img src="https://gulu-dev.com/post/2015-05-09-legacy-code/senior_dev.jpg" width="816" height="479" srcset="https://gulu-dev.com/post/2015-05-09-legacy-code/senior_dev_huf92a1837d7fe18e78b7f1bcc61a28686_60702_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-05-09-legacy-code/senior_dev_huf92a1837d7fe18e78b7f1bcc61a28686_60702_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="senior_dev" class="gallery-image" data-flex-grow="170" data-flex-basis="408px" ></p> <p>在新加入的同事眼中,大概还能分得出的几坨貌似解耦的模块,(由于各种不足为外人道的潜规则)在老同事眼里,其实跟一整坨也没什么区别。不同的是,老同事对系统的全貌和脉络,至少还有逻辑上的概念,知道整个系统的重心和支撑点在哪儿,知道那茂密毛发下面隐藏着什么(……)。</p> <p>所以,在你感觉眼前的代码比较混乱的时候,想想这头牛吧。虽说不管毛长毛短,能挤奶的就是好牛,可为了让自己以后能活得轻松点,给它洗洗澡剪剪毛啥的还是值得考虑的。</p> <hr> <h2 id="标准的受控重写-managed-rewrite-应该长什么样">标准的受控重写 (Managed Rewrite) 应该长什么样?</h2> <p><img src="https://gulu-dev.com/post/2015-05-09-legacy-code/a_typical_process.png" width="813" height="473" srcset="https://gulu-dev.com/post/2015-05-09-legacy-code/a_typical_process_huc8f8b9b79280486cbfecb0b2d0e90287_98344_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-09-legacy-code/a_typical_process_huc8f8b9b79280486cbfecb0b2d0e90287_98344_1024x0_resize_box_3.png 1024w" loading="lazy" alt="a_typical_process" class="gallery-image" data-flex-grow="171" data-flex-basis="412px" ></p> <p>当你决定撸起袖子大干一场的时候,如果没有上图这样的详细计划和分步的路线图,而只是盲人摸羊,走到哪里算哪里的话,会很容易掉到坑里的,达到预期的可能性也会大大降低。请注意,这张图最重要的价值在于,它把一个风险很大的单步决策__拍碎成了许多细碎的小步骤__,每个步骤彼此独立,需要承担的风险或重要性用颜色表示。这样一方面清晰地知道自己的目标和进展,不会迷失方向;另一方面也随时保留可以撤销至某个安全点上的能力。</p> <h2 id="进化-evolution而非革命-revolution">进化 (Evolution),而非革命 (Revolution)</h2> <p><img src="https://gulu-dev.com/post/2015-05-09-legacy-code/evolution.png" width="809" height="471" srcset="https://gulu-dev.com/post/2015-05-09-legacy-code/evolution_hu7d319fb52861b1817dec50828f30e647_9345_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-09-legacy-code/evolution_hu7d319fb52861b1817dec50828f30e647_9345_1024x0_resize_box_3.png 1024w" loading="lazy" alt="evolution" class="gallery-image" data-flex-grow="171" data-flex-basis="412px" ></p> <p>这与上一张图表达的实际上是一个意思。正如在做代码重构时的策略那样,我们在战略上也把大的目标切成小块,始终谨慎地保持小步前进,始终避免做过大的决定,始终把风险控制在能接受的范围内。</p> <h2 id="平行实现-parallel-implementations">平行实现 (Parallel Implementations)</h2> <p>__平行实现__是完全重写某个服务的重要手段,关于这个主题 John Carmack 曾写过一篇非常精彩的文章 <strong>&ldquo;Parallel Implementations&rdquo;</strong>,现在由于 altdevblogaday 这个网站关闭链接已经失效了,不过大家可以在 <a class="link" href="https://archive.org" target="_blank" rel="noopener" >Internet Archive</a> 这个网站上找回<a class="link" href="https://web.archive.org/web/20120702214656/http://www.altdevblogaday.com/2011/11/22/parallel-implementations" target="_blank" rel="noopener" >此文章在 2012 年时的快照</a>。此文非常精彩,强烈推荐。</p> <p>此文是这样开头的:I used to <strong>Code Fearlessly</strong> all the time, <strong>tearing up everything</strong>(!!!) whenever I had a thought about a better way of doing something. There was even a bit of pride there — <strong>“I’m not afraid to suffer consequences in the quest to Do The Right Thing!”</strong> 三个黑色加亮部分体现了卡马克同学一贯的价值观,俺写到这里心里默默为卡神点了三个赞。</p> <p>整个文章的精华在这一段:</p> <pre><code>What I try to do nowadays is to implement new ideas in parallel with the old ones, rather than mutating the existing code. This allows easy and honest comparison between them, and makes it trivial to go back to the old reliable path when the spiffy new one starts showing flaws. The difference between changing a console variable to get a different behavior versus running an old exe, let alone reverting code changes and rebuilding, is significant. </code></pre> <p>还有这一段:</p> <pre><code>There are two general classes of parallel implementations I work with: The reference implementation, which is much smaller and simpler, but will be maintained continuously, and the experimental implementation, where you expect one version to “win” and consign the other implementation to source control in a couple weeks after you have some confidence that it is both fully functional and a real improvement. </code></pre> <p>文章的最后,卡神写到:</p> <pre><code>Every single time I have undertaken a parallel implementation approach, I have come away feeling that it was beneficial, and I now tend to code in a style that favors it. Highly recommended. </code></pre> <p>正是因为这几段都很精彩,所以原封不动摘录于此。(其实要不是篇幅所限,真想干脆全文摘录了) 平行实现的细节我就不展开讲了,大家直接看原文好了。</p> <h2 id="幽灵替补-ghost-alternative-柔性服务和灰度发布">幽灵替补 (Ghost Alternative) ,柔性服务和灰度发布</h2> <p>“幽灵替补”是我为前文提到的一种做法随便起了个名字,方便记忆。前文提到,在一篇讲 GitHub 的某服务的重写过程中,他们把该服务的__新实现__挂在__老实现__上一起跑(正如附身的幽灵),当有新的请求过来时,新老两套系统同时开始处理,但最后的输出仍采用老系统的结果,新的系统输出结果被记录下来,与老系统的结果比对。千万次运行下来后,新系统得到了足够的考验,错误率低到一个程度后,再淘汰掉老系统。</p> <p>写完了才发现<a class="link" href="http://en.wikipedia.org/wiki/Parallel_adoption" target="_blank" rel="noopener" >这里(Parallel Adoption)</a>有 wikipedia 上对此方法的综述,也可供参考。</p> <p>这里是一个典型的例子:</p> <p><img src="https://gulu-dev.com/post/2015-05-09-legacy-code/ME_process_data.png" width="786" height="1033" srcset="https://gulu-dev.com/post/2015-05-09-legacy-code/ME_process_data_hue8bdffecf4e8cb8ae62dd9efd2ffcb66_111779_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-09-legacy-code/ME_process_data_hue8bdffecf4e8cb8ae62dd9efd2ffcb66_111779_1024x0_resize_box_3.png 1024w" loading="lazy" alt="ME_process_data" class="gallery-image" data-flex-grow="76" data-flex-basis="182px" ></p> <p>至于最近说的比较多的柔性服务和灰度发布,可以参考这两篇:</p> <ul> <li><a class="link" href="http://mp.weixin.qq.com/s?__biz=MzAwMTM5MDAyMw==&amp;mid=204182743&amp;idx=4&amp;sn=1ccc18de1756b7aa86b7e2f2c90868a7&amp;3rd=MzA3MDU4NTYzMw==&amp;scene=6#rd" target="_blank" rel="noopener" >腾讯大讲堂:发10亿个红包,微信为啥没崩溃?</a></li> <li><a class="link" href="http://mp.weixin.qq.com/s?__biz=MzA4MjU4NjMwNg==&amp;mid=202784602&amp;idx=2&amp;sn=9d5060aed944323b5d2d8d08a8c5c22f&amp;3rd=MzA3MDU4NTYzMw==&amp;scene=6#rd" target="_blank" rel="noopener" >微信技术总监周颢:一亿用户背后的架构秘密</a></li> </ul> <p>这些技术,都可以帮助我们实现新老系统之间顺利的迁移和过渡工作。</p> <hr> <p>总得来说,对系统的维护者来说,<strong>一味地积极重构,和一味的消极补丁,都是不可取的</strong>。有经验的开发者,会更准确地评估权衡改动带来的风险和工作量。如同我在“<a class="link" href="http://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue" target="_blank" rel="noopener" >一个有趣的交互 bug ——兼谈游戏的引导系统</a>”一文中提到的那样,大部分问题都可以由浅至深地分析出多个不同的解决方案,以便于在不同的情形下去取舍和平衡。</p> <hr> <p>最后这句话,与大家共勉吧 :)</p> <p><img src="https://gulu-dev.com/post/2015-05-09-legacy-code/improve_work.png" width="809" height="473" srcset="https://gulu-dev.com/post/2015-05-09-legacy-code/improve_work_hue40fc9f6082d2cc7d97dfeaacbe71800_10715_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-09-legacy-code/improve_work_hue40fc9f6082d2cc7d97dfeaacbe71800_10715_1024x0_resize_box_3.png 1024w" loading="lazy" alt="improve_work" class="gallery-image" data-flex-grow="171" data-flex-basis="410px" ></p> 2015.05 利用 AppVeyor 实现 GitHub 托管项目的自动化集成 https://gulu-dev.com/post/2015-05-01-appveyor-ci/ Fri, 01 May 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-05-01-appveyor-ci/ <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/drawing-cd-header.png" width="600" height="290" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/drawing-cd-header_hue5eb4234bc56bafc1807af0c87c67a1d_49737_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/drawing-cd-header_hue5eb4234bc56bafc1807af0c87c67a1d_49737_1024x0_resize_box_3.png 1024w" loading="lazy" alt="title" class="gallery-image" data-flex-grow="206" data-flex-basis="496px" ></p> <hr> <p>今天拿<a class="link" href="https://github.com/SeaSunOpenSource/usmooth" target="_blank" rel="noopener" >手头一个 GitHub 项目</a>实验了一下在线的集成服务,前后试用了 <a class="link" href="https://travis-ci.org/" target="_blank" rel="noopener" >TravisCI</a>, <a class="link" href="https://circleci.com/" target="_blank" rel="noopener" >CircleCI</a> 和 <a class="link" href="http://www.appveyor.com/" target="_blank" rel="noopener" >AppVeyor</a>。由于测试工程内包含了一些使用了 WPF 的 C# 代码,前面两个跑在 Linux/Mono 上不是很友好,而 AppVeyor 的配置非常顺利,与 GitHub 的互操作也没有任何问题,所以完成之后记录一下备忘。</p> <h2 id="基本流程">基本流程</h2> <p>首先将 AppVeyor 跟 GitHub 绑定后,就可以点击项目主页上的“ + New Project ”添加工程了。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/new_project.png" width="672" height="237" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/new_project_huf071558c4de1e9e84a094f870d8c0045_16145_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/new_project_huf071558c4de1e9e84a094f870d8c0045_16145_1024x0_resize_box_3.png 1024w" loading="lazy" alt="new_project" class="gallery-image" data-flex-grow="283" data-flex-basis="680px" ></p> <p>添加工程之后,需要到 Settings 里面做一些对应的配置。这里择要说一下吧。</p> <hr> <p>General 一栏中,有一个变量是生成的版本字符串,通常在后面打包时会用到。{build} 是一个自增长的序列号。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_01_build_version_format.png" width="448" height="73" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_01_build_version_format_hu7d15ba4e25feea06dfa31d449952d4a1_2852_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_01_build_version_format_hu7d15ba4e25feea06dfa31d449952d4a1_2852_1024x0_resize_box_3.png 1024w" loading="lazy" alt="cfg01" class="gallery-image" data-flex-grow="613" data-flex-basis="1472px" ></p> <p>Environment 一栏中,比较重要的是 Init script 和 Install script 的区别——简单说,前者发生在 <code>git clone</code> 和 <code>git checkout</code> 前,后者发生在它们之后。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_02_install_script.png" width="641" height="229" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_02_install_script_hu32363a7093ca3a0060ed2729b962aaa6_8682_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_02_install_script_hu32363a7093ca3a0060ed2729b962aaa6_8682_1024x0_resize_box_3.png 1024w" loading="lazy" alt="cfg02" class="gallery-image" data-flex-grow="279" data-flex-basis="671px" ></p> <p>如果像测试工程那样在库中包含 submodule,就需要在 Install script 内做 <code>git submodule update</code> (如上图)</p> <p>Build 一栏中的各种选项应该是不需要解释,程序员都很熟悉了。</p> <p>Artifacts 一栏中,如果指定的是目录, AppVeyor 会自动打成 zip 包,包名就是 <code>Deployment Name</code>,在这里可以用上前面的版本字符串。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_03_artifacts.png" width="692" height="182" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_03_artifacts_huc275155f56940849398dd0d251d861cb_10943_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/config_03_artifacts_huc275155f56940849398dd0d251d861cb_10943_1024x0_resize_box_3.png 1024w" loading="lazy" alt="cfg03" class="gallery-image" data-flex-grow="380" data-flex-basis="912px" ></p> <p>接下来是最后也是最重要的一栏 —— Deployment。 如果前面的步骤都很顺利,这一步就会把生成并打包好的文件发布出去。由于我的测试项目在 GitHub 上,这里我就选了 <strong>GitHub Releases</strong> (需要填入 <code>GitHub authentication token</code>)。注意这里需要明确指出待发布的 Artifacts 列表。</p> <p>这里有一项是 <code>Draft Release</code>(以草稿方式发布)比较有用,发布的内容只有当手动确认后才会公开出现在 GitHub 项目的 Releases 栏内,以免造成误发布。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy.png" width="488" height="250" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_hu6ee7cc1dbb90ea3b3365607e18695396_14100_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_hu6ee7cc1dbb90ea3b3365607e18695396_14100_1024x0_resize_box_3.png 1024w" loading="lazy" alt="deploy03" class="gallery-image" data-flex-grow="195" data-flex-basis="468px" ></p> <p>以 draft 方式发布在 GitHub 上后效果如上图。</p> <hr> <p>好了,如果一切顺利的话,至此 AppVeyor 已完成了 <strong>从获取代码,编译,打包,部署到 GitHub ,并以 GitHub Release 方式发布</strong> 的整个流程,还算简单吧。注意,在整个过程中,无需自己手写一句 git, make, msbuild, zip, copy 等各种内部和外部的命令,甚至连 solution file (*.sln) 都不需要手动指定,俺觉得总体上还是非常简洁的。</p> <hr> <h2 id="状态显示status-badge">状态显示(Status Badge)</h2> <p>有了持续集成的服务以后,最自然的想法是给项目加上当前集成状态的标示。有了这个东东,任何查看项目的人都能第一时间获知集成的状态,不良提交造成 build break 的情况就能迅速得到重视和修复。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/badge.png" width="327" height="71" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/badge_hufca7155811604486f1dc0fadcc65a073_4031_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/badge_hufca7155811604486f1dc0fadcc65a073_4031_1024x0_resize_box_3.png 1024w" loading="lazy" alt="badge" class="gallery-image" data-flex-grow="460" data-flex-basis="1105px" ></p> <p>如何添加可以看<a class="link" href="http://www.appveyor.com/docs/status-badges" target="_blank" rel="noopener" >这个页面</a>。</p> <hr> <h2 id="控制部署的触发">控制部署的触发</h2> <p>默认情况下 AppVeyor 是每个提交都会编译和部署的,也就是说,如果没有错误,每一次提交最终都会触发一次发布(Release)。而大多数时候我们需要的实际上是“每次提交的时候都编译,但只有指定的版本才触发部署”。读了<a class="link" href="http://www.appveyor.com/docs/branches#build-on-tags-github-only" target="_blank" rel="noopener" >一下文档</a>,我发现这个需求可以通过 <code>APPVEYOR_REPO_TAG</code> 这个环境变量(仅在 push 了包含 tagged commit 之后为真)来实现。</p> <p>具体做法如下图所示,在 Deployment 的选项里新增一个部署条件(Add deployment condition)判断一下 <code>APPVEYOR_REPO_TAG</code>,就可以做到只有打 tag 的提交才触发部署了。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_on_tag_1.png" width="620" height="68" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_on_tag_1_hu224decda2c733e3b4495102fa8183cad_3757_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_on_tag_1_hu224decda2c733e3b4495102fa8183cad_3757_1024x0_resize_box_3.png 1024w" loading="lazy" alt="deploy1" class="gallery-image" data-flex-grow="911" data-flex-basis="2188px" ></p> <p>打完之后测试了一下,这是一个 untagged commit 的行为,嗯,一切正常。</p> <p><img src="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_on_tag_2.png" width="851" height="102" srcset="https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_on_tag_2_hu4cf02edd1d6ae0248897951b0b06842f_17851_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-05-01-appveyor-ci/images/deploy_on_tag_2_hu4cf02edd1d6ae0248897951b0b06842f_17851_1024x0_resize_box_3.png 1024w" loading="lazy" alt="deploy2" class="gallery-image" data-flex-grow="834" data-flex-basis="2002px" ></p> <hr> <p>最后好奇看了一下针对 public projects 的收费版的价格,每个月 $29.5。不过俺觉得免费版日常已经基本够用了。</p> 2015.04 《中国文化概论》单元作业两则 https://gulu-dev.com/post/2015-04-06-chinese-culture-homework/ Mon, 06 Apr 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-04-06-chinese-culture-homework/ <p><img src="https://gulu-dev.com/post/2015-04-06-chinese-culture-homework/2015-04-06-chinese-culture.jpg" width="346" height="500" srcset="https://gulu-dev.com/post/2015-04-06-chinese-culture-homework/2015-04-06-chinese-culture_hu1c01d8acf78b7d73ef5e09d0bc76855e_38557_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-04-06-chinese-culture-homework/2015-04-06-chinese-culture_hu1c01d8acf78b7d73ef5e09d0bc76855e_38557_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="culture" class="gallery-image" data-flex-grow="69" data-flex-basis="166px" ></p> <hr> <h2 id="第五周-政治文化单元作业---宗法制度对中国社会的影响">第五周 政治文化单元作业 - 宗法制度对中国社会的影响</h2> <p><strong>谈一谈古代的宗法制度对中国社会的影响。</strong></p> <p>话题很大,由于没有系统地思考过这个问题,我只是浮光掠影地谈一下我的浅见。</p> <p>宗法制度是一套完整的伦理体系,在几千年的社会发展中,与自上而下的政治体系(庞大的官僚系统)一同维持着整个封建社会的运转。应该说,正如梁启超所言,虽然形式已不复存在,但精神仍是根深蒂固的。这种影响看不见,摸不着,却又在社会生活中无处不在。</p> <p>首先,宗法的基础——血缘关系——赋予了社会当中每一个人的天然的__归属感和依托感__。跟西方社会普遍强调的独立家庭有所不同的是,在中国,每一个人自出生的那一刻起,就成为了一个超越“家庭”这个概念的大家族的一份子。在典型的中国家庭中,我们从小就会学着熟悉有着繁多而琐细的亲戚称呼和枝繁叶茂的堂兄弟姐妹。从而使得个人看待社会的角度,很少以一个独立个体和人格的角度,而几乎总是站在峰峦叠嶂的家族关系之内。比如每一年的春运,还有所谓的“富贵不返乡,如锦衣夜行”,就是这种归属感和家族视角的集中体现。反过来,如柴静制作《穹顶之下》就是一个很少见的行为,是我们的社会欠缺的“个人”-“社会”的直接观察,联系和反馈。</p> <p>其次,对于个体而言,这种家族关系很容易造成一种泛化的__舆论压力__,压抑个性的追求,从古到今皆是如此。从庞勒的《乌合之众》中,我们可以知道,群体的行为会表现出排斥异议,极端化、情绪化及低智商化等特点,这些特点在个人和家族的冲突当中很容易被放大,因为大部分群体是临时性的(如集会),对个体影响有限,而家族群体则不然,大部分情况下是贯穿一个人生命始终的,所以造成的压力如果无法消解,就会很自然地被持续发酵和放大。这也是为什么在中国社会中,大多数人很小的时候就已经明白,如何去妥善地应对那些看起来是善意的(但是却是明显不切实际的或有悖个人意志的)期望。</p> <p>最后,宗法制度为大大小小的家族,上至帝王贵胄,下至平民百姓树立起__天然的权威__,也就是家长。这种对天然权威(也即是家长意志)的强调,使得温顺与服从成为理想家庭关系的基调,家长意志从来都是个人成长的一条暗线。在这种家庭环境中长大的“听话的”孩子,普遍在独立思考能力和批判性的思维能力方面有所不足。他们往往有着良好的教养和执行能力,却缺乏突破常规和挑战未知的勇气。进一步讲,出于对权威的敬畏,这样的人在社会关系中很难做到真正的不卑不亢,他们习惯于在给定的环境中表现出习惯性的“俯视”和“仰视”,却缺乏人际交往中一种根本性的“平视”。在我待过的民企和外企的对比中,这种社会环境所造成的文化氛围的差别,尤为明显。</p> <p>总得来说,宗法制度有着温情的一面,它把每个家庭有机地联系在一起,在家庭到社会之间构筑起强有力的纽带;但与西方较为独立的家庭结构并通过自然感情去后发地建立社会纽带相比,又在个体上捆绑了较多的权威意志和群体意志,对个性的培养多有冲击,有时甚至是压抑。在向现代社会的转变中,我们可以以现代的开放精神,对传统的伦理规范做出新的诠释,这其实也可以是我们对待传统文化的一般态度。</p> <ul> <li><code>2015-04-06</code> 答第五周 政治文化单元作业</li> </ul> <h2 id="第二周-哲学文化上单元作业---儒学文化的价值与局限">第二周 哲学文化(上)单元作业 - 儒学文化的价值与局限</h2> <p><strong>从尊奉孔子为“大成至圣先师”的极高礼赞,到高呼“打倒孔家店”乃至蔑称“孔老二”时的全盘否定,再到如今方兴未艾的“国学热”以及饱受争议的“中华文化标志城”规划,儒家思想作为中国传统文化的轴心和主要代表,在近现代受到前所未有的冲击。在中西文化交流日益频繁的今天,如何评价儒学文化在中国传统文化中的价值与局限,请谈谈你的看法。</strong></p> <p>正如题目中所言,儒学,是中国传统文化的轴心。我个人的体会是,虽然屡经各种冲击,儒学不仅没有中断,反而绵延至今,正如老师所言,不少道德准则,在两千年后仍然适用,在社会变迁中并未失去指导性的色彩。这里面反映出的,是一个具有绵延不断的__延续性__,海纳百川的__包容性__和与时俱进的__拓展性__的文化基因。我认为,这种延续性、包容性和拓展性,是儒学在不同的领域能展现出多样化的价值的原因,也是在过去,现在和将来都会具有强大的生命力的保证。</p> <p>我眼中的儒学文化,是中国文化脉络的内核与根基,也就是说,如果说枝繁叶茂的中国文明有着庞然之“形”,那么儒学就是凝然之“神”和傲然之“骨”。某种意义上,老师所讲述的儒学三期,正如我们中华文明所走过的轨迹,虽然饱经沧桑,却从未迷失和断绝。其实可以这样反推,假如没有儒家作为文化内核,中华文明还能否有这样的发展轨迹呢?道家,法家,甚至是墨家,以这些哲学为根基的文化,会发展出什么形态的文明呢?是否能展现出类似的延续性,包容性和拓展性呢?这是很难说的。</p> <p>如果说评价儒学文化的价值与局限,以我现在的能力,对这个命题恐怕还很难做出有价值的评价与回答。所以这里我只是浅谈一下自己的一些看法,浅薄粗疏之处,请老师指教。</p> <p>首先,我认为不用担心所谓现代的 “礼崩乐坏”,“文化冲击”,几千年的社会变迁尚且不能使其断绝,站在更广阔视角上的交流和融合,更不会让它堙没在历史之中了。况且,站在非中国传统文化(也即西方文明)的视角上,通过文明之间的对比和验证,涌现出来的许多对我们的认识,分析与研究,更是能避免中国文化的传承者们“只缘身在此山中”的困境,不少时候,甚至能给我们更加清晰准确的思考。前段时间我对汤因比的著作产生了一些兴趣,也是源于这种想法。</p> <p>其次,由儒家文化给我们的文明带来的这种“东方式”的文明特征,我们需要有深刻的认识。这种认识,不应被一时一地的社会制度所左右。无论是施政者还是普通人,都应有充分的理解,才能做出顺应我们文化特征的,不仅有现实意义,同样有历史意义的决策和实践。如果对传统文化,尤其是儒家文化缺乏深入透彻的理解,那么对中国社会的普遍性的思考模式和行为模式就可能会产生判断偏差,那么不管是“治国”(提出施政方案并躬亲执行),还是“修身齐家”(在中国社会中处理家庭和社会关系),都有与现实脱节的风险。</p> <p>最后,站在更广阔的视角,如果希望我们的文化在历史的长河中,能长盛不衰,历久弥新,那么就需要与其他的文化不断地参照印证,进一步地吸收和拓展。正如老师讲到的宋明理学对道家和佛教的吸纳那样,不断地去与其他的文化互动,融合,才能激活传统文化内含的巨大现实意义。我甚至认为,宋明之时程朱理学对道和佛的吸纳,仍然只是一种被动的接受,是被边缘化时的一种应对和反击;对于现今多元化的文化融合而言,更应主动地自我更新,注意吸收西方文明(尤其是自然科学)在近代的一些方法论与认识论,正如王阳明在理学的基础上发展出心学的轨迹那样,让传统文化在中华文明重新成为主流文明之际,萌发出更为积极的新枝新叶。对新时代的儒家传承者而言,这也算是在全球化的今天,“兼济天下”的一种理想和责任感罢。</p> <p>命题很大,自不量力所答。言之泛泛,有欠细致,尚请见谅。</p> <ul> <li><code>2015-03-22</code> 答第二周 哲学文化(上)单元作业</li> </ul> 2015.03 一个有趣的交互 bug ——兼谈游戏的引导系统 https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/ Sun, 29 Mar 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/ <hr> <p>昨晚在 iPad 上玩一个叫《变形金刚》的游戏时,遇到了一个无响应的 bug。想了想感觉可以记一下,写在这儿,对自己也是个提醒。</p> <hr> <p>当把游戏放下,过段时间后从解锁屏幕恢复时,这个游戏就无法响应任何的触摸输入了,屏幕上的具体情况如下图:</p> <p><img src="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/transformer.jpg" width="360" height="480" srcset="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/transformer_hu0d3f40e5d40763f82d21da1274c82816_33775_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/transformer_hu0d3f40e5d40763f82d21da1274c82816_33775_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="transformer" class="gallery-image" data-flex-grow="75" data-flex-basis="180px" ></p> <hr> <p>有经验的开发者,光靠这个截图,应该就已经大致明白发生了什么问题了。我们简单分析一下吧:</p> <ol> <li> <p>中间的消息框,是提示会话已过期需重新登录,这一类对话框很常见。为了防止玩家在登录已经失效的情况下操作,这个对话框通常会被设计为模态的,也就是所有其它的界面响应都会被它拦截,只有“确定”那个按钮能响应用户操作。</p> </li> <li> <p>而在屏幕下方,呈现的是一个标准的新手引导提示——所有的界面元素全部被遮挡,只有屏幕下方正中间的“剧情”按钮被高亮提示。</p> </li> </ol> <hr> <p>明白了各自的作用,理解这个“全屏无响应”的现象就很容易了——它们俩不约而同地觉得自己是全屏唯一该被响应的元素,其他所有元素都得被拦截。结果就是它们互相把对方给拦截了,谁也响应不了,整个游戏就死锁无响应了。</p> <hr> <p>原理说明白,解决方案就很简单了。</p> <ul> <li>首先,最简单的方案是,设计上显式指定响应的优先级。如让失效提示总是比新手引导更“高”,也就是弹框能无视新手引导的屏蔽(或反过来)。对代码做最小的修改,让他们彼此不冲突即可。(方案A)</li> <li>其次,稍复杂的方案是,让模态对话框和新手引导使用同一种机制去做屏蔽全屏触摸的操作,而通过那种机制去保证多个独占的界面元素同时发生时不会产生冲突。比如在有的操作系统上,当屏幕上已有一个未处理的模态对话框而又弹出一个时,原有那个会临时隐藏,待新的那个响应之后,原有那个会又弹出来,也就是一个__“栈(stack)”的串行机制__。这个机制的好处是,再有新的独占元素加入,也很容易纳入支持,代码里可以以统一且一致的方式处理。(方案B)</li> <li>最后,针对新手引导的特点,还有一种做法可能会让代码逻辑更简单和清晰——当新手引导中高亮独占的元素时,总是按照那个元素的区域在屏幕上的一个特定层生成一个隐形的新按钮。当玩家触摸时,总是把触摸事件由这个隐形按钮转发给对应的元素。这么做看起来是把事情复杂化了,但实践中往往能简化问题,因为跟那个被独占的目标按钮 (可能出现在 UI 场景树的任意层次上) 不同,总是临时创建新按钮,可以保证新手引导的所有交互始终发生在指定的那一层,极大地降低了新手引导与其他游戏机制冲突的可能性。(方案C) <ul> <li>大家知道,新手引导通常是纵贯整个游戏的所有功能模块的,一旦管理不善,琐碎的 bug 简直层出不穷。而且如果新手引导与被引导的具体功能交互按钮耦合过紧,模块内部的修改很容易波及到新手引导。(这也是新手引导往往都是项目后期才加进来的原因,当结构还在大改时,加新手引导就是作死啊)</li> <li>通过一层间接性,把引导系统和每个模块的具体 UI 实现隔离开,就能降低引导系统被无意中破坏的可能性,从而提高整个项目的工程质量。</li> </ul> </li> </ul> <p>这三种方案无所谓孰优孰劣,在不同的情境下,它们都有值得被考虑和实现的理由。进度压力和图省事,往往会使得我们选择方案A,但是,只有清楚ABC各自的利弊得失,并作出符合当时情况的选择,才能在项目的不断演进中,对潜在风险的变化情况做到心中有数。</p> <hr> <p>说完了这个交互的 bug,我想把话题转换一下,谈一谈作为一个普通玩家,我自己喜欢什么样的引导。这一部分非常主观,能坚持看下去的同学也挺不容易了,就随便看看吧。</p> <p>相比那些全屏独占,一步一步强制引导玩家完成整个引导流程的游戏而言,我更喜欢那种不主动打断玩家的,不强制执行的,比较温柔的引导提示,用黑话讲叫 “<strong>非侵入式的引导</strong>”。当打开一个新游戏的时候,与其被引导强制带着完成十几甚至几十步操作(这个过程有的游戏甚至长达5分钟以上),我更喜欢自己戳戳点点,看看会发生什么,这其实是一种很重要的乐趣。就好像新买了一个游戏机,我们一般不会去捧着说明书按部就班地走一遍流程(不管它印刷得再精美),而是会插上游戏抄起手柄立刻来两盘。</p> <hr> <p>非侵入式的提示有很多种,比较常见的有这些例子:</p> <p><img src="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/non-intrusive-1.jpg" width="400" height="300" srcset="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/non-intrusive-1_huca6b4e33dbdf50a8a206896623174892_39281_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/non-intrusive-1_huca6b4e33dbdf50a8a206896623174892_39281_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="non-intrusive-1.jpg" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>图标或按钮的右上角红点或数字——常用于新的功能开启,新的事件发生,新的消息收到,新的奖励可领,等等。</p> <p><img src="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/non-intrusive-2.jpg" width="374" height="215" srcset="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/non-intrusive-2_hu4bc3dc8b24d78ab146941aa42c89693e_30867_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/non-intrusive-2_hu4bc3dc8b24d78ab146941aa42c89693e_30867_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="non-intrusive-2.jpg" class="gallery-image" data-flex-grow="173" data-flex-basis="417px" ></p> <p>不需要手动确认的“气泡或文字标签”——“点点这里会有奇妙的事情发生哦~~”</p> <p><img src="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/medieval.jpg" width="1024" height="768" srcset="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/medieval_hu129f84285f83c6f1a773f7244b8a8b78_363035_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/medieval_hu129f84285f83c6f1a773f7244b8a8b78_363035_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Medieval" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>中世纪的战役视角,是非常典型的温和型提示系统。注意看左边侧边栏的一竖排事件图标提醒——如果你不处理,它就静静待在那里,处理之后即刻消失。而左上角的语音和文字提示是纯被动的,完全不影响任何玩家当下做出的操作。这些小的细节交织在一起,给整个游戏营造出一种非常温和,克制的氛围。</p> <hr> <p>总得来说,非侵入式的提示比一步一步的机械引导,对于设计者而言需要更多的思考和尝试,因为玩家和非玩家,对不同游戏类型的熟悉度,都会导致不同玩家对引导完全不同的需求。常见的是,有的玩家拿起来就能玩,他们会跳过任何提示;而有的玩家不管你怎么引导都不知道该怎么玩(无关智商)。有时,我们也许只是太久没玩一个游戏,忘了在游戏里能干些什么,应该怎么操作,而引导系统又怎么能知道我是否还记得怎么玩,啥时候想要再重温一遍哪个系统的引导呢?</p> <hr> <p>一些单机游戏在这方面显然有着非常细致的考虑,它们会把游戏的玩法(和剧情的进展)以完整的百科全书 (或可交互的方式)放在一个角落里。随着游戏的展开,玩家随时可以回去查看和熟悉:</p> <p><img src="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/game1.jpg" width="960" height="544" srcset="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/game1_hu786d9179966b36770873b01714003245_188613_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/game1_hu786d9179966b36770873b01714003245_188613_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game1" class="gallery-image" data-flex-grow="176" data-flex-basis="423px" ></p> <p>《英雄传说:闪之轨迹2》里有非常完善和排版精美的百科全书式游戏机制说明。</p> <p><img src="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/game2.jpg" width="960" height="544" srcset="https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/game2_hubb28872f3a4d3ecbd16354c9c3907e9c_109600_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-29-a-modal-deadlock-issue/game2_hubb28872f3a4d3ecbd16354c9c3907e9c_109600_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="game2" class="gallery-image" data-flex-grow="176" data-flex-basis="423px" ></p> <p>《讨鬼传·极》中玩家可以随时以新手任务的形式重温各种不同武器的玩法。</p> <p>说起来,如果我们不能把引导系统设计得 <strong>那么</strong> 聪明,那不如下些笨功夫,像这些单机游戏一样,做得细致一些罢。</p> 2015.03 如何对手写笔记进行漂亮和高效的排版? https://gulu-dev.com/post/2015-03-15-handwriting/ Mon, 16 Mar 2015 23:58:00 +0000 https://gulu-dev.com/post/2015-03-15-handwriting/ <p>在出差回程的飞机上,看着窗外的云海发呆,突然想起来前两天看到的<a class="link" href="http://www.zhihu.com/question/28645128" target="_blank" rel="noopener" >这个问题:“如何对手写笔记进行漂亮(和高效)的排版?”</a> 左右无事,就在本子上简单地答了一下,今天回到电脑上补了一下词句和格式,就发上来了。</p> <p>抛砖引玉,请学霸和学神们轻拍——好吧,俺知道学神是不会点开这个问题的——“啥是手写的笔记,能吃吗?”——学神们从来不会手动地记笔记,他们的大脑就是随身带着的海量便携笔记。</p> <hr> <p>大多数人(包括我自己)都认为,现在已然是数字时代——与传统的手写笔记相比,以数字形式呈现的笔记具有各种明显的优势。但是对前面那个问题的思考,让我回想起纸和笔的很多独特的优点,那么我们就先从手写笔记的好处说起吧。</p> <hr> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">I like the process of pencil and paper as opposed to a machine. </span></span><span class="line"><span class="cl">I think the writing is better when it&#39;s done in handwriting. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> - Nelson DeMille </span></span></code></pre></td></tr></table> </div> </div><hr> <p>正如 Nelson DeMille 所说,纸和笔的组合,其实是一个灵活度非常高的系统,基本上相当于以下这些工具的集合:</p> <ul> <li>一个朴素的文本编辑器</li> <li>一个朴素的画板</li> <li>一个朴素的制表工具 (利用横向的引导线,可快速形成表格)</li> <li>一个思维导图工具</li> <li>一个支持任意符号的公式编辑器</li> </ul> <hr> <p>在下面的两页对开笔记中,我们可以看到,对于一份规划良好的笔记,其主人是如何娴熟地运用这些工具去表达内容的。</p> <p><img src="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-0.jpg" width="800" height="600" srcset="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-0_huf9f7cb41afc7b107418bde7f5b8303a3_131438_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-15-handwriting/images/note-0_huf9f7cb41afc7b107418bde7f5b8303a3_131438_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="note-0" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>可以看到,这页笔记呈现的信息量适中,使用了流程图,弱对齐表格,概念分解大纲,阶梯缩进等方式去组织内容。可贵的是,这些形式都是为内容服务的,不仅不突兀,反而让信息的呈现显得均衡而优美。</p> <p>如果你曾尝试过用上面提到的那些工具,搭配着去实现这一页笔记的效果的话,就会发现,这个过程远不如一支笔和一张纸,来得自然和流畅。</p> <hr> <p>明确地说,纸和笔在以下这些方面具有一定的优势:</p> <ol> <li><strong>无限制的图文混排能力</strong> <ul> <li>各种点、线、面、文字和图形的任意组合混排。这种混排能力,运用得当的话,能让手写的笔记非常直观地展现出思维的原貌。</li> </ul> </li> <li><strong>特殊符号的录入能力</strong> <ul> <li>除非专业的编辑,普通人在使用时,寻找和插入某个特殊符号是非常耗时且不便的,有时甚至根本找不到自己需要的符号。</li> </ul> </li> <li><strong>内嵌不同信息格式的能力</strong> <ul> <li>表格,思维导图,信息流,各种公式,用手书写可以达到任意复杂度。而使用普通的字处理软件编辑一个公式的时间都够画十个小乌龟了。</li> </ul> </li> <li><strong>无限制的信息编辑能力</strong> <ul> <li>在随着思考对笔记本上的内容涂涂改改,圈圈点点时,各种杂乱信息可以即刻反映出脑海中的即时图景。但各种字处理软件或笔记软件,在编辑大量图形化的信息时非常低效,手上的节奏完全跟不上脑子里思路的变化</li> </ul> </li> <li><strong>无限制的信息结构化的能力</strong> <ul> <li>对于任何一页笔记,承载于其上的信息可以有任意多的组织方式,任何一个关键信息可能随时被追加到任何一个坐标点上,帮助做笔记的人厘清思路。这一点上 OneNote 可以部分达到这种便利性。</li> </ul> </li> </ol> <hr> <p>既然有这些好处,那么在实践中,具体应该如何去运用呢?</p> <p>嗯,终于说到正题了。用题主的话讲,究竟该如何对手写笔记进行漂亮和高效的排版呢?</p> <p>且听俺一一道来。</p> <hr> <p><strong>一、使用关键字和精简后的短语,而不是完整的句子。</strong></p> <p>保持极尽可能的简洁,最直接的好处就是__写得快__,而且单位面积信息量更大。刻意地只保留关键信息,对温习也很有好处,当翻开笔记时,可以随时展开并还原出完整的内容,并检验自己的理解程度和记忆效果。</p> <p>看下面的这个例子:</p> <p><img src="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-1.jpg" width="800" height="600" srcset="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-1_hu816c701e864950d96804dde421a0a45a_134453_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-15-handwriting/images/note-1_hu816c701e864950d96804dde421a0a45a_134453_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="note-1" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>这块笔记中,以概念和概念之间的关系为主,这是非常有效的形成__网状知识结构__的记录方式。通常能够帮助学习者廓清各种干扰因素,快速抓住事物的骨架和本质。</p> <hr> <p><strong>二、有意识地留白和缩进,手动控制信息的布局和密度,考虑易读性。</strong></p> <p>千万不要为了节省纸张,记得太满。有的同学的笔记记得像作弊条,密密麻麻。这样一方面关键信息容易被淹没,另一方面,后面要批注或添加新的信息也无从下手。</p> <p><img src="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-2.jpg" width="500" height="622" srcset="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-2_hufababcc037ff8e08f84d26425e04d66f_98168_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-15-handwriting/images/note-2_hufababcc037ff8e08f84d26425e04d66f_98168_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="note-2" class="gallery-image" data-flex-grow="80" data-flex-basis="192px" ></p> <p>在这个例子中,可以看到笔记的主人有意识地使用了锯齿状的反向阶梯缩进来控制布局,信息不仅变得高度有序,而且产生了一种强烈的__韵律感__。大家知道,人们倾向于对优美和规律的事物印象深刻,而对混乱和无序的事物本能地抵触和厌恶。所以,如果记录下的信息呈现出一定的韵律感和节奏感,是非常有利于形成深刻的记忆的。</p> <p>用笔记录的一大目的,在于__更有效地提取信息__。笔记记得越是密不透风,就像文件压缩得越厉害,提取信息就越慢,学习效率和转化率也就越低。</p> <hr> <p><strong>三、使用一些程序设计的思维方式来组织信息,避免冗余。</strong></p> <p><strong>「宏定义」</strong> 作为程序猿,我们喜欢把一些复杂的表达式定义为一个“宏”(macro),然后每次使用这个宏,就相当于使用了整个表达式。在笔记中,为了方便,我们完全可以把一些专有概念定义为一个宏,以便引用。举个例子,“跨省”就是一个很好的宏,一说你就知道是什么。</p> <p><strong>「函数与信息引用」</strong> 作为程序猿,我们把通用的代码组织成函数或类,以免把同样的逻辑到处复制粘贴,反复修改,导致代码无法维护。把这种思想运用到笔记中,我们会尽量抽取信息的公共部分,在必要时加以引用,就可以避免迷失在重复而冗余的信息里。</p> <p>这一招对理科的习题尤其管用,你会慢慢发现,那些被提取出来的公共部分,慢慢地就成了你的工具箱的一环,也就是各种解题方法论的来源。这个过程,俺称为 <strong>“思维的工具化” (toolization)</strong> 。当你的积累达到一定程度(也就是工具箱里的工具足够多)时,你就会自然而然地在头脑中形成框架性的方法体系。这时,不管什么题目,只要立足于教科书上已有的知识点,你会比其他人更容易地分解为已知问题的集合,从而更快地解决问题。</p> <p><strong>「模块化」</strong> 大多数人记录笔记的唯一方式,就是随着时间的流逝,不断地在末尾追加新的内容。今天记两个解题技巧,明天记几句课余讨论,后天又记两段课堂笔记,这其中又穿插着各种习题。这样其实是不太利于消化吸收的。更好的方法是按照主题划分地盘,相同相近的主题尽量放在一起,形成思维上的连贯性,避免不相干信息之间造成交叉干扰。如果有 <strong>标签 (tags)</strong> 就更好了,在笔记本的边缘分门别类地打上标签,更有利于快速检索。</p> <p><img src="https://gulu-dev.com/post/2015-03-15-handwriting/images/tags.png" width="706" height="410" srcset="https://gulu-dev.com/post/2015-03-15-handwriting/images/tags_hu4edb66ee5706080a7dfe1e4ca9b2260d_373448_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-03-15-handwriting/images/tags_hu4edb66ee5706080a7dfe1e4ca9b2260d_373448_1024x0_resize_box_3.png 1024w" loading="lazy" alt="tags" class="gallery-image" data-flex-grow="172" data-flex-basis="413px" ></p> <p>本图取自<a class="link" href="http://item.taobao.com/item.htm?spm=937.1000770.1000419.2.1ABH3l&amp;id=43693211114&amp;asker=wangwang&amp;wwdialog=bbxxbbmc&amp;ad_id=&amp;am_id=&amp;cm_id=&amp;pm_id=" target="_blank" rel="noopener" >这里</a>(taobao 链接),感谢 linyukun2008 授权使用。</p> <hr> <p><strong>四、善于设计和使用词汇表。</strong></p> <p>在正文中如果用到了一个名词,概念,有必要补充说明的话,应该补充在末尾的词汇表里,这样当下次再遇到时,可以直接引用词汇表。如果只是记在“首次遇到这个概念”的地方,日后翻阅笔记时,就有可能会出现“找不到自己记在哪儿了”的尴尬。</p> <hr> <p><strong>五、“关联式记忆”</strong></p> <p>笔记不是学术论文,是写给小伙伴们看的(当然主要是自己),不一定要那么严肃,俏皮一点,卖个小萌,写点琐事,段子,甚至兴之所至写首打油诗,也未尝不可。多年以后,拿起自己的笔记,看着上面的片段,或温馨,或捧腹,或感动,别有一番滋味在心头。</p> <p><img src="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-3.jpg" width="800" height="600" srcset="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-3_hu27262fdade79bcb6081e8e91be4d8540_119361_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-15-handwriting/images/note-3_hu27262fdade79bcb6081e8e91be4d8540_119361_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="note-3" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>画个小人,一下就记住了吧 ^_^</p> <p>这种记忆方式学名叫做“关联式记忆”,意思是说,把你要记住的信息总是和一个特定的上下文关联起来。这样当你忘掉这个信息的时候,一回忆起当时的上下文,相关的信息就一下子都回来了。根据俺的经验,这一招对考试还是挺管用的。</p> <p><img src="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-4.jpg" width="800" height="600" srcset="https://gulu-dev.com/post/2015-03-15-handwriting/images/note-4_huc45c2529561f5a731efcc4b4c50b931d_125669_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-03-15-handwriting/images/note-4_huc45c2529561f5a731efcc4b4c50b931d_125669_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="note-4" class="gallery-image" data-flex-grow="133" data-flex-basis="320px" ></p> <p>“手绘版”图标——是上课太无聊了吧……</p> <hr> <p>有一次春节回老家过年,整理十多年前的旧物,看到物理笔记本上写的小酸诗,打的欠条,从报纸上抄来的游戏攻略,上课时收到的小纸条,脑海中涌现出小伙伴们的音容笑貌,瞬间无比的清晰。这一本本学生时代的笔记本,在那时,也许只是应付高考的枯燥流水账,多年以后,却成了记忆中低声的沙沙私语和温柔的轻轻呢喃。</p> <hr> <p>好了,在跑题跑到不着调之前,就这样收尾了吧。 在结束前,俺再多唠叨一句。技巧终归只是技巧,再多的技巧,也比不上“认真”二字。</p> <p>你说呢?</p> <hr> <p>另,感谢一下我的妻子 Jessie,本文中出现的笔记样本,均取自她在西山居 (感谢组织!) 安排的营养师培训讲座上的课堂笔记。看了她的笔记,俺终于明白,为啥初中和高中六年间的各种考试俺从来都考不过她了——还是收下俺的膝盖吧~~</p> 2015.02 玩的就是资产! - 比特币与游戏货币体系 https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/ Tue, 24 Feb 2015 00:00:00 +0000 https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/ <img src="proxy.php?url=https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_gaming.jpg" alt="Featured image of post 2015.02 玩的就是资产! - 比特币与游戏货币体系" /><p>上个月在知乎上看到一个问题:</p> <p><a class="link" href="http://www.zhihu.com/question/27621853" target="_blank" rel="noopener" >如果使用电子加密貨幣來充當遊戲貨幣體系的一環,會對遊戲有什麽影響?</a></p> <p>这个问题很有意思,当时我就随手点了关注。趁着春节假期还没结束,今天俺就写几段来抛砖引玉吧:) 如有偏颇之处,还请大牛指正。</p> <p>这篇文章里,俺先探讨一下已经出现了的几个观点,然后再聊聊已有的游戏与比特币结合的各种情况,最后无责任地开几个脑洞,过年嘛,图个一乐:)</p> <hr> <pre><code>It's not about the money - it's about the game. - Wall Street: Money Never Sleeps </code></pre> <hr> <h2 id="预先的约定和探讨">预先的约定和探讨</h2> <h3 id="约定">约定</h3> <p>为了消除一些歧义和模糊,讨论前我们先做一些约定。也就是说本文提到前面的这些字眼的时候,实际上指代的是后面这一长串内容。</p> <ul> <li><strong>游戏币</strong> - 为了实现网络游戏内的物品和价值流动,由开发商在游戏内发行的虚拟货币。</li> <li><strong>加密货币</strong> - 电子货币的一种,特点是借助加密技术实现,发行和交易过程由算法定义和保证。</li> <li><strong>比特币</strong> - 目前应用最广泛的加密货币,由中本聪发明,特点主要是去中心化和匿名性。</li> <li><strong>替代币</strong> - 为了某些特定需求或特性,依据比特币的参考实现,定制化后得到的一些特定版本。俗称“山寨币”。</li> </ul> <p>他们之间的关系见下图:</p> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/nouns.png" width="537" height="307" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/nouns_hub1aa3b1ecdaff55343db6e3d93496486_20517_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/nouns_hub1aa3b1ecdaff55343db6e3d93496486_20517_1024x0_resize_box_3.png 1024w" loading="lazy" alt="nouns" class="gallery-image" data-flex-grow="174" data-flex-basis="419px" ></p> <hr> <p>在已有的答案里,我看到了一些个值得一说的观点,这里先逐个探讨一下:</p> <h3 id="讨论观点1-产出和产量的问题">讨论:(观点1) 产出和产量的问题</h3> <p><strong>使用比特币做游戏货币,游戏公司没法控制产出和产量,“游戏内货币流通处于无法控制的局面”,这样能行得通吗?</strong></p> <p>看到有不少同学纠结于控制权的问题,那么我们就先聊一下这个吧。</p> <ul> <li>首先,对于拿加密货币来做游戏币的游戏,假如希望完全控制游戏内的货币产出和流通,是完全可能而且技术上并不复杂的。只需要发行一种针对该游戏特别定制的替代币就好了,比如魔兽世界可以定制一下比特币的代码,发行一种“魔兽币”,仅在魔兽世界的游戏内有效。有需要的话,游戏也可以提供与比特币的单向或双向兑换接口。</li> <li>其次,假如直接使用比特币,也问题不大。原因有二: <ol> <li>游戏虽然无法直接发行货币(除非自己掏钱),但可以产出各种虚拟资源(如能量,矿石等) 这些资源,根据其稀缺程度,也能被用来调整游戏内的流通情况。</li> <li>需要明确的是,游戏内货币体系控制的关键在于控制<strong>消耗</strong>而非产出。怎么样通过消耗的差异性,去平衡不同消费能力的玩家的游戏体验,从而维持和保护整个系统的<strong>相对</strong>平衡,这才是最紧要的问题。实际流通的货币是由游戏机制(官方)产出,还是由外部世界注入,其实影响相对较小。</li> </ol> </li> <li>但是这也并不意味着全无问题,对于这后一种情况,游戏公司需要注意的是<strong>外部币值大幅波动</strong>带来的运营风险。如果币值短期内变化太剧烈,而系统又缺乏足够的弹性,就会导致玩家各种抱怨,比如同样的奖励前后价值不一的问题,伤害游戏体验的风险会很大。</li> </ul> <hr> <h3 id="讨论观点2-总量有限导致的通缩问题">讨论:(观点2) 总量有限导致的通缩问题</h3> <p><strong>“很可能,最初的一个铜板可以买到一瓶可以回满血的小型体力药水,而后来100个金币也不能让我的血量回满&hellip;在比特币环境下,玩家打怪收益越来越少,怎么也爽不起来。数量有限性和通缩性质决定了这种货币无法应用于游戏。”</strong></p> <p>这实际上是一个有意义的问题。由于比特币数量有限,游戏内的货币总量越大,交易越活跃,受众越广泛,其实际价格就越高,同样面额的一个单位就越值钱,相应的交易单位就越小。从比特币面世的这几年历史来看也的确如此,从第一次交易的“一万个比特币购买了一个25美元的披萨优惠券”,到后来的“0.13个比特币结算了一笔650元的餐费”,可以想见,如果价格持续上涨,迟早有一天得要 0.000025 BTC 这样的方式去交易。</p> <p>这个通缩的问题,主要是由于产出的稀缺性和应用的广泛性的矛盾引起的。而且由于比特币的产量每四年减半,沉积在遗失或遗忘的私钥中的钱也会越积越多,然而(按照目前正常的节奏)应用却会越来越广泛,随着时间的推移,通缩问题会逐渐被放大。</p> <p>那么通缩具体会带来什么问题呢?</p> <pre><code>“通缩螺旋” (凯恩斯学派的经济学家们认为,物价持续下跌会让人们倾向于推迟消费,因为同样一块钱明天就能买到更多的东西。消费意愿的降低又进一步导致了需求萎缩、商品滞销,使物价变得更低,步入“通缩螺旋”的恶性循环。同样,通缩货币哪怕不存入银行本身也能升值(购买力越来越强),人们的投资意愿也会升高,社会生产也会陷入低迷。) </code></pre> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/deflation.jpg" width="320" height="231" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/deflation_hue24d9344d28a55b4871a534ce066aa4b_15657_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/deflation_hue24d9344d28a55b4871a534ce066aa4b_15657_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="deflation" class="gallery-image" data-flex-grow="138" data-flex-basis="332px" ></p> <p>总得来说,通缩不单单是货币本身的事,是一个社会化的问题,从历史上看,急剧的通胀和通缩几乎总是很危险的。由于俺没有系统地学习经济学,这一点就不多嘴了。</p> <hr> <p>实践中看,由于大部分游戏的大部分时间里总是处于通胀状态(不少网络游戏到了后期钱甚至失去了一般等价物的意义),适度的系统性通缩是有好处的。然而也可能会有下面的问题:</p> <ol> <li>首先是<strong>技术可行性</strong>的问题。虽说比特币理论上是可以任意拆分到最小单位—— 1 Satoshi,也就是 0.00000001 BTC,但实际上为了与价格波动相匹配,游戏内与货币挂钩的系统大多需要某种程度上的动态化,也就是<strong>根据 btc 的实际价值动态调整数值</strong>的能力。从工程角度讲,这非常显著地增加了系统的实现复杂度,而且并不是所有的游戏类型都适合动态化的。</li> <li>其次是玩家心理上的影响。玩家的游戏内财富总体趋势是积少成多的,但由于前面说的通缩,光看绝对值反而是越来越低的,感觉上可能会有点怪怪的(因为我们已经习惯了随着年复一年的通货膨胀,手上的钱的面额越来越大却越来越不值钱),而对这种心理上的不适,通过改单位 (如大家都换用 mBTC 或 μBTC) 也很难消除。嗯,大家可能会觉得谈玩家心理有点小题大做了,可是在游戏行业的体验让我明白,小看了玩家的直观感受对整体游戏体验的影响,是会吃大亏的,呵呵。</li> </ol> <p>总得来说,缓慢的通缩不是坏事,而相对急剧的通缩也几乎总是可以采取一些措施去协调,要么可以<strong>使用系统内非货币资源来刺激消费和流动</strong>,要么可以在游戏内<strong>实现一些风险各异的投资/投机渠道</strong>,同时也可作为系统回收货币的一个途径。</p> <hr> <p>其他的几个问题相对浅显,俺也就不再一一细说了。接下来开始正面地回答原问题 :)</p> <hr> <h2 id="网络游戏与数字货币的几种结合方式">网络游戏与数字货币的几种结合方式</h2> <h3 id="第一种小打小闹型的游戏类应用">第一种,小打小闹型的游戏类应用</h3> <p>所谓小打小闹,是从游戏规模角度来说的。有不少休闲类的游戏,尤其是在移动设备上,由于游戏本身就非常简单,游戏内实际上是没有流通的货币体系的。这类游戏里,加密货币可以用来充当纯粹的激励用途。比如前段时间在 iOS 上架的 <a class="link" href="https://itunes.apple.com/us/app/sarutobi/id932194840" target="_blank" rel="noopener" >SaruTobi</a>,玩家在玩的同时,可以获得系统按某个规则零星分发的比特币,而系统用于激励玩家的比特币,则来自于游戏的广告收入和内购。</p> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/sarutobi.png" width="630" height="263" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/sarutobi_hu4afed385e7b1c91a165e132af18a9248_104275_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/sarutobi_hu4afed385e7b1c91a165e132af18a9248_104275_1024x0_resize_box_3.png 1024w" loading="lazy" alt="sarutobi" class="gallery-image" data-flex-grow="239" data-flex-basis="574px" ></p> <p>对这一类游戏,有一个适合小额支付的加密货币叫狗狗币(Dogecoin),这个币的特点是确认速度快,单位非常小(按目前的价格一万狗狗币只值人民币9元左右),很适合用在这种单向的绝大部分都是小额交易的游戏环境里。</p> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/dogecoin.png" width="640" height="285" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/dogecoin_hu9ecce36ae4b16af183381861220ab7da_151073_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/dogecoin_hu9ecce36ae4b16af183381861220ab7da_151073_1024x0_resize_box_3.png 1024w" loading="lazy" alt="dogecoin" class="gallery-image" data-flex-grow="224" data-flex-basis="538px" ></p> <p>前两天还看到一个网站叫 <a class="link" href="http://ownme.ipredator.se/" target="_blank" rel="noopener" >BTC Piñata</a> 很有意思,也可以算在俺说的这种现金激励型的分类里头。 这网站为了强调自己的安全性很强,把一个含有10个btc的钱包的私钥放在服务器上,宣称谁能攻破服务器就能拿到这笔奖励。很眼熟吧,是不是想起了下面这货?</p> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/3m-security-glass-billboard-3-million-dollars.jpg" width="640" height="580" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/3m-security-glass-billboard-3-million-dollars_hu0f3e1b3c8e0adbbad237162e6432ddb4_81802_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/3m-security-glass-billboard-3-million-dollars_hu0f3e1b3c8e0adbbad237162e6432ddb4_81802_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="3m" class="gallery-image" data-flex-grow="110" data-flex-basis="264px" ></p> <p>看了这图的文件名我知道这玻璃为什么叫 3M Security Glass 了——因为里面放了 “3 Million Dollars”。</p> <p>其实游戏内这样的手段可以更多,比如首杀得私钥什么的,呵呵。</p> <h3 id="第二种允许使用比特币充值游戏内仍保留自身的货币体系">第二种,允许使用比特币充值,游戏内仍保留自身的货币体系</h3> <p>这是最传统的模式(交易所模式),比特币仅仅作为法币的另一个替代品和支付手段存在。</p> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_1.png" width="522" height="343" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_1_hu1f9dba604cdaf8cb3639d08dde4c37a2_21306_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_1_hu1f9dba604cdaf8cb3639d08dde4c37a2_21306_1024x0_resize_box_3.png 1024w" loading="lazy" alt="btc_1" class="gallery-image" data-flex-grow="152" data-flex-basis="365px" ></p> <p>这一种模式本身没什么好说的,要点在于是否允许把游戏币转回比特币,也就是提现。我的看法是完全封死,仅保留单向的输入,游戏乐趣会失去不少,而完全开放的话,对游戏本身的素质和安全性的要求又会高出很多,折衷的办法是可以允许每日限额提现,当日充值越多,或VIP等级越高,这个额度就会相应的高一些。系统也可以通过调整兑换汇率去调控。</p> <p>另外,从政策角度看,国家现阶段对加密货币的定位主要是**“虚拟商品”**,因此转入转出本身不仅合法,而且是相对符合政策精神的。大家可能会觉得加密货币是具有一定风险的新鲜事物,而且又有一定的自由主义的精神,国家未必会在短期接受,但实际上的情况是政策的拟定者显然已经对加密货币有了相当深入的了解和评估,他们的评价总得来说是到位和中肯的,既具有一定的前瞻性和现实性,亦不失温和与理性。</p> <pre><code>“算法货币只解决了信用问题,但如果没有适用经济需求的供给调节机制,就无法解决币值的波动问题,它可以成为金融产品,金融资产,却无法成为一个好的货币。但是,法定货币与私人货币的共存是人类社会的常态,数字形态的私人货币可与法定的电子货币共存。开源共享的分布式信息技术创造了信息的互联网,我们也可以用这个技术传递数字货币,低成本高效率地完成价值传递。” - 原央行副行长、现全国人大常委会常委、财经委副主任吴晓灵 “以互联网公司为主的互联网清算体系逐步形成,阿里双12已冲击传统清算体系。当谈及超主权货币的探索时,王永利表示,如比特币,瑞波币这样的互联网货币体系有待验证,值得探讨,未来的货币发展趋势可能会是超主权虚拟货币。” - 中国银行原副行长王永利 </code></pre> <h3 id="第三种直接使用数字货币作为游戏的流通货币不再实现游戏专属的游戏币">第三种,直接使用数字货币作为游戏的流通货币,不再实现游戏专属的游戏币</h3> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_2.png" width="533" height="298" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_2_hu6a451163462564f7c68fcfc3ca8b4a5e_22110_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/btc_2_hu6a451163462564f7c68fcfc3ca8b4a5e_22110_1024x0_resize_box_3.png 1024w" loading="lazy" alt="btc_2" class="gallery-image" data-flex-grow="178" data-flex-basis="429px" ></p> <p><strong>链上交易vs链下交易</strong></p> <p>如果是刚刚说过的传统的交易所模式,除了充值以外,正常的游戏内活动通常是链下交易的。但链下交易由于资金都在服务器端,风险比较大。虽然一般来讲,这些资金是由官方离线储存在冷钱包里的,安全系数理论上比一般的游戏服务器上的数据要高。但不像法币有成熟的金融机构做依托,加密货币的所有安全都需要自行打理,一旦因为内部原因或社会工程的原因发生事故,后果不堪设想。</p> <p>那么如果在游戏内采用链上交易呢,风险都转移到了客户端。由于与官方相比,玩家大部分是缺乏足够安全意识的,很容易造成私钥丢失或被盗。其实可以考虑官方送硬件钱包,这样虽然安全了,不过感觉麻烦一点,每次花钱还需要扫一扫。</p> <p><strong>私钥的所有权问题</strong></p> <p>这个问题天然的答案是每个玩家的私钥由该玩家自己所有。但是问题在于,不管私钥在物理上存储于服务器还是客户端,如果游戏宣称“私钥的所有权是玩家(通俗的说就是口袋里的钱是玩家自主支配的)”,那么理论上严格来讲,游戏的官方实际上是不能动用该私钥去签名的(也就是不能从玩家口袋里转移财富的)这样的话,跟传统的游戏货币相比,官方实际上失去了一部分控制能力,比如盗号销赃,利用bug刷钱等等,在传统游戏来讲,可以直接改数据库甚至全服回档,但如私钥的所有权是玩家,官方就无法这么直截了当了(但随时冻结/解冻问题应该不大),因为(法理上)私有财产是不容侵犯的。</p> <p><strong>是否使用定制的替代币</strong></p> <p>使用定制的替代币是一个折衷,损失一部分与外界原生的流通性,来隔离和保护游戏内的各种机制,尽量少受外部市场的干扰。这个前面已经讨论过,就不再多说了。</p> <hr> <h2 id="花式脑洞展览会">花式脑洞展览会</h2> <p>前面介绍的都是相对正统的应用,后面咱们放松一点,开几个脑洞乐一下吧 :)</p> <h3 id="游戏内的组织运作">游戏内的组织运作</h3> <p>这里咱们先介绍两个概念,然后再讲讲对游戏来说这两个概念意味着什么。</p> <p>有一个跟比特币关系比较密切的术语叫做 DAC (Decentralized Autonomous Corporation 或 Distributed Autonomous Community) 直译过来叫做去中心化自主社团或分布式自治社区 (写着写着有种共产主义的感觉)。这种组织的关键在于它不依赖于某个个体的判断和决定 (因为人可能会过于主观,或独裁,或被收买),而是依赖于某种公之于众的机制(或算法)来运作和做出选择(It can be thought of as a corporation run without any human involvement under the control of an incorruptible set of business rules.)。举两个俺认为比较符合 DAC 的例子吧,一个是互联网本身,一个是比特币的支付网络,当然它们也不算是严格意义上的社区。俺的理解是,理想中的 DAC 可以看做是开源运动(代码自主协作),Wikipedia (知识自主协作)和众筹(财富自主协作)的增强版——当然现在大多数宣称自己是 DAC 的组织实际上仍有明确的主导者,正如 Linux 开源社区沐浴在 Linus 的光环下那样。说到这里俺想起来 Linus 曾在自传中提到过他之所以相信并选择了 Linux 的开源开发模式,不知道是不是受到了身为共产主义者的父亲的影响。呃,扯得有点远……</p> <p>除了 DAC 外还有一个比特币专属概念叫多重签名 (multisig,也就是 “n-of-m”),接触过比特币的同学可能大多都听说过。所谓多重签名,俺这里简单介绍一下,就是同样一笔交易,需要多个私钥的签名才会有效。举个例子,假定我们有 5 个人,每人有一把钥匙,而所有的钱都放在一个上了把锁的箱子里。当需要发生交易行为的时候,总是需要至少 3 个人的钥匙才能共同打开这把锁。这对于咱们刚说过的 DAC 来讲天然是一个好的用于“共同保管”和“纷争处理”的工具。</p> <p>有了 DAC 和 multisig 的帮助,我们在游戏里能做的事儿可就多了。比如说玩家可以在游戏里找合伙人开公司,与参与者分红,或者发起众筹组团去刷 boss ,然后按地址发奖励等等。当然这些事情现在也都能做,只是原先必要的某些信任或交情,现在可以交给机制来保证了,并且所有的资金流动全部有据可查,降低了这些社会活动的门槛。</p> <h3 id="由玩家发行的自主货币">由玩家发行的自主货币</h3> <p>是的,正如在加密货币的领域已经发生的那样,自主发行货币不是啥难事。现在要去 GitHub 上 fork 比特币的代码下来,改成自己的山寨币,还需要一些程序方面的知识,即使这样,在 <a class="link" href="http://coinmarketcap.com/" target="_blank" rel="noopener" >coinmarketcap.com</a> 上列出的山寨币都已经有近六百种了。以后随着行业的成熟,可以期待的是,发行新币比现在要简单的多,差不多就像给一个游戏做 MOD 那样。如果允许玩家发行货币,并可与游戏内的主体货币双向兑换,就是相对成熟和完整的体系了。可参考目前比特币和一众山寨币之间的关系。</p> <pre><code>明朝有个很有趣的货币史记载:一名将军克扣军饷过重,在他的治下每名士兵只能领到300文等于1两银子的军饷;由于他的军队是附近地区最主要的采购者,于是在整个地区之内,铜钱和银两的兑换关系也都变成300文等于一两了。另外一个有趣的例子在明末的福建地区。福建地区原本使用嘉靖钱,兑换比例是3两=1000文;然后康熙朝建立统治之后发行了康熙钱,官方汇率是1两=1000文。为了满足当地人民存钱不贬值的需求,就出现了1嘉靖钱=3康熙钱的情况,并维持了十余年之久,当地人民甚至私自铸造明钱来满足交易的需求……在宋朝的笔记记载之中,更是指出,东京汴梁的每一个行业都有自己的交易贯,没有一个是足额的,各自分别用不同的数百枚钱币设定为一贯钱进行结算。这个贯就是一个自我调整的通货单位,有时以铜钱形式出现,有时以储藏的形式出现,有时又以记账的形式出现,十分多变。 -- 摘自 旗舰评论——战略航空军元帅的旗舰 “构建通货:游戏世界的货币史——从流亡之路(Path of Exile)谈起” </code></pre> <p>如果游戏官方发行了一个定制版的加密货币,那么完全可以通过提供一些接口,来方便玩家在官方版本的基础上扩展出自己的货币(正如给魔兽世界做插件那样)。大家小时候都做过大富翁里的“银行家”吧,跟玩得好的小伙伴都曾有过一个“秘密市场”来交换小玩意儿吧,如果能在游戏里发行货币的话,就能玩出很多花样。上学的时候曾经用食堂的鸡腿换过同桌的作业抄,这下方便多了。</p> <p>而且一旦降低了自主发行货币的门槛,反而会减少官方货币的复杂度和运作风险。一方面,一些奇奇怪怪的小需求就可以交给自主货币去实现了,另一方面,很多实验性的功能可以放在自主发行货币中去做,成熟了再合并回去,从这个角度来说,甚至可以允许发行一些一次性的为了处理某种特殊情况的货币,用过即焚。</p> <h3 id="去中心化的虚拟世界">去中心化的虚拟世界</h3> <p>好吧,这个脑洞开得更大了。大家知道现在的网络游戏基本上全是客户端/服务器架构的。很自然会有同学问,为什么没有 P2P (也就是不需要服务器) 的大型网络游戏呢?</p> <h4 id="p2p-游戏的困难和应对之道">P2P 游戏的困难和应对之道</h4> <p>这个问题细究起来实际上有很多原因,但其中最重要的问题,是 P2P 类的游戏缺乏<strong>合法性认证 (authorization)</strong> 的能力,通俗的说就是没法区分普通客户端和非法客户端。还有一个问题,就是如何保存每个玩家的游戏进度,完全去中心化的分布式存储是个相对复杂的问题,这个跟大家熟知的企业内的分布式存储是两码事。</p> <p>其实,这两个问题本质上是一个问题,就是<strong>唯一性</strong>的问题,而比特币的区块链 (Blockchain) 技术,恰恰就是用来解决去中心化网络内的一致性问题的完美解决方案。下面我们分别来看这两个问题。</p> <ol> <li>反作弊这个问题,即使在有服务器辅助校验的情况下,都很难彻底解决。但是只要玩家的操作信息保留下来了,还是相对比较容易分辨其行为是否为非法的。我们可以周期性地把活跃用户的关键动作采集并打包成一次交易 (transaction) 提交到网络。这样每个玩家的关键操作均以交易形式保存在区块链上。当某个玩家被举报时,系统可以通过分析链上与该地址有关的所有历史操作,来确认该玩家的合法性,未通过合法性校验的玩家均会被系统以特定交易的形式标记到黑名单中。</li> <li>进度保存在传统的网络游戏中是通过服务器端的数据库来完成的。正如在前面的反作弊话题中提到的那样,我们完全可以以特定交易的形式把玩家的进度保存在链上。这样可能会有匿名性失效的风险,也就是通过给定的地址可以查到对应的玩家的进度。不过相应地,也可以通过地址池(同一个玩家同时维护大量有效地址)的技术来降低暴露的风险。而且正如普通交易的原则那样,每次交易总是使用一个新地址即可。</li> </ol> <h4 id="数据同步和更新">数据同步和更新</h4> <p>为了丰富的游戏细节,完全去中心化的游戏同样也会是需要一个胖客户端的,可以使用 torrent 或 magnet 技术来实现这一点。通过在创世区块 (Genesis Block) 中包含官方客户端的 magnet 链接,我们可以保证同一个游戏的客户端总是衍生自同一个源。每次客户端的更新,都可以通过一个包含增量更新包的 magnet 链接的更新交易来实现。这样我们不仅保证了更新的唯一性,跟传统的网游更新相比,还通过区块链消除了可能存在的篡改客户端植入木马等风险。此外,用区块链作为游戏的唯一官方认证的历史记录,还有一些好玩的用途,比如可以写个扫描脚本,自动生成游戏世界的编年史,呵呵。</p> <h4 id="非关键数据的本地化">非关键数据的本地化</h4> <p>前面我们提到玩家的进度可以保存在区块链上,然而有大量的数据很有价值却并非关键性数据 (如玩家的成就和邮件,也包括崩溃时的诊断信息等),这一类不影响游戏公平性的数据,我们可以从玩家进度中剥离出来,用 Git 存在本地,这也减轻了区块链的负担。用 Git 的好处是,我们可以很方便地整合一些基础的 Git 命令在客户端里,不费太大力气就有了 p2p 的推送功能。对于开发人员或 GM,能够随时直接在游戏内 git pull 任意一个玩家的诊断信息,是非常非常方便和高效的。</p> <h4 id="运营特点">运营特点</h4> <p>在运营方面,基于 P2P 的游戏,与服务器客户端的结构相比,有很多显而易见的好处,比如省去了维护服务器硬件(或租云服务器)的成本,也不会因为服务器宕机导致全服游戏中断。充值内购都可以直接以普通交易的形式来实现,不再需要与渠道分成。</p> <h4 id="decentralized-game-architecture-dga">Decentralized Game Architecture (DGA)</h4> <p>把前面说的这些串起来,就是下面的这个等式:</p> <p><img src="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/formula.png" width="748" height="182" srcset="https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/formula_huce5ef605d44fc6bbf5238e1b86e4b820_20219_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics/formula_huce5ef605d44fc6bbf5238e1b86e4b820_20219_1024x0_resize_box_3.png 1024w" loading="lazy" alt="formula" class="gallery-image" data-flex-grow="410" data-flex-basis="986px" ></p> <p>好了,俺的手也敲累了,这次就先这样吧。喜欢这篇文章就到知乎上为<a class="link" href="http://www.zhihu.com/question/27621853/answer/40488719" target="_blank" rel="noopener" >这个答案</a>点个赞吧 :)</p> <p>(全文完)</p> <hr> <p>[2015-02-27] 补:谢谢<a class="link" href="http://weibo.com/changjia" target="_blank" rel="noopener" >@长铗</a>君的<a class="link" href="http://weibo.com/1232750075/C5VXljaz8" target="_blank" rel="noopener" >勉励</a>:</p> <pre><code>“这篇文章是我见过的探讨游戏与数字货币结合的研究最深的,比如比特币作为游戏币,链上交易,玩家自行发行数字币,我想最现实的一个应用是,私钥绑定游戏用户账号,解决游戏开发商新游戏上线后难以找回种子用户的痛点。” </code></pre> <p>长铗君提到的很有趣的一点是**“私钥与用户而非特定的游戏绑定”**,也就是类似传统游戏中通行证的概念了。这个俺想了一下,比较直接的解决方案就是游戏厂商送硬件钱包,相当于招商银行一卡通,使用这张卡就能在该厂商的所有游戏里通存通兑。单一的私钥能定位到独立用户,每个新发行的游戏都生成一个对应的新地址,对账也很方便。</p> <p>安全方面,每次使用时先验证手机上的6位数字令牌,再用硬件钱包上的私钥签名。这样的话,如果硬件钱包遗失,可以通过手机找回;如果手机遗失或被植木马,没有私钥也无法动用资金。除非手机和硬件钱包同时丢失,否则应该问题不大。</p> <hr> <h2 id="修订历史">修订历史</h2> <ul> <li>2015-02-24 原文发布链接:https://gulu-dev.com/post/2015-02-24-bitcoin-and-online-game-economics</li> <li>2015-02-25 本文同时发于我在巴比特的<a class="link" href="http://www.8btc.com/author/5666" target="_blank" rel="noopener" >专栏</a>,谢谢 <a class="link" href="http://weibo.com/8BTC" target="_blank" rel="noopener" >@巴比特资讯</a> 帮助编辑。</li> <li>2015-03-21 本文已授权 GameRes 发布在<a class="link" href="http://www.gameres.com/327830.html" target="_blank" rel="noopener" >游戏策划</a>栏和<a class="link" href="https://mp.weixin.qq.com/s/DNN86AoELZa1247R1E8Iug" target="_blank" rel="noopener" >3月20日的公众号推送</a>。</li> </ul> 2015.02 “Abort,Retry,Fail?” - 也谈错误处理 https://gulu-dev.com/post/2015-02-03-error-handling/ Tue, 03 Feb 2015 19:14:00 +0000 https://gulu-dev.com/post/2015-02-03-error-handling/ <p><img src="https://gulu-dev.com/post/2015-02-03-error-handling/trying-failure.jpg" width="300" height="298" srcset="https://gulu-dev.com/post/2015-02-03-error-handling/trying-failure_hu7361ffc0a016b11643a17263b6feac7a_26973_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2015-02-03-error-handling/trying-failure_hu7361ffc0a016b11643a17263b6feac7a_26973_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="failure" class="gallery-image" data-flex-grow="100" data-flex-basis="241px" ></p> <h2 id="简要导读">简要导读</h2> <p>先说明一点,想看到俺讨论“返回值和异常哪种更好”的可以退散了(那是各种编程规范和团队负责人的事情)。本文是对于实践的表述和针对性思考,谈论的是日常开发中普遍面临的问题和对应的解决方案。</p> <hr> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Life is an error-making and an error-correcting process. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">...And if we can be ever so much better — ever so much </span></span><span class="line"><span class="cl">slightly better — at error correcting than at error making, </span></span><span class="line"><span class="cl">then we&#39;ll make it. </span></span></code></pre></td></tr></table> </div> </div><p>—— From <a class="link" href="http://en.wikiquote.org/wiki/Jonas_Salk" target="_blank" rel="noopener" >Jonas Salk</a></p> <hr> <h2 id="错误的相对性">错误的相对性</h2> <p>先来个地图炮吧,能在当前的抽象层次下 <strong>就地处理</strong> 的,就不是错误,而应被视为业务逻辑的一部分。</p> <p>这句话暗含的意思是,错误处理并非孤立的流程,而与现场的上下文密切相关。当前层次的错误处理,在更高的一层看来,极有可能只是业务逻辑的一部分。</p> <p>举个例子,即使你的程序因为错误宕机了,对于更高层次的操作系统来说,只是从容地杀掉进程释放资源而已,对于操作系统而言,这只是一个正常的业务处理流程;即使整个操作系统崩溃了,对于更高层次的使用者——用户——来说只是从容地重启操作系统并恢复工作的上下文而已(也是一个正常的业务处理流程)。</p> <p>错误处理是 <strong>相对的</strong>,是 <strong>上下文相关的</strong>,是与业务逻辑有机结合的。</p> <p>那么弄明白这一点有什么价值呢?</p> <p>理解了错误处理的相对性,我们就不会随手写出调用链上无数嵌套的 <code>if err != nil { return err }</code> 和反复的 catch &amp; rethrow,把同一个错误在系统内到处传播;而是在不同的系统层次上工作时,总是会针对性地结合当前的情境,审慎地编写符合上下文的处理逻辑。</p> <hr> <h2 id="错误的上下文敏感性">错误的上下文敏感性</h2> <p>在同一个系统内,对错误的处理要保持一致,不要随意混用各种不同机制,这样的话就不再多说了,这里我们谈一下针对不同上下文的不同敏感度的区分问题。</p> <p>在日常开发中非常普遍的是,对同一类错误始终使用了同样的处理方式,而大部分时候仅仅是出于惯性/惰性/复制粘贴,并不符合当时的情境。</p> <p>拿游戏开发举个例子,同样是一个重要程度一般的数据文件(如一个对话框内容的本地化后的文本)打开失败,如果是应用的初始化阶段,我们可能会直接停下来,提示用户程序的数据完整性可能有问题,并提示退出,下载和重新安装。这是一个合理的响应,因为我们不知道如果继续运行,还会有多少文件会受影响。但如果程序已经跑起来有段时间了,用户已经在里面付费并正在开心地玩耍了,这时候我们是不是还会冒冒失失地弹个框 “<em>数据文件已损坏,请重新安装</em>” 呢?合理的选择是,只要不影响游戏主流程的进行,就把错误默默记录下来,尽量让游戏继续流畅地运行吧,这就是上面提到的“<strong>编写符合上下文的处理逻辑</strong>”。</p> <hr> <h2 id="注意区分-expected-error-和-unexpected-error">注意区分 Expected Error 和 Unexpected Error</h2> <p>这两者有什么区别呢?前者是程序员明确知道有可能会发生的具体错误,通常会写下对应的错误处理代码(所谓的“白盒处理”);后者是程序员在编写代码时,对可能发生(但不知道具体是什么)的情况做出的处理(所谓的“黑盒处理”)。前者就不多说了,这里主要说一下后者。</p> <p>我们知道,Windows 应用程序如果一段时间内失去响应,无法处理窗口消息,Windows 就会弹出对话框提示用户,选择是否终止掉这个无响应的进程,这就是典型的 Unexpected Error Handling。明确考虑了这种错误,并系统性地处理的系统,比完全没有考虑的裸奔系统有着更高的健壮性和可靠性。</p> <p>正常的业务逻辑处理流程中,对未期望错误的处理这种需求也不少见。比如事务性的操作在 Rollback 的过程中,经常是需要保证不能失败的(可能会造成递归 Rollback 导致栈溢出)。再比如一些 delegate 出去的回调函数(调用时无法得知被调用的函数具体干了些什么),有时不仅要求不能抛出异常,而且一定要在给定的时间内完成,否则就会失效。</p> <p>对于C#这种具有一个单一的异常基类 (System.Exception) 的语言,有时我们经常看到一些代码懒得按具体异常一个一个处理,直接全部捕获了事的代码,如下所示:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">try</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="c1">// lines of business logic </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="n">catch</span> <span class="p">(</span><span class="n">Exception</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// error-handling </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这种“一网打尽”型的处理方式就是典型的混淆了 Expected Error 和 Unexpected Error 的处理。</p> <p>这种代码有几个问题,首先,因为条件太过宽泛而不可能做任何具体而有效的处理(除非在内部再根据类型展开);其次,当真正的未期望错误的处理来临时,几乎可以肯定的是,这种临时针对具体业务不会去一一考虑和处理(那样的话就太冗长了);总得来说,这种通吃型代码就像一个黑洞,把路过的所有异常全部收入囊中。如果一个程序中这种黑洞越来越多,不受控制,工程质量就很堪忧了。请注意,这里我不是在说 <code>catch (Exception) {}</code> 不好,尽量不要用,而是一定要知道自己在干什么,审慎地使用。</p> <hr> <h2 id="注意区分受控环境和非受控环境">注意区分受控环境和非受控环境</h2> <p>这两者有什么区别呢?一个较大规模的项目,代码库往往包含了各种平台组件、第三方库、工具和服务等等。在这一体系中,定义一个的明显的受控边界是很重要的。如果项目本体和第三方的代码任意交织,随意调用而不加约束,那么出了问题时,将很难确认是自己的问题还是别人的问题。打个比方,如果你的项目中不允许使用异常,那么你会允许一个可能随时抛出异常的第三方库在自己的代码中被随便调用吗?</p> <p>在受控环境中,我们很容易以标准的形式去统一地定义和处理错误。不管是选择使用一张全局共享的错误码表,还是从一个标准的异常基类派生出携带各类信息的异常类体系,都能比较容易地维护一致性。而非受控环境,则往往用于跟各种不同的代码库打交道,应该以尽量考虑服从对方的需求为主,仅在必要时考虑与受控环境内进行传播或转换。有些情况下,这取决于本体对这个外部组件是接口依赖还是实现依赖。相对较重的接口依赖出于方便,往往就不再转换了。</p> <p>关于边界的定义实际上是一个很紧要的问题,对实际的工程质量影响很大。举个例子,如果在 Host Runtime 上跑了一个 DSL,那么 DSL 的运行错误要如何通知 Host 呢?是不加选择地直接转换成 Host 这边的异常并随时抛出吗?是发一个错误消息后继续执行吗?需要暂时中断并等待处理结果吗?这些问题如果不考虑清楚,并定义明确的规则和边界,整体的工程质量就很难保证。</p> <hr> <h2 id="不要混淆防御性编码和错误处理">不要混淆防御性编码和错误处理</h2> <p>先来看下面几段代码:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span><span class="lnt">32 </span><span class="lnt">33 </span><span class="lnt">34 </span><span class="lnt">35 </span><span class="lnt">36 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// 下面三个函数都是类 SmartFile 的成员函数 </span></span></span><span class="line"><span class="cl"><span class="c1">// 假设 m_file 是该类的成员变量 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="c1">//----------------------------------------------------------------------------- </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">SmartFile</span><span class="o">::</span><span class="n">foo_A</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">assert</span><span class="p">(</span><span class="n">m_file</span><span class="p">);</span> <span class="c1">// 以断言方式确认 m_file 必须有效 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// 敏感度最高,如果不满足假定 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">m_file</span><span class="o">-&gt;</span><span class="n">Read_A</span><span class="p">();</span> <span class="c1">// debug -&gt; 在断言处中断执行流程 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// release -&gt; access violation crash </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="cm">/* 其他逻辑 */</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">//----------------------------------------------------------------------------- </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">SmartFile</span><span class="o">::</span><span class="n">foo_B</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">m_file</span><span class="p">)</span> <span class="c1">// 以 pre-condition 方式确保 m_file 必须有效 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span><span class="p">;</span> <span class="c1">// 敏感度一般,不满足假定则整体跳过该函数(不被当做错误) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="n">m_file</span><span class="o">-&gt;</span><span class="n">Read_B</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="cm">/* 其他逻辑 */</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">//----------------------------------------------------------------------------- </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">SmartFile</span><span class="o">::</span><span class="n">foo_C</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">m_file</span><span class="p">)</span> <span class="c1">// 以最小保证为原则,仅在必要时保证 m_file 的有效性 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">m_file</span><span class="o">-&gt;</span><span class="n">Read_C</span><span class="p">();</span> <span class="c1">// 敏感度最弱,如果不满足,仅相关语句不执行 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="cm">/* 其他逻辑 */</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这几段代码,每种都是日常开发中经常见到的写法,而且在注释中我们可以看到,每一种都有一定的合理性去支撑。可如果在实际项目中不考虑一致性,这几种混着用,维护的人就会很抓狂——SmartFile 这个类到底能不能保证 m_file 在用到时的有效性?如果不能,那什么情况下会无效?如果需要使用而又正好处于无效状态中,应该重置已有对象还是创建新的对象?</p> <p>在实际工程的庞大业务逻辑中,这些问题往往都不是一眼能看出来的。如果带着这些疑问去写新的代码,轻则会写出很多冗余的判断逻辑,重则很难保证不会破坏原来编写者的初衷和假定。这样随着项目的推进,由于维护者对可能的状态缺乏了解,代码逻辑都会逐渐演变为“碰巧能工作”(happens to work),整个工程活动很快就会演变成“靠偶然编程”(programming by accident)。</p> <hr> <p>什么是 programming by accident?</p> <p>——连续调用 A B C 无法正常工作,哪位兄弟看一下? ——调一下 B A C 试试? ——还是不行,那调 A B C C 试试? ——好了?嗯,那就提交吧,收工!</p> <hr> <h2 id="在明显未做到异常安全的环境中不要使用异常处理">在明显未做到异常安全的环境中,不要使用异常处理</h2> <p>这一条似乎是不言自明的,可是实践中我们还是看到了太多的反例,其中尤以 (之前提到的) 不负责任地“裸调”第三方接口为甚。</p> <p>看这段代码:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">critical_business_logic</span><span class="p">(</span><span class="n">LPCRITICAL_SECTION</span> <span class="n">cs</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="o">::</span><span class="n">EnterCriticalSection</span><span class="p">(</span><span class="n">cs</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">g_p3rdPartyLibInterface</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">g_p3rdPartyLibInterface</span><span class="o">-&gt;</span><span class="n">RunProc</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="o">::</span><span class="n">LeaveCriticalSection</span><span class="p">(</span><span class="n">cs</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>也许这段代码一直以来都运行得很好,经历过各种线上的考验。但有经验的程序员,看到这段代码的第一时间就会皱起眉头。是的,也许 g_p3rdPartyLibInterface 这个第三方对象上的 RunProc 函数现在工作得很好,但谁能保证在以后维护升级的过程中它始终保证不抛出任何异常?一旦有异常抛出,层层 unwind 直至 Unexpected Error Handling 逻辑处(如果有的话),业务逻辑早就千疮百孔了,能继续跑下去的可能性低到接近零,这个错误就自然退化成一个 application failure 了。</p> <hr> <p>问题很严重,修改起来却很简单。 理论上最好的情况是,大家都能自觉做到编写 exception-safe 的代码,把那些需要显式释放的资源通过 RAII 交给析构函数去做 (对应 C# 的 finally 和 Go 的 defer 等类似机制),这样万一因为更新第三方库什么的,发生了没想到的异常,各种资源还是会井然有序地释放,不会产生破坏性的后果。 如果无法做到 exception-safe 那至少应把第三方库的调用套一层 wrapper,处理各种 Expected Error 和 Unexpected Error,保证不会有异常溜出来。</p> <hr> <h2 id="如果一段代码看起来做到了异常安全注意新的代码不要破坏这种安全性">如果一段代码看起来做到了异常安全,注意新的代码不要破坏这种安全性</h2> <p>这一点和上一点是孪生命题。不要主动写一些破坏异常安全的代码,简而言之,是因为“部分的异常安全”基本上等同于“零异常安全”。尽量维护已经建立起来的异常安全性,一方面是为了不产生 corrupted object (也就是部分字段在 unwinding 时正常清理而新增部分不能),另一方面也是对可能抛出的点考虑和准备得更充分,避免无意中新增的逻辑在 unwinding 的过程中被无意中跳过。</p> <hr> <h2 id="如何优雅地解决忘了处理返回值这个问题">如何优雅地解决“忘了处理返回值”这个问题</h2> <p>在日常开发中,不管团队的水平如何,“忘了处理返回值”都是时常会发生的错误。各种编程指南编程修养什么的已经把“不要忘了检查返回值”说过很多遍了,有的甚至上升到人品的高度,说什么“不检查返回值的程序员都是xxx”之类让人无语的话。可是,靠程序员的记忆,习惯和修养真能保证不出问题吗?我的看法是,这个,基本上,很难。至少我自己,在看自己以往写得代码的时候,不止一次发现某个重要调用没有检查返回值,不禁后背一凉直冒冷汗,哆嗦着补上。实践证明,靠人是靠不住的,尤其是在高密度的脑力活动过后,疲劳时,人脑的可靠性会进一步下降。</p> <hr> <p>那么这个问题应该怎么处理呢?</p> <p>有同学的第一反应是“用异常吧”,不过结合俺上面的几条来看,这种属于大手术了,弄不好还是伤筋动骨级别的。再一个,仔细地推敲一下,异常只是保证了“在运行时抛出后,如果不响应,将一直 unwind 到程序退出为止”,并不能保证“程序员会主动编写相应的响应逻辑”,况且“没处理的异常”造成的伤害可能比“没检查返回值”还要大,很显然改异常往往是行不通的。</p> <p>一个看起来呆板了点(但是实践上比较有效)的办法是,把错误的返回值设计为一个指针型的输出参数,如下所示:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">this_is_so_important_that_the_error_must_be_checked</span><span class="p">(</span><span class="n">eErrorType</span><span class="o">*</span> <span class="n">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">assert</span><span class="p">(</span><span class="n">err</span><span class="p">);</span> <span class="c1">// 或其他机制保证 err 有效 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">bad_things_happen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="o">*</span><span class="n">err</span> <span class="o">=</span> <span class="n">ERR_something_is_wrong</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="o">*</span><span class="n">err</span> <span class="o">=</span> <span class="n">ERR_ok</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>为什么这个方案在实践中比较有效呢?这利用了一点心理学知识——不管是有意无意的忽视,还是选择性懒惰,亦或仅仅是遗忘也罢,都是起源于“函数的返回值可以被调用方忽略”这一事实。如果我们把它搞成<strong>指针形式的输出参数</strong>,那想要调用成功,不仅需要定义一个 <code>eErrorType err;</code> 形式的变量,而且需要以略不舒服的**“传地址”**方式 ( <code>&amp;err</code> ) 传进去。这就相当于显式地提醒了调用者——“别忘了检查这个变量哦”。程序员也会考虑性价比的,如果已经“当当当”多敲了一行变量定义和一个别扭的参数,接下来要是不允许他顺便检查一下这个值,他们会觉得很吃亏,而且会觉得如芒在背,浑身不自在的。嗯,那我们的效果就达到了。</p> <p>有同学会说:“写这种天怒人怨的接口,会被愤怒的程序员拖出去吊打的……有没有看起来正常点儿的办法?”</p> <p>嗯,一个温和一点儿的办法是,当发生错误时,总是把错误关联上出问题的时间戳/线程id/进程id等信息,通过该系统的某个接口发送到一个收集错误的容器,此容器内保存了一段时间内该系统发生的各种错误,必要时还可以写到数据库去。调用方可以在需要的时候去检查该系统的错误情况。</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">this_is_so_important_that_the_error_would_be_collected_into_an_error_container</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">assert</span><span class="p">(</span><span class="n">theErrorContainer</span><span class="p">);</span> <span class="c1">// 或其他机制保证 theErrorContainer 有效 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">bad_things_happen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">theErrorContainer</span><span class="p">.</span><span class="n">AppendNewError</span><span class="p">(</span><span class="s">&#34;Oops!&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="cm">/* 其他逻辑 */</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这个机制的好处是,一旦发生错误,除非显式丢弃,否则不会丢失。尤其适用于并发的异步调用,或没有副作用的同步调用。检查密度也可以按需控制,必要时每调必查,无所谓时可过段时间再查一下。</p> <p>有同学又会说:“这个虽然错误丢不了,可是要是自始至终完全忘了检查那个错误容器了该咋办咩?”</p> <p>好说好说,说话间俺又掏出一个锦囊,上书金光闪闪的“回调”二字,拆开定睛一看,只见上面蝇头小楷密密写道:“发起调用前,需要先注册错误处理函数(否则调用直接报错退出),出错时调之即可。”</p> <p>这样做的好处不仅是用一个函数把每次调用都要写的错误处理逻辑收拢到了一处,而且还可以用来控制发生错误时的控制流,如使用该函数返回一个 boolean 来决定,是忽略错误继续执行,还是直接退出结束本次调用。</p> <p>嗯,关于“忘了处理返回值”,这一次就先说这么多吧。</p> <hr> <h2 id="如何有效地处理应用程序的-crash">如何有效地处理应用程序的 crash</h2> <p>看到这个话题,有同学可能会说,crash 了生成 core dump,然后把当时的 log 和其他上下文信息通过错误收集工具发到开发者的服务器上,再使用自动化工具根据 callstack 的调用链分类排序,开发者就可以调试和诊断问题了。</p> <p>嗯,不错,这是教科书式的标准做法,俺点个赞先。</p> <p>考虑下面两种情况:</p> <ol> <li>在即将上线的版本里,存在一个非常非常低概率重现的宕机问题(让我们假定在开发周期中只发生过一两次,这个假定用以确保你没有机会去验证针对这个问题的猜测和修复是否真的有效),你缺乏足够的信息去确定问题的精确位置,但从事发时的线索可以把范围缩小到某一片代码(这些代码是你不能去掉或关闭的)。那么此时,你的合理的措施是什么?</li> <li>你在已经上线的项目中使用了某个外部的第三方服务(假定叫 xyz 服务),该服务除了线上的 API 接口外,还提供了本地的动态链接库文件,用以帮助你的应用程序与其对接。现在该服务升级到了一个新的版本,也同时提供了对应的 dll 给你。你按照往常那样迁移到新版本之后,过了半个月,陆陆续续地收到某几个玩家发来的宕机报告,你一看,问题出在新替换的 dll 里。这时你面对的两难是,你已经无法回到老的版本了(因为依赖了 xyz 的新版本的服务),而从少量的样本中又很难推敲出问题出在什么地方,于是你赶紧发邮件与 xyz 公司沟通,第二天 xyz 公司悠悠地回复了你,咦,没有听说别的 licensee 出现类似的问题哦,要不你们再查查,是不是姿势不对,操作系统没打补丁?眼看宕机报告积少成多,那么此时,你的合理的措施又是什么?</li> </ol> <p>在实践中,任何项目都很难严格保证所有的代码没有任何问题(这也是我们需要 Unexpected Error Handling 的根本原因)。在这种情况下,为了把不幸发生问题时的不良影响降到最低,我们可以在任何我们认为没有把握的代码 (包括新写的扩展模块,集成的第三方库等等,可以认为它们约等于上面提到的非受控环境) 上,加上一个较强的保证,这样在万一发生问题时,得以让程序继续以受限的方式运行或体面地退出。对于游戏服务器而言,不在 core dump 的第一时间崩溃,不仅避免了直接造成巨大的负面影响,也可以有机会通过发紧急公告和把未被破坏的重要信息入库等手段,降低宕机带来的损失。</p> <hr> <p>那么具体应该怎么处理这一类 crash 呢?</p> <p>这里以 Windows 平台上的 C/C++ 为例,简单说一下。大部分所谓的宕机,实际上是操作系统抛出的 OS 异常,在 Windows 平台上,可以用 <code>__try {} __except () {}</code> 的结构化异常处理机制来捕获并处理的。比如几个最常见的 0xC00000005 Access violation, 0xC00000094 Integer division by zero, 0xC000000fd Stack overflow 等等,已经能囊括最常见的 90% 以上的宕机了。具体的处理很直白,这里就不贴代码了,在<a class="link" href="http://support.microsoft.com/kb/315937/en-us" target="_blank" rel="noopener" >这里</a>可以看到一个处理栈溢出并恢复执行的例子。</p> <hr> <p>以下几点补充说明一下:</p> <ul> <li>对应这一类保护过的模块,通常我们都会制作一些运行时开关,当发生问题时,直接用开关把对应的子系统关掉,这就是所谓的“以受限的方式运行”。</li> <li>在这里添加的宕机保护,并不会妨碍生成需要的 dump 以便开发者诊断,实际上只要拿着当时的异常指针,就可以按需生成 dump 文件。</li> <li>说到 dump 顺便说一下,dump 并非是只有在宕机时才可以生成,实际上程序运行的任何时候,都可以使用 dbghelp.dll 来生成 dump,在某些情况下,在问题相关的关键点上生成一些额外的 dump 非常有利于对问题的诊断。</li> </ul> <hr> <p>本拟再简单聊一下 golang 和 Rust 等新近语言在错误处理方面的考虑,考虑到本文的长度,还是且听下回分解好了,这次先这样吧。</p> 2014.12 开发效率与执行效率,我们应该怎样斟酌? https://gulu-dev.com/post/2014-12-14-efficiency-tradeoff/ Sun, 14 Dec 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-12-14-efficiency-tradeoff/ <p><img src="https://gulu-dev.com/post/2014-12-14-efficiency-tradeoff/efficiency.png" width="630" height="325" srcset="https://gulu-dev.com/post/2014-12-14-efficiency-tradeoff/efficiency_hu88f0756f70ca5bba3795040efb205e71_142030_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-12-14-efficiency-tradeoff/efficiency_hu88f0756f70ca5bba3795040efb205e71_142030_1024x0_resize_box_3.png 1024w" loading="lazy" alt="efficiency" class="gallery-image" data-flex-grow="193" data-flex-basis="465px" ></p> <p>这是几天前在知乎上看到的问题,切入点还蛮有趣的,试着答一下吧。</p> <ul> <li><strong>开发效率与执行效率,我们应该怎样斟酌?</strong><a class="link" href="http://www.zhihu.com/question/26992395" target="_blank" rel="noopener" >题目链接</a></li> </ul> <h2 id="简单分析">简单分析</h2> <p>老规矩,先简单分析一下题目:</p> <ul> <li>开发效率通常反映在两点上:1.开发新功能是否迅速;2.修复缺陷是否及时。</li> <li>所谓运行效率,通俗地说就是(生产环境下)系统运行速度快,平均响应时间短,均匀流畅。不管是功能上还是用户体验上,都没有不必要的卡顿和等待。当因为某个因素性能开始恶化时,恶化的情况能随该因素的增强而有一定的收敛。</li> </ul> <p>既然谈到取舍问题,显然题主已经预设了“二者之间至少存在一定程度上的负相关”这一隐含假定。那么这二者是否的确是此消彼长的对立关系呢?俺觉得还是值得讨论的。下面俺将分别从 a) 项目类型和开发模式 b) 产品质量和工程质量 c) 项目管理和团队建设 这三个角度具体地谈一下。</p> <hr> <h2 id="项目类型和开发模式">项目类型和开发模式</h2> <p>在项目中前期,通常可以清晰地通过以下几点来了解项目的基调和开发模式:</p> <ol> <li>项目的大致开发周期和预期的团队规模</li> <li>行业内同类型项目的横向对比</li> <li>技术骨干的经验和背景</li> </ol> <p>有同学会问,(作为团队一员),这些因素虽然不难了解到,可是跟咱们讨论的开发效率和运行效率有什么关系?俺要说,这个关系可不小。项目开发中的很多变量,从立项的第一天起就被初始化成常量了。俗话说“兵熊熊一个,将熊熊一窝”,一个团队的战斗力高低和能量释放程度,先天的基因是一个很重要的因素(一不小心掉到基因决定论里了),下面我们具体看一下。</p> <hr> <p>对于强调快速迭代,小步前进的敏捷团队而言,通常项目规模也不会很大,复杂度也会较行业内同类型项目为低,如果能适当地辅以一些局部的深入挖掘和特色创新,就可以形成一定的差异度和竞争力了。</p> <p>这种项目通常不具备压倒性的优势,也很可能(在初期)缺乏足够的市场推广资源,这种时候,<strong>相对较高的开发效率和迅速灵活的反应能力就成了生死存亡的关键</strong>。在这种情况下,对于技术骨干而言,最重要的素质在于<strong>审慎地控制复杂度</strong>,绝大部分情况下不要主动选择花样作死,避免在无谓的细节上纠缠不清,尽量采用业界惯常做法(必要时可适当简化)。 通过设计和流程上的管控 (而非细枝末节的优化),让运行时的性能始终保持在可控的安全区域以内,是省心省力且比较有效的手段。回过头来比较开发效率和运行效率,孰轻孰重,孰易孰难,就不难权衡了。</p> <hr> <p>对于大型组织内的大型项目,就是另一番情景了。</p> <p>先说结论,大型系统内的开发效率和运行效率相干性并不高,通常都与参与者的“整体上对系统的熟悉程度”和“问题域的相关经验”严重相关。</p> <p>为什么这么说呢?跟每个开发人员都得独当一面的小团队不同,大型组织内的成熟系统,往往有着相对较细致的团队分工和相对完善的开发流程。整体而言,大型组织的开发效率<strong>总是相对偏低</strong>是很正常的,正如越宽阔的江河流速越缓慢是一个道理。</p> <p>再来看运行效率,由于庞大的系统有着巨大的惯性,大大小小的框架在经年累月的开发积累中已然成型,内部隐含着往往是海量的“不足为外人道”的专门知识和方案(以及大量的 workarounds 和 hacks),在这种情况下,哪怕是理论上非常优越(架构级和算法级)的优化,实践中都有可能被系统中的各种细节牵跘,以致无法达到预期的效果,有时甚至还会出现导致性能下降的咄咄怪事。</p> <p>总得来说,对于大型系统而言,开发效率和运行效率<strong>相关性并不显著</strong>。对于主导者而言,由于比小团队拥有多得多的资源可供调配,这两者不再是孤立的因素,而是需要从整体上去协调,规划和取舍的诸多因素之二。对于参与者而言,“整体上对系统的熟悉程度”比“有更多的学院派的理论和技巧”在日常开发中更具价值;“问题域的相关经验”比“写代码手速高/能刷ACM”在招聘求职时更有参考意义。</p> <hr> <h2 id="产品质量和工程质量">产品质量和工程质量</h2> <p>在工作中,我们经常会听到有人这么说:“xxx做功能很快的,三天一个模块,五天一个系统” 云云。其实我从小就很羡慕这样的同学,他们头脑清晰,思维敏捷,见机果断,手脚麻利,再复杂的问题丢给他们,也能须臾间找到要害,让你不得不拍腿感叹:“硬是要得!”。而我自己则是一个反应有点迟钝木讷,经常慢半拍的人。同样是一件事,我经常要回过头慢慢想想,才能弄得明白来龙去脉。</p> <p>后来随着年岁渐长,我慢慢懂得,快有快的优势,慢却也有慢的好处。在日常开发中,一味图快,唯快不破,速度至上,往往会导致工程质量的低下。更有一类开发团队,打了鸡血一样生冲硬突,其实只是在弥补和掩饰指挥者内心的不安,慌张,焦虑和信心不足。</p> <hr> <p>摘录两段纯银老师的话以增兴味吧:</p> <pre><code>我们都知道,产品并不是功能越多越强就越有竞争力。你拼命加班,飞快迭代,发布各式各样的型号版本,把自己搞得鸡血沸腾,但最终决定胜负的并不是速度,而是精度。拿我很钦佩的Instagram举例子,至今不开发Android版也不去完善网页端,平均一两个月才更新一个版本,不到一年用户数已经突破了700万大关!故而产品的理念与方向,比速度与激情更重要得多——但我看国内很多团队就知道抄,东抄西抄,自己的想法很少,就算有想法也往往是“抄这家”“抄那家”的贪多求全。这样的6X12,6X14又有何意义呢?真没见过几款产品单单靠“抄得快”“抄得全”就能成气候。勤不仅不能补拙,还有可能造成设计过度与产品失衡,结果越补越拙…… </code></pre> <p>&ndash; 出自 <a class="link" href="http://firecacada.blog.163.com/blog/static/7074376201173101010942/" target="_blank" rel="noopener" >唯快不破?</a></p> <pre><code>这个行业对速度的无条件信仰,已经到了病态的地步。不计其数的例子证明,市场表现最好的那个创新者,并不是同类中的第一个,甚至未必是前十个,也未必是版本更新最勤快的那个,而是产品最具感染力的那个。但大家还是在头上绑着「唯快不破」的布条向前冲锋。「快」的确是个好事,却不是万试万灵的真理。快速发布和产品感染力之间没有必然联系,如同多试错和找准路之间也没有必然联系。在创新市场强调速度,本质上是用体力支出来弥补信心不足。但体力并不是唯一的竞争项目。 </code></pre> <p>&ndash; 出自 <a class="link" href="http://firecacada.blog.163.com/blog/static/707437620126124237400/" target="_blank" rel="noopener" >杂念·6月(下)</a></p> <p>当然了,纯银老师更多的是在谈产品,这里我们为免偏题,只谈技术。</p> <hr> <p>俺认为,<strong>良好的设计决定了系统的基因</strong>。在设计良好的系统中,干正确的事情比干错误的事情更容易,更快;在恰当的场合提供方便的脚手架,让开发和测试更便利;良好的解耦和内聚,让开发者在局部改动中可以“从心所欲而不逾矩”。充斥不良设计的系统中,开发人员会觉得处处掣肘;再小的修改也会时刻担心影响到完全不相干的东西;不仅新功能难以快速实现和验证,优化同样难以实施,或勉强实施后实际效果与预期相差甚远。两者之间,开发效率和运行效率可谓天差地远。</p> <p>其次,<strong>为团队提供所能提供的最好的工具</strong>。工具的作用不应被低估。更好的硬件,更短的编译时间,甚至更舒适的椅子,都会让开发效率和工程质量提高不少,这方面的研究和结论非常多,我也就不再赘述了。软件开发是一项以人为核心的活动,“在开发人员身上节省项目成本”是我能想象到的最铺张浪费的管理行为。</p> <p>除此之外,俺还在实践中体会到一个心得:**深思熟虑后一气呵成的模块,反而会有相对较高的质量,不太会出问题;那些一直以来没有想通想透,虽然看起来功能运作良好,但仍需反复修改调整的系统,在将来只会需要更多的修修补补。**这一点王垠同学在 “半年来的工作感受” 和 “谈&rsquo;测试驱动开发&rsquo;” 这两篇文章中 (王垠同学的 blog 最近无法访问,感兴趣的同学可以自行搜索),也有类似的有趣表达。我选择了一些摘录如下:</p> <pre><code>……(前略) 在 Google 的六个月里,我无视同事对于测试的要求,从无到有的做出了如此精密的系统,一个测试都没有写照样做得好,为什么呢?因为我的代码非常的简单清晰,我随时都可以把它们完整的呈现在头脑里面,从而让“心灵之眼”可以看到可能出现的错误。也许这就是所谓的“逻辑思维”。 对测试过分依赖的人,往往不具有这样的思维能力。他们不能够看到代码最简单的本质,所以需要做很多试探,以求达到“近似解”。为了不至于偏差很多,就写很多测试,用以捕捉和防止每一次的错误。这就像一个初学画画的人,一点一点的描,用橡皮反复的擦,可总也抓不住事物的精髓。 ……(中略)…… 当我的代码需要大量的测试才能确保正确的时候,那就是它该被推翻重写的时候。所以我的代码往往没有任何补丁和变通,可以说是无懈可击。这就像是一个真正会画画的人,他闭目沉思,然后一气呵成。当然,优美的代码并不是一蹴而就的,有的代码被我推翻重来几十次才最后成功,但我最后的代码不留下丝毫错误的痕迹。所以我觉得,看一个程序员的水平,不要看他留下来多少行代码,而要看他删掉了多少行。 …… -- 出自 “半年来的工作感受” (王垠) </code></pre> <p>是的,真正的优美源于简单和优雅(Simplicity &amp; Elegance),这是我与上面摘录文字的最大共鸣,也是为什么软件开发除了作为工程活动的一面之外,还展现出类似于园艺活动那样的艺术特质。总得来说,俺认为,真正的开发效率来源于对问题域的深刻认识,这绝非一朝一夕之功,也绝非某个&quot;绝顶&quot;算法的花拳绣腿,或某种“技巧性”的实现所能涵盖。我们真正欣赏和追求的高效开发,应该是庖丁解牛式的深厚技艺中举重若轻水到渠成的“快”,而非赶鸭子上架的加班赶工中匆匆写就的勉强可以运行的代码。</p> <pre><code>美的程序不可能从修修补补中来。它必须完美的符合事物的本质,否则就会出现上面的情况,有许许多多无法修补的特例。程序员跟画家其实差不多……(中略)……在修改代码的时候,你必须用“心灵之眼”看见代码背后的事物。这也是为什么很多高明的程序员不怎么用调试器(debugger)的原因。他们只是用眼睛看着代码,然后闭上眼,脑海里浮现出其中信息的流动,所以他们经常一动手就能改到正确的地方。 -- 出自 “谈'测试驱动开发'” (王垠) </code></pre> <hr> <p>所以写出简单正确的代码,远比写出运行效率高的代码要重要得多。在保证正确性的前提下,一个系统越简单,当遇到瓶颈时改善它的性能就越容易。而且程序员工作时的心情和效率,与代码质量关系很大。对比曾经历过的项目,我发现:在结构良好,流畅整洁的代码上工作时,往往心情愉快,能持续高效地产出所谓“优美的”代码(逻辑漏洞少,性能较好);在混乱(甚至“肮脏”)的代码上工作,往往很难鼓起勇气去修改,强忍着草草了事,代码质量也偏低,bug不断。</p> <p>在工程质量角度,最后再补充一句略有“负能量”的话吧,如果不顾团队的具体情况,不能切实可靠地制定方案,一味贪多务得,疲于追求开发效率,那么再好的 codebase 都会很快地腐烂。而且这个过程通常是<strong>不可逆</strong>的——想把好代码弄糟很容易,把糟代码弄好可就难了。当代码糟到一定程度时,优秀的开发人员就会不可阻挡地离去,那么离游戏结束也就不远了。</p> <h2 id="项目管理和团队建设">项目管理和团队建设</h2> <p>现在我们假设开发团队有 A, B 和 C 三种人,其中 A 是经验非常丰富的领域专家,B 是正处于当打之年的程序员,C 是经验不足但有成长欲望的新人。那么如何搭配才算是比较理想的比例呢?</p> <p>虽然不同的项目情况相差非常大,但我在实践中慢慢体会到,一个相对比较理想的比例大致是 1:2:3。一个 A 掌好舵,保证你的方向,两个 B 是你的左膀右臂,保证你的速度和力量,三个 C 是你的生力军,保证你的潜力和活力。保持一个相对平衡的团队结构,从长远上总是比堆人的效率高得多。另外再说一下,这是一个非常非常主观和粗略的估计,这么简单粗暴地给个数字,对于具体的项目情境通常是不靠谱的,还需要负责任的指挥者去量体裁衣,以免削足适履。</p> <hr> <p>想必大家也已经看出来了,决定这一系统的关键就是 A 的质量。而往往由于 A 又要肩负一定的沟通(主要指对外部团队和需求),行政(对组织而言)和管理(对内部团队)的责任,无法做到全身心地投入在开发上,发挥的作用往往要打一个折扣。这一点,即使是公认战力超高的卡神亦难免俗,卡神在 Rage 发布前夕 (2011年8月,ZeniMax 收购了 id software 两年后) <a class="link" href="http://www.gamasutra.com/view/feature/6461/carmack_on_rage.php" target="_blank" rel="noopener" >说了如下一番话</a>:</p> <pre><code>If anything, since the ZeniMax acquisition, it's been great. I don't even have to pretend to be an executive anymore. I don't have to go to board meetings. I don't have to do anything! I can just sit in my office and work. My core is defined as being an engineer. I take resources and a goal, and I try and put them to the best use to get us there. That's what I do. I don't want to be doing anything else. 如果说 ZeniMax 的收购改变了什么的话,到目前为止都挺好的。我再也不必假装是个管事儿的了。不需要再参加董事会议,也不必“不得不”做什么事儿了,我终于可以踏实地坐在办公室里干活了。 本质上我是一个工程师。给定一些资源和一个目标,我会尽可能地找到最理想的运作方式来实现这个目标,这就是我想做的,除此之外的其他事情我毫无兴趣。 </code></pre> <p>我想,除了少部分比较幸运的同学,对大部分技术专家而言,在现在以及未来很长的一段时间内,不用考虑商业上的妥协,能完全按照理想的步调和节奏去完成一部心目中的佳作,仍只是一个遥远的梦想。这大约就是所谓的“人在江湖,身不由己”罢。</p> <hr> <h2 id="写在最后">写在最后</h2> <p>杂七杂八地说了这么多,到最后已经有点歪楼了。那么放几句应景的话,与大家共勉吧。</p> <hr> <p>“当你被迫把东西做得很简单时,你就被迫直接面对真正的问题。当你不能用表面的装饰交差时,你就不得不做好真正的本质部分。”</p> <hr> <p>「當你擁有的資源越少、擁有越少的時間去思考,這些限制才能夠逼你作出真正關鍵的決定。而不是讓你在思慮周詳之後作出一個廢物。」</p> <hr> <p>「尝试将有用的人从杂音中分离出来,将有意愿帮忙的人从一大堆无聊评论中分离出来是很难的。在Linux 8086项目中我的确错误地放弃了这一目标,如何将那些只会空谈而又无所事事的人弄走是一门学问。」</p> <hr> <p>“只有在成为某个领域的专家之后,你才会听到心里有一个细微的声音说:“这样解决太糟糕了!一定有更好的选择。”不要忽视这种声音,要培育它们。优秀作品的秘诀就是:非常严格的品味,再加上实现这种品味的能力。”</p> <hr> <p>&ldquo;The physics of software is not algorithms, data structures, languages and abstractions. These are just tools we make, use, throw away. The real physics of software is the physics of people.&rdquo;</p> <hr> 2014.11 Surface Pro 3 上手体验 https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/ Sun, 30 Nov 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/ <p><a class="link" href="https://gulu-dev.com/post/2014-11-16-open-world" target="_blank" rel="noopener" >上一篇</a>弄得有点长,有成为话唠之嫌。这一篇紧凑点,字少图多才是王道啊。</p> <hr> <p>Surface Pro 3 比它的前两代明显多了不少好评。俺年初就把它扔进了关注列表里,前段时间趁着双十一时亚马逊的折扣收了这货。用了段时间,写个小结,也是给感兴趣的同学一个参考。</p> <p><img src="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_0_overall.jpg" width="954" height="632" srcset="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_0_overall_huc919c9fd3651a47d6da99068e5bc0f0a_128276_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_0_overall_huc919c9fd3651a47d6da99068e5bc0f0a_128276_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="sp3" class="gallery-image" data-flex-grow="150" data-flex-basis="362px" ></p> <hr> <p>喜欢<a class="link" href="http://necromanov.wordpress.com/" target="_blank" rel="noopener" >旗舰(墙外)</a>的评论方式,打分制清晰好看,俺就东施效颦一下。</p> <p><strong>起评分 80</strong></p> <ul> <li>硬件上虽与 iPad 的浑然一体有一定差距,但工艺也可当得起“考究”二字。</li> <li>兼容性好,日常使用没有遇到不兼容的 Windows 程序和游戏。</li> <li>软硬契合度不如 iOS;系统的 App 质量偏低,有凑数之嫌。</li> <li>触控体验跟 iOS/Android 相比做了很多有趣的创新和尝试,偶尔会感到操作有点突兀,不够直观和舒适。</li> </ul> <p><img src="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_1_apps.jpg" width="800" height="502" srcset="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_1_apps_hufd836ef9b78603cf94ca1a5a9868a4b7_56261_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_1_apps_hufd836ef9b78603cf94ca1a5a9868a4b7_56261_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="sp3" class="gallery-image" data-flex-grow="159" data-flex-basis="382px" > 日常工作环境下的软件都可以在 Surface Pro 3 下正常运行。</p> <hr> <p><strong>加分项</strong></p> <ul> <li>(+3) 键盘手感良好,击键体验值得一提,跟普通的蓝牙键盘相比舒适度要好上不少。</li> <li>(+3) 拿笔写的时候,手掌和手腕放在屏幕上,不会影响书写,比 iPad 强太多了。</li> <li>(+2) 屏幕比例非常适合阅读 PDF (竖屏);直接在 pdf 上手写标注很自然。</li> <li>(+2) 分屏操作很适合一边看书一边记笔记;触屏笔的段落复制很好用 (用笔尖划下一段文字到笔记,体验非常自然)</li> </ul> <p><img src="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_2_writing.jpg" width="800" height="549" srcset="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_2_writing_hu89152e3bcecc2ebf3f78e52e714d0876_67691_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_2_writing_hu89152e3bcecc2ebf3f78e52e714d0876_67691_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="sp3" class="gallery-image" data-flex-grow="145" data-flex-basis="349px" > 这个是直接在正在阅读的 PDF 上手写标注的效果。</p> <hr> <p><strong>减分项</strong></p> <ul> <li>(-2) 应用市场不成熟,选择偏少。</li> <li>(-2) 分辨率问题。传统 Windows 程序在这个分辨率下整体偏小,不够协调。而新的 WPF 程序虽然分辨率无关,但为了适配屏幕比例,会有轻微的模糊,同样影响了效果。</li> <li>(-1) 在用触摸操作滑动传统 Windows 窗口内的滚动区域时,没有惯性,略显生涩。</li> </ul> <p><img src="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_3_text_blur.png" width="909" height="229" srcset="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_3_text_blur_hu227efca261c8d437baaeef1f9ab6e98e_52490_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_3_text_blur_hu227efca261c8d437baaeef1f9ab6e98e_52490_1024x0_resize_box_3.png 1024w" loading="lazy" alt="sp3" class="gallery-image" data-flex-grow="396" data-flex-basis="952px" > 这个是前面提到的分辨率无关的字体渲染导致的模糊,可以看到清晰的标题栏和稍模糊的正文所产生的对比。</p> <p><img src="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_4_resolution.jpg" width="1452" height="647" srcset="https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_4_resolution_hu19baf17bf07162a9b87c14b99512bcd7_88155_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-30-surface-pro-3-hands-on/sp3_4_resolution_hu19baf17bf07162a9b87c14b99512bcd7_88155_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="sp3" class="gallery-image" data-flex-grow="224" data-flex-basis="538px" > 同样是分辨率问题。一些传统软件的界面,偶尔可能会出现这种未能按照分辨率自动调整的情况。有时候显得不够协调。</p> <hr> <p><strong>其它杂项</strong></p> <ul> <li>内置 Office 2013,触控笔按键直接打开 OneNote,体验自然。</li> <li>内置了 MSE (Microsoft Security Essentials),不用再装杀软。</li> <li>键盘无右 Ctrl,略觉不便。</li> <li>续航较 iPad 稍弱;运行大型3D游戏发热明显。</li> <li>重启较快20秒左右(平常通常用不着)。锁屏解锁无延迟。</li> <li>日常开发环境 Visual Studio 等开发工具和环境搭建完成后,硬盘剩余 90G 左右。</li> </ul> <hr> <p><strong>实得分 85</strong> 值得向有移动办公需求的同学推荐。</p> 2014.11 开放世界游戏中的大地图背后有哪些实现技术? https://gulu-dev.com/post/2014-11-16-open-world/ Sun, 16 Nov 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-11-16-open-world/ <blockquote> <p>有两件事物,我愈是时常反覆地思索,就愈是感受到发自心底的由衷的赞美和无边的敬畏——这就是我头顶灿烂的星空,和我内心的道德准则。 - 康德</p> </blockquote> <p>早些时候在知乎上看到这个有趣的题目,忍不住写两笔吧 :)</p> <hr> <p><a class="link" href="http://www.zhihu.com/question/26538198/answer/33537161" target="_blank" rel="noopener" ><strong>原题目:开放世界游戏中的大地图背后有哪些实现技术?</strong></a></p> <p>补充说明:诸如GTA,武装突袭之类的游戏中,开发者是如何实现超大地形的?对于这一问题有什么主流的解决方案?</p> <p>补充:例如一些开发者提到的浮点精度问题是如何解决的?又如果npc在玩家视野之外是如何运算的??</p> <hr> <p>以下部分是我的答案:</p> <hr> <p>首先肯定一下,这是一个非常有趣的问题。在这个答案里,我将尝试先回答主干问题,再对补充说明里的几个问题简单说一下。</p> <p>下面是本文将涉及到的一些相关技术的列表,(需要说明的是,这些技术单独来看并不复杂,实际动手实现并理解各种取舍以后,在项目当中针对具体的需求去设计和搭配才是关窍之所在)</p> <hr> <p><strong>一、程序技术篇:算法和架构(Programming Algorithms &amp; Architecture)</strong></p> <ol> <li>无限循环的平铺地图(Infinite Tiling)</li> <li>可预测随机数和无限宇宙(Predictable Random)</li> <li>精度问题解决方案(Precision Problem Solving)</li> <li>超大地形的处理 (Terrain Visualization) 4.1 古典算法(从 GeoMipMapping,Progressive Mesh 到 ROAM) 4.2 层次的艺术(Quadtree 和 Chunked LOD) 4.3 以GPU为主的技术(Paging,Clipmap 到 GPU Terrain)</li> <li>id tech 5 的 megatexture (超大地表上的非重复性海量贴图)</li> <li>过程式内容生成 (Procedural Content Generation) 6.1 过程式纹理(Procedural Texturing) 6.2 过程式建模(Procedural Modeling)</li> </ol> <p><strong>二、内容制作篇:设计和创造(Content Design &amp; Creation)</strong></p> <ol> <li>随机地图类游戏 (Diablo II) 中地图的拼接</li> <li>无缝大世界 (World of Warcraft) 中区域地图的拼接</li> <li>卫星地质数据的导入,规整化和再加工(一些飞行模拟类游戏)</li> <li>超大地图的协同编辑:并行操作,数据同步,手动和自动锁的运用</li> </ol> <p><strong>三、异次元篇:我们的征途是星辰大海</strong></p> <ol> <li>终极沙盒(EVE):当规模大到一定程度——宇宙级别的混沌理论与蝴蝶效应</li> <li>打通两个宇宙(EVE &amp; Dust):发现更广阔的世界——宇宙沙盒游戏和行星射击游戏联动</li> </ol> <hr> <h2 id="一程序技术篇算法和架构programming-algorithms--architecture">一、程序技术篇:算法和架构(Programming Algorithms &amp; Architecture)</h2> <h3 id="1-无限循环的平铺地图infinite-tiling">1. 无限循环的平铺地图(Infinite Tiling)</h3> <p>我们就从最平淡无奇的无限循环平铺地图说起吧。这应该是最原始,也是最没有技术含量的开放世界构筑方式了。</p> <p>技术上由于过于朴素,也没什么好说的,就是在同一个坐标系内像铺地砖那样展开,坐标对齐即可,就是接头处需要注意一下,不要穿帮就行。但是千万别因为简单就小看这个技术哟,上面列表里面的不少技术都是在循环平铺的基础上发展起来的,下面我们就来瞧一瞧吧。</p> <p>按照维度的不同,循环平铺有下面三大类:</p> <p>a. 在一维方向上扩展的横版卷轴游戏(以动作类游戏为主)和纵版卷轴游戏(以射击类游戏为主)。这些类型的游戏里,为了避免循环平铺给玩家带来的重复的疲劳,卷轴游戏会添加一些随机或动态的元素,比如超级玛丽里的背景上云朵的位置,分出多个层次以不同速率卷动的背景层,等等。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.1-1-mario.jpg" width="392" height="220" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.1-1-mario_huc4ec958174509a48675ce459984557e5_11779_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.1-1-mario_huc4ec958174509a48675ce459984557e5_11779_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Mario" class="gallery-image" data-flex-grow="178" data-flex-basis="427px" ></p> <p>b. 在二维方向上循环平铺的固定视角2D游戏。这一类游戏里,比较典型的就是 Diablo。暗黑中的随机地图生成,在本质上,就是叠加了一定的随机性,约束和边界条件的循环平铺效果。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.1-2-diablo.jpg" width="550" height="413" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.1-2-diablo_huc98328427d52d4d8e34094c478a43471_34095_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.1-2-diablo_huc98328427d52d4d8e34094c478a43471_34095_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Diablo" class="gallery-image" data-flex-grow="133" data-flex-basis="319px" ></p> <p>c. 在 3D 游戏里循环平铺高度图,形成连绵不断的地形效果。这在早期的模拟飞行射击类游戏里比较常见,现在已经很难搜到图了,我在上大学的时候写的第一个地形渲染 demo 就是平铺的,可惜刚刚翻硬盘已经找不到了555。这一类游戏,在平铺时适当地辅以一些贴图的变化,可以在很省内存的条件下,做出非常不错的效果。</p> <p>找不到游戏内的图,拿下面这个高度图来凑数吧。请大家脑补一下,把下面这个灰度图立体化之后,一块一块像地砖一样循环平铺以后,3D渲染出来的连绵起伏的直抵地平线(好吧,直抵远裁剪面)的山脉的壮观效果吧。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.1-3-heightmap.jpg" width="512" height="512" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.1-3-heightmap_hu0aae0cf110562292d09af28b7b32748a_321245_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.1-3-heightmap_hu0aae0cf110562292d09af28b7b32748a_321245_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Tiling in 3D" class="gallery-image" data-flex-grow="100" data-flex-basis="240px" ></p> <hr> <h3 id="2-可预测随机数和无限宇宙predictable-random">2. 可预测随机数和无限宇宙(Predictable Random)</h3> <p>(本节内的描述和算法,部分参考了《Game Programming Gems I》 <a class="link" href="http://www.gameenginegems.net/gemsdb/article.php?id=75" target="_blank" rel="noopener" >“2.0 Predictable Random Numbers”</a> 一文,请感兴趣的同学自行查找原文通读)</p> <p>有个传说中的游戏叫 Elite ,不知道有没有同学玩到过。据说这游戏运行在32K内存的机器上,其中16K还是只读的ROM。这游戏据说拥有难以匹敌的游戏深度:近乎无限个行星,每一个都有各自的名字和特征。</p> <p>可预测随机数本身是游戏内运用非常广泛的一个技术,这里我们着重谈一下它在为游戏提供(微观上)更丰富的细节和(宏观上)更广阔的世界的作用。这一技术的最重要原则是,为了在一个游戏世界中给出无限空间的幻觉,我们需要满足两个分解条件,可以把它们成为宏无限(macro-infinite)和微无限(micro-infinite)”。前者涉及到问题的空间规模,后者则任意一个对象所支持的最小细节层次级别。</p> <hr> <p>从实现上来说,如何设定随机种子是这个技术的核心。由于给定一个随机种子,生成的随机序列是完全可预测的,那么根据游戏内的一些时空的设定,通过对随机种子进行一些定制,得到在游戏内任意某个时刻和某个空间点上完全可预测的行为就是可行的了。</p> <p>最简单的是使用以下几个元素与随机种子配合计算:</p> <ol> <li>世界坐标(即 X Y Z 值,既可以表示空间中的某个点,也可以表示某个区域)</li> <li>系统时间(可以用真实时间,也可以用游戏内设计者定义的时间,如果是前者的话需要考虑离线时的处理)</li> <li>正态分布(在游戏里建一个查找表即可,这是最廉价的方案)</li> </ol> <p>这些因素加上对应的随机序列,已经可以营造出宏观上比较有深度的一个宇宙空间了。理论上,如果所有的随机性都是由给定的随机种子产生,而这些随机种子要么是游戏定义的常量,要么是查表得到,要么是均匀流逝,要么是由更高层次的随机种子生成,那么这样一层一层上溯到尽头的话,任何一个游戏内的宇宙,都可以归因到一个初始的种子,这个种子,就是决定论中经典物理学的所谓的<strong>第一推动</strong>吧。其实如果真做到了这一点,我们大可以把这个种子交给玩家,在首次进入游戏的时候掷一个 2^64 骰子。这是真正的上帝创世的感觉,想象一下,上帝说,要有光,于是掷出了骰子,第一推动怦然落地,整个时空的巨大齿轮开始运转,在不同的时间点和空间点上,更多的随机序列被生成出来&hellip;</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.2-1-elite2.jpg" width="640" height="517" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.2-1-elite2_hue7413b6e229fe206c51088a69f0b49d1_74160_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.2-1-elite2_hue7413b6e229fe206c51088a69f0b49d1_74160_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Elite" class="gallery-image" data-flex-grow="123" data-flex-basis="297px" ></p> <p>这幅图来自于游戏 Frontier:Elite II(出自<a class="link" href="http://rakanalysis.wordpress.com/2010/09/26/from-the-archive-frontier-elite-ii-a-retrospective-review/" target="_blank" rel="noopener" >这篇文章</a>),下面配的字样是:<strong>“This picture doesn&rsquo;t even begin to show the scale of the universe.”</strong> 大家感受一下。</p> <hr> <p>微观上本质上也是一样的,只是把发散的过程倒过来,变成了逐级收敛的过程。为了在某一个点上放大时,能得到尽可能细致,准确和一致的表现,我们需要对较低层次的世界定义更丰富的规则和约束,比如黑洞对周围的影响情况,双星体系的轨道,恒星的种类与行星个数之间的对应关系,不同恒星系结构下行星表面的大气构成,等等。这样才能较好地平衡多样性和独特性,带来更真实的模拟效果和更令人信服的游戏体验。</p> <hr> <h3 id="3-精度问题解决方案">3. 精度问题解决方案</h3> <p>当足够大尺度的世界被创建出来时,就会自然而然地遇到精度的问题。同时这也是补充说明中提到的一个问题,这里我们简单介绍一下几种实践中的解决方案。</p> <p>先描述一下问题吧,我们知道,IEEE754 中规定的32位浮点数中,有23位是用来表示尾数的。也就是说,对于接近1的浮点数,可能会带来 2E-24 左右的误差(大约是 0.0000001192)以现实单位计算的话,如果游戏世界是边长为100km的正方形,那么在这个正方形的最远角落里,我们的最小空间单位是约 7.8 毫米;而如果是中国这么大的面积的话,空间误差将达<strong>半米</strong>左右。那么可以想象一下,如果是宇宙级别的游戏,采用32位浮点数作为全地图精度,那么实际误差可能会有多么大。</p> <p>在实践中,这种误差可能会带来以下这些影响:</p> <ol> <li><strong>无法将相邻的对象对齐</strong>。这种情况,场景美术(关卡设计师)应该会比较头疼,这对于游戏的编辑器来说是大问题了。物件没法在引擎编辑器里对齐;在不同的平台上,对齐也不一样;甚至不同的编译器,同一个编译器的不同版本编出来的引擎;对齐都不一样 &hellip; 所以说处女座不要做 LD :P。</li> <li><strong>模型间的穿插和裂缝</strong> 本来封闭的墙角可能漏个洞,本来放在地上的石头变成了悬浮在空中。这实际上是上一种的变种。</li> <li><strong>骨骼动画的抖动</strong> 由于世界矩阵往往参与骨骼动画的运算,误差可能会被逐级放大,在那些最远离根骨骼的骨头上(也是玩家最容易注意到的地方),抖动可能会发生得非常剧烈。</li> <li><strong>物理模拟失真</strong> 一些柔体的模拟可能会直接失败,而刚体也可能会产生怪异的运动。碰撞系统也可能无法正常工作。</li> </ol> <p>所有这些一旦发生,都是很容易觉察的。一旦你发现在一个很大的坐标上有这些问题,而接近原点处问题却消失了,那么很有可能就是精度在作怪。而需要注意的是,出现这种问题,只和游戏中出现的数字的规模和跨度有关,和游戏选择了什么样的长度单位(如用毫米还是公里做单位)无关。</p> <hr> <p>那么怎样使用有限的坐标精度来描述较大尺度的世界呢?</p> <p>最直接的方案是 <strong>使用双精度浮点数</strong> (64 位),如果这是可接受的选择,那么就不必费心引入后面讨论的复杂度了。</p> <p>其次是 <strong>区域划分法</strong> 。我看到 Milo 同学已提到,不过这里出于完整性的考虑,再简单说一下。正如 Milo 同学所说,“把世界划分成不同的区域,在区域内的计算使用其局部坐标系统。”相对应的跨区访问,需要对应的“本地A -&gt; 世界 -&gt; 本地B”的坐标转换。</p> <p>还有一个方案是 <strong>节点中转法</strong>。正如移动电话的基站用来承载和协调整个通信网络那样,我们在游戏的给定活动区域使用静态信标,所有的逻辑上与之相关的单位,都以该信标的坐标作为参考单位,这样也可以做到把数据访问局部化。相距足够远的两个物体(相当于上面的跨区访问)交互总是通过静态信标来完成(正如移动电话网络中发生的那样),这样的好处是相关的复杂度可以隔绝在这个中转系统的内部。</p> <p>此外凯丁同学提到了一个 <strong>坐标转换法</strong>,“所有位置信息都以角色位置为中心做一次转换”。这正是 《Game Programming Gems IV》 <a class="link" href="http://www.gameenginegems.net/gemsdb/article.php?id=280" target="_blank" rel="noopener" >“4.0 Solving Accuracy Problems in Large World Coordinates”</a> 文中的方案。这个方案可以解决部分问题(主要是渲染相关的问题),但是仍有一些问题需要考虑,比如:1. (上面提到的)编辑器内操作的物体,在序列化到文件中时,精度丢失的问题。2. 大部分物理模拟通常需要一个角色无关,摄像机无关的全局坐标系。等等。</p> <hr> <h3 id="4-超大地形的处理-terrain-visualization">4. 超大地形的处理 (Terrain Visualization)</h3> <p>终于说到对超大地形的处理了。可以说从上世纪九十年代起,超大地形的可视化,一直是3D游戏领域热门的话题。今天我们就借着这个机会,把相关的算法和实现理一理吧。</p> <p>考虑到篇幅太长的话,俺的手指头招架不住,再一个不少对这个话题感兴趣的同学可能压根就不是程序员,一些实现细节可能我就只是简单提一下,贴代码什么的还是算了,尽量保证整篇文章的信息浓度适中吧。</p> <hr> <p>总的来说,这十多年来,地形渲染技术的发展史就是一部生动的对现代GPU的开发,利用和改进史。整个过程大致可以分成三个阶段:一开始,GPU处理顶点能力很弱,这个时期的各种精巧算法(如一些VDPM和后期的ROAM),<strong>尽力在用CPU来降低顶点的总量</strong>,避免一不留神压垮图形系统;后来,图形系统的能力上去了,人们开始更多地考虑到<strong>把地形系统融入到通用的场景管理</strong>中去,如四叉树八叉树什么的就是在这个阶段被广泛应用的;再往后,GPU已经很强大了,CPU由于要承担更复杂的游戏逻辑,越来越成了整个系统的瓶颈,这个阶段,人们琢磨的更多的是,怎么<strong>利用GPU给CPU减负</strong>了,一直到如今,由 GPGPU 带动起来的异构计算,也都是这个路数。</p> <hr> <p>由于内容比较杂,超大地形这个段落,按上面的描述,咱们分为三个小段分开来讲吧。让俺先沏上一杯碧螺春,为客官一一道来。</p> <h4 id="41-古典算法从-geomipmappingprogressive-mesh-到-roam">4.1 古典算法(从 GeoMipMapping,Progressive Mesh 到 ROAM)</h4> <p><strong>GeoMipMapping</strong> 是从纹理的 MipMapping 技术演化来的一个地表处理技术,原理上是根据任何一小块地形在屏幕上显示的实际尺寸(主要跟与摄像机的距离和起伏程度有关)来选择对应密度的网格,然后把不同分辨率的网格之间以某种方式拼接起来(没有这一步的话就会有裂缝),本质上是一种比较粗糙的区域 LOD 算法。顺便说一下,由于那时候针对顶点级的处理很多,导致这种T型裂缝很常见,以至于有个专门的名字叫“T-Junction”,针对这个的处理在当时也有很多方案。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping.png" width="551" height="398" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_hu37982908fe5f728922d174999fe801af_196859_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_hu37982908fe5f728922d174999fe801af_196859_1024x0_resize_box_3.png 1024w" loading="lazy" alt="GeoMipMapping" class="gallery-image" data-flex-grow="138" data-flex-basis="332px" ></p> <p>这是俺刚刚到老硬盘里刨出来的大三时写的 GeoMipMapping 代码,编了一下居然还能跑起来。有点土,别介意:P 可以看到不同的 MipMap 级别是用不同的颜色渲染的,也可以看到接头处 T 型裂缝的处理。唉,这代码勾起了俺的青葱回忆啊,那就顺便再来两张 T 型裂缝的示意图和消除过程吧。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_2.png" width="522" height="535" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_2_hu66d785c0deb7a2ef1550abe1f02bb096_11948_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_2_hu66d785c0deb7a2ef1550abe1f02bb096_11948_1024x0_resize_box_3.png 1024w" loading="lazy" alt="GeoMipMapping" class="gallery-image" data-flex-grow="97" data-flex-basis="234px" ></p> <hr> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_3.png" width="612" height="495" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_3_hufa91071e6f3828101159313c6df7d764_14033_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-geomipmapping_3_hufa91071e6f3828101159313c6df7d764_14033_1024x0_resize_box_3.png 1024w" loading="lazy" alt="GeoMipMapping" class="gallery-image" data-flex-grow="123" data-flex-basis="296px" ></p> <hr> <p><strong>Progressive Mesh</strong> 是后来很流行的技术 Simplygon 的前身,原理上基本也是一致的,就是以某种方式渐变性地化简某个给定的 Mesh。</p> <p>渐进式网格有两种:视点无关的 (View-Independent Progressive Mesh,VIPM) 和视点相关的 (View-Dependent Progressive Mesh,VDPM)。两者的区别就是,前者预先离线生成好所有的渐变过程,运行时直接用就行(也是后来 Simplygon 采用的方案),而后者随着摄像机的位置和角度的变化,生成对应的简化模型。两相对比,VIPM的好处是运行时运算开销低,简化模型的效果好,缺点是费内存(因为数据都存下来了,当然后来增量的方式能省一些),而VDPM在当时是不错的选择,因为跟VIPM相比不用费额外的内存,而且对于视点(就是摄像机)变化不剧烈的应用,不需要每帧处理和更新对应的简化模型(普通的MMO类的一般一秒一次就够了),此外由于一些简单的遮挡剔除和背面剔除,能够比VIPM裁掉多得多的顶点(一般能多裁1/3到一半吧,在当时这可是头等大事)。</p> <p>总的来说,至少在当时,两者的应用都比较广泛,而到了后来,显存越来越大,总线却越来越紧张,VDPM这种典型的刷顶点的算法(比较费总线带宽)就逐渐失去了市场,这是后话了。</p> <p>大家可以在<a class="link" href="http://www.cbloom.com/3d/" target="_blank" rel="noopener" >这里</a>看到一些 PM 在地形渲染上的应用。图咱就不上了,大家可以到 Simplygon 的网站上去看。</p> <hr> <p><strong>ROAM</strong> 可算作是上面提到的 VDPM 更进一步了。这个算法实际上借鉴了当时主流引擎的标配BSP的思路,想利用二叉树这个最简洁的空间描述数据结构,把(CPU端的)顶点消减发挥到极致。整个地表被组织成一个巨大的二叉树,有两个队列,一个是分割队列,一个是合并队列,分别用于处理摄像机移动时,增加进入视野的区域细节和消减退出视野的区域细节。精心设计的 ROAM 效果非常华丽(尤其是在线框模式下),你会看到在各种因素的影响(包括局部坡度,与摄像机的夹角,遮挡情况等等)下,各种三角形像魔术般的不断地变幻,生成和擦除超多的细节,效果非常魔幻。我印象很深的是当时连续打Quake3两个小时完全无感的我,调试这玩意的时候,每每不到十分钟眼就花了。</p> <p>网上找了两张比较典型的 ROAM 大家感受一下吧。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-roam_1.jpg" width="802" height="599" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-roam_1_hu5e56bd12cccf0fae0a0447d2bba5bb6d_398895_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-roam_1_hu5e56bd12cccf0fae0a0447d2bba5bb6d_398895_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="ROAM 1" class="gallery-image" data-flex-grow="133" data-flex-basis="321px" ></p> <hr> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-roam_2.jpg" width="636" height="398" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-roam_2_hu37966b96b23b4e43c75d03959e6c0b79_247493_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.1-roam_2_hu37966b96b23b4e43c75d03959e6c0b79_247493_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="ROAM 2" class="gallery-image" data-flex-grow="159" data-flex-basis="383px" ></p> <hr> <h4 id="42-层次的艺术quadtree-和-chunked-lod">4.2 层次的艺术(Quadtree 和 Chunked LOD)</h4> <p>其实用于空间管理的树状结构有四叉树和八叉树(还有上面的二叉树),但地表通常以前者居多。是因为,从小范围来看,变化剧烈的地形是3D的,适合八叉树在xyz三个方向上扩展;但当尺度大到一定规模之后,地形通常退化为相对扁平的2D空间,就像摊平了的地球表面那样,在竖直的Z方向上变化相对不大,而XY平面则是可能无限延伸的。</p> <p><strong>Quadtree</strong> 四叉树很直白,具体的细节我就不讲了。值得一提的是四叉树往往也同时用于场景管理的快速剔除和查找,从理论上来讲,四叉树是一个平面上最迅速的用于剔除空间,定位一个物体,内存开销也是相对较低的数据结构。当用于地形渲染时,顶点剔除的效率也很高,我印象中仅次于高度优化的 ROAM。内存开销低主要是因为四叉树是可以完美展开到一个位数组里的,这样的话意味着整个树的利用率达到了百分之百——所有的空间都用来存储数据而不是维持结构。</p> <p>不过四叉树也不是啥都好,T型裂缝就比 GeoMipMapping 难处理,因为存在跨级的多段 T 缝,如下图:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-quadtree_2.png" width="326" height="311" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-quadtree_2_hu76a728fd5d5eda4d5f08916155ee0be9_8605_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-quadtree_2_hu76a728fd5d5eda4d5f08916155ee0be9_8605_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Quadtree" class="gallery-image" data-flex-grow="104" data-flex-basis="251px" ></p> <p>除此之外还有一些细节问题,这里就不一一说明了,地形的四叉树渲染还是有很多细节需要细心处理的,此处暂且放下不表。</p> <hr> <p><strong>Chunked LOD</strong> 是一种杂合改良的 LOD,其实糅合了上面说的不少细节,本质上是一种分区块地消减细节的技术。所谓 Chunk 是批量处理的一种方式,只是一种粒度划分的单位而已,跟现在 Java 的 GC 里分区回收概念上差不多。</p> <p>下面是典型的 Chunked LOD 后的效果:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-chunkedlod_1.jpg" width="1015" height="378" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-chunkedlod_1_huf19351310bc97eb066bf378f4afdf3ae_366735_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-chunkedlod_1_huf19351310bc97eb066bf378f4afdf3ae_366735_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Chunked LOD" class="gallery-image" data-flex-grow="268" data-flex-basis="644px" ></p> <p>顶点多次过滤优化后的效果:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-chunkedlod_2.jpg" width="901" height="439" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-chunkedlod_2_hu952dc52a72d294819288161c226ca9f7_355227_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.2-chunkedlod_2_hu952dc52a72d294819288161c226ca9f7_355227_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="Chunked LOD" class="gallery-image" data-flex-grow="205" data-flex-basis="492px" ></p> <p>效果在当时还是很惊艳的。通常不到50k的渲染数据量已经能有非常逼真的效果了。</p> <hr> <h4 id="43-以gpu为主的技术从-pagingclipmap-到-gpu-terrain">4.3 以GPU为主的技术(从 Paging,Clipmap 到 GPU Terrain)</h4> <p>上面的基本上都是传统方案,这一节我们将逐渐过渡并挨个介绍一下以 GPU 为运算主体的算法。</p> <hr> <p>所谓<strong>分页</strong>(Paging)实际上是仿效虚拟内存的运行机制的一种方法。由于地表的顶点数据都是静态数据,适合常驻显存。当世界尺度较大时,显存没法一次放入所有数据,那么系统就像虚拟内存那样,把那些暂时没有用到的数据交换出去。随着游戏的进行,Paging In/Out 也在不断进行,辅以一定的异步机制,加载到显存的延迟可以被很好地掩盖。玩家的直观感受就是:哇,海量的细节。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-paging.png" width="996" height="798" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-paging_huc7d6f16ad37adc6d345d9ff187055e93_55255_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-paging_huc7d6f16ad37adc6d345d9ff187055e93_55255_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Paging" class="gallery-image" data-flex-grow="124" data-flex-basis="299px" ></p> <hr> <p>而 <strong>Clipmap</strong> 则比 Paging 更进一步,以金字塔的形式逐级把数据排列好,直接整体更新和渲染。从这里也可以看出 GPU 时代人们的思维方式的逐步变迁。从以前顶点级别(Vertex Level)的“锱铢必较”,到后来的一次多塞一点也无所谓,只要批次(Batch)少就 OK。下图可以看出 Clipmap 的基本思路。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-clipmap.png" width="1071" height="467" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-clipmap_hued76b3622157c0a6994f059a47fb7a08_68945_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-clipmap_hued76b3622157c0a6994f059a47fb7a08_68945_1024x0_resize_box_3.png 1024w" loading="lazy" alt="Clipmap" class="gallery-image" data-flex-grow="229" data-flex-basis="550px" ></p> <hr> <p>所谓的 <strong>GPU Terrain Rendering</strong> 就是把高度图从内存里经由 2D Vertex Texture 搬到 VS 里去生成三角面,这样的好处是 CPU 和内存就被彻底解放出来了。只是访问上有一些限制,不像直接处理内存那样方便。具体的细节可以看这里:<a class="link" href="http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter02.html" target="_blank" rel="noopener" >GPU Gems 2: Terrain Rendering Using GPU-Based Geometry Clipmaps</a></p> <p>在 GPU 上做还有个巨大的好处是可以借助 Gaussian Noise 即时生成更多的细节了。直接拿一小块预生成的高斯噪点图在需要的时候叠加一下,就能在没太大额外开销的情况下,增加各种细节。如下图所示:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-gpu_terrain.png" width="418" height="194" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-gpu_terrain_hub75c5aafd42f44573cd50e430a9f2f95_98287_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.4.3-gpu_terrain_hub75c5aafd42f44573cd50e430a9f2f95_98287_1024x0_resize_box_3.png 1024w" loading="lazy" alt="GPU Terrain" class="gallery-image" data-flex-grow="215" data-flex-basis="517px" ></p> <hr> <p>随着大家对 GPU 理解的深入,地形的处理又有很多的小技巧可以做,尤其是在 PS 里面,比如法线生成,动态uv展开,光照按需叠加/衰减什么的。不过呢据我所知没有什么非常别具一格的架构上的新思路了,所以就不再深入了。</p> <h3 id="5-id-tech-5-的-megatexture-超大地表上的非重复性海量贴图">5. id tech 5 的 megatexture (超大地表上的非重复性海量贴图)</h3> <p><strong>megatexture</strong> 在当年(2007)是一个非常值得一提的技术。在这个技术出现之前,几乎所有的地表渲染用到的贴图都是若干张 blend 到一起后,以 tiling 的形式重复平铺在地表上的(包括比较典型的魔兽世界也是如此),这样的直接后果是图片的种类用多了耗资源,用少了又很容易感到单调和重复。而 megatexture 则是一张全局的超大贴图,从根本上避免了重复这个问题,理论上(实践上也是)能够生成非常壮丽和独特的地质风貌,是传统的刷地表无法创作出的效果。可以说这个技术让<strong>真正的全景地貌</strong>成为可能。</p> <hr> <p>技术上的细节puzzy老师写过一个 pdf,强烈推荐感兴趣的同学搜来看一下(可以搜“ <strong>ID Tech 5 中&quot;Megatexture&quot;针对地形的D3D9 基本实现原理 - 姚勇</strong>”),珠玉在前,我就不啰嗦了。就来一张效果图吧(好吧我知道能坚持看到这儿的同学,这图基本上肯定都看过了)</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.5-rage_megatexture.jpg" width="845" height="474" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.5-rage_megatexture_hua002a5c7428797fa28842ba4524c3777_484692_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.5-rage_megatexture_hua002a5c7428797fa28842ba4524c3777_484692_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="megatexture" class="gallery-image" data-flex-grow="178" data-flex-basis="427px" ></p> <p>全局超大贴图对一个开放性世界的价值不言而喻。想象一下,跟拿乐高积木拼接出来的视觉效果(传统的 texture blending and tiling)相比,一幅万米画卷上,每个像素都可以随意描绘,是一种什么感受。 比如,你可以相对轻松地实现“整个世界的地貌瞬间被密集核弹蹂躏了一场之后”的效果。如果你想模拟整个生态环境的变迁,在不同粒度上的整体性修改更是无价之宝。</p> <hr> <h3 id="6-过程式的内容-procedural-content-generation">6. 过程式的内容 (Procedural Content Generation)</h3> <p>“过程式生成”是一个不是很恰当的翻译,实际上更贴近本意的说法是“以程序的手段生成”,这里我们简洁起见,仍使用过程式生成的字样吧。</p> <p>过程式的内容生成是随机技术的在视觉效果上的一个重要衍生。这个技术虽然到最近才被广泛应用,但实际上从技术角度讲,在很久以前就已经有比较成熟的实现了。我手头的 2003 年出版的翻译版 Game Programming Gems III 中 就有 4.16 和 4.17 连续两篇文章以“程序手段生成的纹理”作为主题。这是构建超大规模的世界的一个重要的技术工具,尤其是与上面的 megatexture 技术结合起来,可以创造出非常令人震撼的视觉复杂度。</p> <p>下面是 sourceforge 上一个开源的项目 <a class="link" href="http://pcity.sourceforge.net/" target="_blank" rel="noopener" >PCity - Procedural Modeling and Rendering of Cities</a></p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6-pcity.png" width="500" height="350" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6-pcity_hu75099873a126bebe16d2d40cd0d13cff_290707_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6-pcity_hu75099873a126bebe16d2d40cd0d13cff_290707_1024x0_resize_box_3.png 1024w" loading="lazy" alt="1.6-pcity" class="gallery-image" data-flex-grow="142" data-flex-basis="342px" ></p> <p>可以看出,对于过程式生成来讲,只要有非常小的初始数据集(meta-data),可以在宏观上达到很大尺度和复杂度的视觉效果。</p> <p>过程式生成有两大分支,一个是过程式纹理,另一个是过程式建模(上面的 PCity 属于后者),下面我们分别来谈一谈。</p> <h4 id="61-过程式纹理procedural-texturing">6.1 过程式纹理(Procedural Texturing)</h4> <p>人们发现,自然界中有很多视觉效果是可以用数学公式加上一些简单的随机性来模拟的,比如云彩,水流,火焰,木纹,大理石,草地,夜空,大气等等,程序生成的纹理效果大大丰富了普通纹理能表现的效果,就好像是物理引擎给游戏增加了活力一样。一个普通的噪点图,在不同的情境下,作为辅助参数来参与生成动态纹理,可以产生出近乎无穷无尽的变化。</p> <p>这是过程式生成的云,出处在<a class="link" href="http://www.indigorenderer.com/content/cloud-render" target="_blank" rel="noopener" >这里</a>。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_cloud.jpg" width="640" height="470" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_cloud_hub2390a58db765072ba127e0d49c88e50_108745_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_cloud_hub2390a58db765072ba127e0d49c88e50_108745_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="proc_cloud" class="gallery-image" data-flex-grow="136" data-flex-basis="326px" ></p> <p>这是过程式生成的外观,使用了 Allegorithmic 公司的 Substance Designer,出处在<a class="link" href="http://blog.mediarain.com/2013/03/procedural-textures-using-allegorithmic-substance-designer/" target="_blank" rel="noopener" >这里</a></p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing.png" width="600" height="397" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_huff4ba93d4497dab903072fd1403ccb76_115925_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_huff4ba93d4497dab903072fd1403ccb76_115925_1024x0_resize_box_3.png 1024w" loading="lazy" alt="proc_texturing" class="gallery-image" data-flex-grow="151" data-flex-basis="362px" ></p> <p>这里是一些分解材质,相当于过程式纹理的图素,出处在<a class="link" href="http://www.3dtotal.com/team/Tutorials/yann_vaugne/alientut_1.asp" target="_blank" rel="noopener" >这里</a>。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_2.jpg" width="660" height="289" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_2_hube0820cb71952018e332cd75d9beda6b_26142_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_2_hube0820cb71952018e332cd75d9beda6b_26142_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="proc_texturing" class="gallery-image" data-flex-grow="228" data-flex-basis="548px" ></p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_3.jpg" width="660" height="287" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_3_hue2013b81e61fcb33dbf53fa8f0d7f1ca_34650_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6-proc_texturing_3_hue2013b81e61fcb33dbf53fa8f0d7f1ca_34650_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="proc_texturing" class="gallery-image" data-flex-grow="229" data-flex-basis="551px" ></p> <h4 id="62-过程式建模procedural-modeling">6.2 过程式建模(Procedural Modeling)</h4> <p>过程式建模特指以程序的手段动态建模。这是一个更大的话题,现在比较成熟的中间件的代表是 Speedtree,比如下面这个效果:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-speedtree.jpg" width="470" height="354" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-speedtree_hu4f3105289da2aef34114cd5ea42e8c7a_71636_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-speedtree_hu4f3105289da2aef34114cd5ea42e8c7a_71636_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="speedtree" class="gallery-image" data-flex-grow="132" data-flex-basis="318px" ></p> <p>完全不同风格的纹理,模型的任意杂合,随意生成,效果也非常真实,非常适合做恐怖游戏。在 Speedtree 的网站上还可以看到长成茶壶的树之类的奇葩。我还记得有一年的GDC,在 IDV 的 Speedtree 的展台看到的一段华丽视频,就是各种藤蔓植物在几秒钟之内长满了一个峡谷内的整个大坝,电影级的效果非常震撼,不知道现在网上是否还能找到。</p> <hr> <p>过程式建模是一项非常迷人的技术,我本人也曾被深深吸引,在上面投入过一段时间的精力。2010年时,我在开发一款飞行射击类的 MMO,当时接触到了 <a class="link" href="https://www.linkedin.com/company/557710?trk=prof-exp-company-name" target="_blank" rel="noopener" >Gamr7</a> 的过程式建模技术,感觉很不错,在飞行类游戏中,地面物体的建模可以完全以程序方式生成,这个对当时的我来说吸引力太大了。那时我花了一个月把 Gamr7 的技术集成到自己的框架里,并在上海世博会期间,与 <a class="link" href="https://www.linkedin.com/profile/view?id=518127" target="_blank" rel="noopener" >Bernard Légaut 先生</a> 一起在世博会的法国企业馆展示了合作成果。摘两张当时的 PPT 吧。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-wings_of_fate_1.png" width="697" height="311" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-wings_of_fate_1_hu41b26b5bb74b939564d0ab9375741bdd_269343_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-wings_of_fate_1_hu41b26b5bb74b939564d0ab9375741bdd_269343_1024x0_resize_box_3.png 1024w" loading="lazy" alt="wings of fate" class="gallery-image" data-flex-grow="224" data-flex-basis="537px" ></p> <p>截图中的素材基本上都是使用了过程式自动生成的(不是美术手放上去的),树是用 speedtree 生成的。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-wings_of_fate_2.png" width="688" height="486" srcset="https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-wings_of_fate_2_hu9f24fa8a38a011aefab32e34b25e23a6_160199_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/1.6.2-wings_of_fate_2_hu9f24fa8a38a011aefab32e34b25e23a6_160199_1024x0_resize_box_3.png 1024w" loading="lazy" alt="wings of fate" class="gallery-image" data-flex-grow="141" data-flex-basis="339px" ></p> <p>总得来说,过程式建模是一项<strong>潜力远远没有得到释放</strong>的技术,现有的工具还处于比较原始的阶段。在当年 PPT 的技术展望(Beyond the Tech)一节中,我写到“(过程式建模带来的)<strong>更高级的抽象使我们可以控制更高的复杂度,从而带来更丰富的细节</strong> (Higher abstraction makes much more details and complexities manageable) ”时至今日,受限于技术的发展,这仍只在某个特定的主题(如 Speedtree 的植被模拟和一般的城市模拟)内有效。对于随机性的粒度,我们仍缺乏有效的手段去控制。当年展望时的两大 Expectation(一个是建立起模式和库抽象从而满足不同层次上的复用需求,另一个是如何统一过程式技术中的无序和有序,有效地控制随机性的粒度),现在据我所知仍是所缺甚多,颇为渺茫。当然了,对有志之士,这也不失为一大探索方向。</p> <h2 id="二内容制作篇设计和创造content-design--creation">二、内容制作篇:设计和创造(Content Design &amp; Creation)</h2> <p>聊完了程序方面的内容,我们来简单讲讲超大规模世界在设计和制作方面的一些情况。这方面因为我不是专家,只是做一下简单的介绍,不足之处,还请专业人士指正。</p> <h3 id="1-随机地图类游戏-diablo-ii-中地图的拼接">1. 随机地图类游戏 (Diablo II) 中地图的拼接</h3> <p>在暗黑二,暗黑三和类似的游戏“火炬之光”等游戏中,通过巧妙的拼接,理论上可以通过组合,生成近乎无限大的地图。</p> <p>这是暗黑三第二章里所有地图的可能的部件形状:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_1.jpg" width="500" height="100" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_1_hu46c5beb8d073c32a2cfd9185692c103e_34900_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_1_hu46c5beb8d073c32a2cfd9185692c103e_34900_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="diablo3" class="gallery-image" data-flex-grow="500" data-flex-basis="1200px" ></p> <p>这是拼接之后的样子:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_2.jpg" width="500" height="138" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_2_hu1a231d69051df286f48c50df067f55f2_33092_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_2_hu1a231d69051df286f48c50df067f55f2_33092_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="diablo3" class="gallery-image" data-flex-grow="362" data-flex-basis="869px" ></p> <p>除了拓扑结构上可以任意排列组合以外,在每一个分片上预留足够多的通用接口也很重要。比如一扇拱门,可以是闭合不可交互的状态,也可以通向下一个直角走廊,也可以是通往一个副本的入口。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_3.png" width="377" height="246" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_3_hu50abfc681e1c9672da7fc93441e914f9_152281_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.1-diablo3_3_hu50abfc681e1c9672da7fc93441e914f9_152281_1024x0_resize_box_3.png 1024w" loading="lazy" alt="diablo3" class="gallery-image" data-flex-grow="153" data-flex-basis="367px" ></p> <p>要注意标准化的组件如果太多也会让玩家觉得单调和重复感过强,火炬之光在这一点上就做得不错。下图是火炬之光生成好的地图样貌:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.1-torchlight.jpg" width="562" height="552" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.1-torchlight_hu85c08e8a829afd725b02ada1c56f7e2d_278570_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.1-torchlight_hu85c08e8a829afd725b02ada1c56f7e2d_278570_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="torch light" class="gallery-image" data-flex-grow="101" data-flex-basis="244px" ></p> <p>可以看到效果还是很不错的,一眼看过去已经不太有重复感了。</p> <h3 id="2-无缝大世界-world-of-warcraft-中区域地图的拼接">2. 无缝大世界 (World of Warcraft) 中区域地图的拼接</h3> <p>无缝世界类游戏的区域拼接和上面的随机生成类游戏的区域拼接是很不一样的。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.2-wow_1.png" width="647" height="473" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.2-wow_1_hu2a95f0961365da3008fa7a805aab58ec_303759_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.2-wow_1_hu2a95f0961365da3008fa7a805aab58ec_303759_1024x0_resize_box_3.png 1024w" loading="lazy" alt="wow" class="gallery-image" data-flex-grow="136" data-flex-basis="328px" ></p> <p>可以看出,不同的区域之间有着很长的接壤线和完全不规则的边缘。在这种情况下,为了简化制作,大部分边界区域以高山作为阻隔。你几乎不怎么会看到有建筑横跨两个不同的区域,原因也是在此。</p> <p>在沙盘制作时,通常的做法是在整个世界地图(对应的世界编辑器)中规划好每个区域的范围,也就是分区分块。每个区域由不同的设计师在场景编辑器中分开制作。在制作当前场景时,与该场景接壤的几个场景的最新版本都会加载上来,编辑器中可以提供一些方便的工具,便于在接壤处对齐。主要是高度的对齐和贴图的融合。前者通常的选择是高度上用 Smooth 工具平滑过渡到邻接场景,后者通常最简单的处理方法是真正的过渡点两边使用同一种贴图即可,实际的美术风格过渡(如果需要的话)在邻接地图的接壤线附近完成。</p> <p>一些贯穿不同地图的元素(如河流等)可能需要世界编辑器的参与来指定水平面的高度和区域范围之类的参数,但这一类元素通常不会太多,因为它们没有明显的 Gameplay 贡献,反而加剧了不同场景之间的耦合。</p> <p>如果游戏有动态的天气/环境氛围系统,那么不同场景之间需要做一些平滑的过渡,这些程序用普通的插值实现就可以了,设计师一般只用关心当前场景内的表现即可。当然有的游戏这个过渡做的不好,玩家体验非常生硬,也是有的。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.2-wow_2.png" width="478" height="348" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.2-wow_2_hudefd70af985dc73d8f683711c921e403_304095_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.2-wow_2_hudefd70af985dc73d8f683711c921e403_304095_1024x0_resize_box_3.png 1024w" loading="lazy" alt="wow" class="gallery-image" data-flex-grow="137" data-flex-basis="329px" ></p> <p>总得来说,这一类无缝大地图的复杂度主要是在编辑器的协同方面(后面我们会再提到),实际的创作复杂度较前面的随机地图生成为低。</p> <hr> <h3 id="3-卫星地质数据的导入规整化和再加工一些飞行模拟类游戏">3. 卫星地质数据的导入,规整化和再加工(一些飞行模拟类游戏)</h3> <p>超大规模的开放性世界地图,还有一类是直接使用卫星地质数据以加强整个世界真实性的。据我所知,育碧出品的 Tom Clancy&rsquo;s H.A.W.X. I &amp; II (就是国内翻译的 鹰击长空 I &amp; II)就是使用了 GeoEye 的商业级高分辨率卫星地图。</p> <p>既然用了真实的卫星地质数据,这一类游戏同样能生成非常震撼的效果,也就不用多说了。找两张截图大家感受一下吧:</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.3-hawx_1.jpg" width="844" height="472" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.3-hawx_1_huef425f9447b102e49b6a1a24f0b6cf69_525389_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.3-hawx_1_huef425f9447b102e49b6a1a24f0b6cf69_525389_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="hawx" class="gallery-image" data-flex-grow="178" data-flex-basis="429px" ></p> <p>这一类的缺点是不能模拟真实世界中没有的环境(当然拿去再创作的不算)。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/2.3-hawx_2.jpg" width="840" height="525" srcset="https://gulu-dev.com/post/2014-11-16-open-world/2.3-hawx_2_hub5a39f758f10ac3ef04beec72c7416ce_519044_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/2.3-hawx_2_hub5a39f758f10ac3ef04beec72c7416ce_519044_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="hawx" class="gallery-image" data-flex-grow="160" data-flex-basis="384px" ></p> <p>这种真实数据的数据源就没什么好说的了,我简单说一下导入后的处理。通常导入后的贴图需要美术在色彩和明暗上二次加工一下,得到和游戏匹配的整体氛围。需要提供比较精确的工具给美术进行高度图和高分辨率纹理的拟合。此外通常这一类地质数据是没有 NormalMap 的,需要提供工具生成一下。</p> <p>还有就是,河流和湖泊这一类水体的处理,3D游戏通常在渲染效果方面对水面特效照顾得比较多。如何生成跟真实环境相匹配的河流和湖泊是一大难点。因为一般游戏里是有一个绝对水平的特效面片的,而如果给真实环境中得来的高度数据上配一个这种特效面片,通常无法跟真实的贴图相吻合(尤其是在山脉和峡谷等地形中的水体)如何提取真实的高分辨率贴图中的水面信息,自动生成对应的3D水面,也是一大话题。当然,如果不怕费事,也可以由美术直接做出来了事。</p> <hr> <h3 id="4-超大地图的协同编辑并行操作数据同步手动和自动锁的运用">4. 超大地图的协同编辑:并行操作,数据同步,手动和自动锁的运用</h3> <p>现在咱们回过头来聊一聊在 wow 这一类超大地图里,如何在多人团队内协同编辑的问题。</p> <hr> <p>对于美术(这里专指负责场景的设计师)来说,常见的最简单做法是每人一块(或多块)区域地图,团队内维护一个公共的物件和贴图库。(物件和贴图可以由专门同事制作,需要时也可外包)在这种情况下,美术的并行化程度很大程度上取决于团队自身能力,对场景编辑器没有特殊的技术上的需求。</p> <p>超大地图的场景编辑器在加载周边邻接的区域地图时,需要显式地标示出其版本和上次修改日期,这样可以把邻接区域的修正工作量降到最低。最好的做法是能够通过版本管理软件,在有同事修改了邻接区域以后自动更新并重新加载(当然可以稍有延时,不用那么即时),这样的并行工作效率可以达到最高。</p> <p>真正的难题通常发生在策划那边,当需要编辑跨区任务或事件时,如果对 Ownership (也就是场景实体的所有权问题)管理不善。跨区系统可能会产生各种层出不穷的 bug。比如同一个 npc 承担了多个跨区职责时,其中的状态就可能会互相干扰,在杀掉某个 npc 这一类任务中更易出现,造成难以重现的 bug。这就需要提供明确的所有权管理机制,明确跨区访问的一般规则,提供简单的全局状态检测工具,在设计时就能提示出绝大多数潜在的冲突。当然,这些的先决条件是所有的区域数据,要么提供中央数据库管理,要么工具做到在团队网络内实时同步。</p> <hr> <p>最后我们来说一下真正有趣的实践,就是**“真正的”协同编辑**。也就是任意个美术和任意个策划可以工作在任意个区域里。是的,你没看错,这才是终极的开发自由度。其实如果这是一个如典型的 WOW 那样的 MMORPG 的话,这就跟“所有的玩家登录到同一个服务器一起游戏”是同一个概念了。所不同的是,这里的“玩家”实际上全部是开发团队的成员,而且他们是有能力创造和修改这个游戏世界的。</p> <p>只要想通了这个概念,实践上并不像想象中那么复杂。由于美术和策划对同一个地图关注的焦点不同,我们只要把他们工作有交集的部分处理好,他们就能一起愉快地玩耍了。实践上来看,两者的交集通常是 a. 整个区域的逻辑高度图和 b. 所有的相对碰撞关系。也就是说,美术和策划的工作内容里,只要不涉及到这两者,都可以随便搞,但如果影响到了这两者,编辑器需要有能力提示正在修改的人会影响到什么(或按默认行为自动处理),通常是不难做到的。举个例子,策划放好 npc 后,美术去调整高度,把 npc 站的广场弄成一个山坡,那么要么提示美术这么干可能会影响到策划的设计,要么自动把对应的 npc 都重新调整位置,吸附在新的地表高度(一般编辑器允许设置为吸附到地表)。</p> <p>当两个美术在修改同一区域时,就涉及到锁的问题了。锁有两种,一种是在编辑时自动触发,场景地表以区域为单位,物件以 Instance 为单位,当编辑时自动把 Owner 设为当前编辑者,提交改动到服务器时可以选择继续锁或是释放锁。一种是手动锁,就是美术框住一片区域,主动加锁,这样有些时候更方便。编辑器制作者需要考虑的一些细节有:锁住的区域在其他开发者的机器上,需要比较显眼的提示信息;保险起见总是多锁一定的范围,以方便地表平滑等工具编辑时对周边区域的影响,等等。</p> <hr> <h2 id="三异次元篇我们的征途是星辰大海">三、异次元篇:我们的征途是星辰大海</h2> <p>上面两部分“程序技术篇”和“内容制作篇”已经把大规模开放世界讲得差不多了,下面的内容我取名叫“异次元篇”,也是随便侃侃,大家随便看看就好。</p> <h3 id="1-终极沙盒eve当规模大到一定程度宇宙级别的混沌理论与蝴蝶效应">1. 终极沙盒(EVE):当规模大到一定程度——宇宙级别的混沌理论与蝴蝶效应</h3> <p>对于开放式世界来讲,如果没有真正与这个世界的尺度相配的开放式的交互,那么仍然是一个死气沉沉的世界。EVE,正是一个为了开放式交互而打造的超大的沙盒宇宙。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/3.1-eve.jpg" width="550" height="322" srcset="https://gulu-dev.com/post/2014-11-16-open-world/3.1-eve_hu3818c7d85794c4b4c87bba45ad97dd51_79043_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/3.1-eve_hu3818c7d85794c4b4c87bba45ad97dd51_79043_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="eve" class="gallery-image" data-flex-grow="170" data-flex-basis="409px" ></p> <p>在这个宇宙中,玩家拥有很高的自由度去探索,创造,建设,摧毁(针对自然环境而言),配合,领导,同盟,背叛(针对社会环境而言)。这游戏我就不展开介绍了,有兴趣的同学可以去看一下 <a class="link" href="http://evewiki.tiancity.com/" target="_blank" rel="noopener" >EVEWiki</a>。有趣的是,当沙盒大到一定程度时,它会在很多方面展现出一种自平衡的性质,就像经济学中那只“看不见的手”,自然生态学中地球这个大型生态系统的自我调节和自我修复。在我看来,这也是开放式游戏的最大的魅力之一,也让系统的复杂度进一步接近真实世界。</p> <h3 id="2-打通两个宇宙eve--dust发现更广阔的世界宇宙沙盒游戏和行星射击游戏联动">2. 打通两个宇宙(EVE &amp; Dust):发现更广阔的世界——宇宙沙盒游戏和行星射击游戏联动</h3> <p>跟上面列举的诸多成功游戏范例不同的是,我接下来要说的,是一个虽然雄心勃勃,但却没有成功的例子。</p> <p>EVE 的制作商 CCP,是一个来自冰岛的很有趣也很有追求的工作室。在 EVE 的大尺度宇宙成功地运行了若干年后,他们选择了一个更大的挑战——设计另外一个大型多人在线游戏,把新老两个宇宙之间联系起来,让两个游戏内的玩家可以互动,相互交谈,配合,雇佣,指派任务,火力支援或其他的互动,最终打通两个宇宙,让两个大型多人在线游戏之间达到有机的协同和交互。</p> <p><img src="https://gulu-dev.com/post/2014-11-16-open-world/3.1-eve-dust.jpg" width="640" height="360" srcset="https://gulu-dev.com/post/2014-11-16-open-world/3.1-eve-dust_hu08e0550ad44b41fa39fa561c114d04d4_36081_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-11-16-open-world/3.1-eve-dust_hu08e0550ad44b41fa39fa561c114d04d4_36081_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="eve" class="gallery-image" data-flex-grow="177" data-flex-basis="426px" ></p> <p>CCP 从一开始就没有掩饰这个雄心勃勃的计划,这是一个令骨灰级玩家们震惊和眩晕的设计,也是一个电子游戏行业从未有过先例的构想。</p> <p>这个构想是如此令人敬畏和富有吸引力,以至于我在拿到 offer 后毫不犹豫地投身 CCP Shanghai 的怀抱。在游戏行业,我感到很幸运,能够有机会参与到这样一个项目中来。然而由于一些大大小小的原因,这个项目最终虽在 PS3 平台上线,却没有取得预期的成功。这里既然与主题无关,我就不打算谈论更多的细节了。</p> <p>在 CCP 两年间,我只是一个很普通的工程师,这里的工作经历极大地拓宽了我的眼界,让我知道了什么是真正的 fearless,对先行者们,我始终满怀敬意,对于自己有机会能参与这样的一个项目,我也始终心怀感激。</p> <hr> <p>谢谢你们,让我能在晚上凝视夜空的时候,脑海中浮现出更广阔的世界。</p> <hr> <p>[2014-11-17] 补记:一开始看这个话题有趣,想着说两句。没想到一动笔就停不下来,一口气写了七八个小时我也是蛮拼的。其间或有错漏疏敝之处,让行家笑话了。如发现错误,请不吝指正,在此先行谢过。如引发了有趣的想法,也欢迎在评论中一起讨论。</p> <p>[2014-11-18] 补记:今天中午有半个多小时 Blog 无法访问,后来联系 FarBox 才晓得是流量已经超上限,因为我用的是基础版,每个月100MB流量,可以用5年。结果今天一天的流量就把5年的配额全用完了……刚回到家以后亡羊补牢了一下,把 png 都换成了 jpg,省一点是一点吧 :) FarBox 反应很迅速,赞一下 :)</p> <p>[2015-10-22] 评论中张翼同学问道:“如果游戏世界是边长为100km的正方形,那么在这个正方形的最远角落里,我们的最小空间单位是约 7.8 毫米”, 这个是怎么算出来的</p> <p>32位的浮点数能表示的最小单位取决于尾数部分 (23位),换成10进位大约是小数点后7位 也就是说,如果有个浮点数 1.0,那么下一个浮点数的精确值为 0.0000001192.</p> <p>边长 100km 的正方形地图,如果坐标原点在中心,那么四个角上的点距离原点最远,勾股定理可算得这四个点与坐标原点的距离是 70.71067812 km,假设与这些顶点 5km 以内算作是游戏的角落区域,那么 (70.71067812km - 5km) * 0.0000001192 ≈ 0.000007832712831904km (约7.8毫米) 也就是说,在这些区域内放置的东西,被保存下来时,如果使用 32 位浮点数,是无法摆出两个物体相距 5 毫米 (&lt;7.8毫米) 这样的布局的。这个最小误差值随着与坐标原点的距离越远就越大。</p> <hr> 2014.09 CppCon2014 分类合辑 & 十大推荐阅读列表 https://gulu-dev.com/post/2014-09-23-cppcon14/ Tue, 23 Sep 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-09-23-cppcon14/ <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Live long and prosper. (&#34;生生不息,繁荣昌盛&#34;) </span></span><span class="line"><span class="cl"> - 瓦肯举手礼 </span></span></code></pre></td></tr></table> </div> </div><p>上个礼拜,首届 CppCon 在西雅图举办,大拿们(Bjarne Stroustrup, Herb Sutter, Andrei Alexandrescu, Scott Meyers)纷纷到场谈笑风生,应该算是 C++ 程序员的年度节日了吧。废话不多说,俺把读过的 slides 粗粗分了一下类,遇到有趣的,就简单说上两句;感觉没啥意思的,就直接略过。所以这个列表是不完全的,完全的可以看 <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations" target="_blank" rel="noopener" >这里</a> 。</p> <p>分类标题后的标签说明 (beginner / intermediate / advance) 是俺对内容涉及深度的简单划分,不是很严谨,见谅。</p> <p>文末是俺挑出的<a class="link" href="http://gulu-dev.com/post/2014-09-23-cppcon14#toc_7" target="_blank" rel="noopener" >十大推荐阅读列表</a>。</p> <hr> <h1 id="分类合辑">分类合辑</h1> <h2 id="教程和介绍-beginner">教程和介绍 (beginner)</h2> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/An%20Overview%20of%20C%2B%2B11%20and%20C%2B%2B14" target="_blank" rel="noopener" >An Overview of C++11 and C++14 - Leor Zolman</a> 比较全面地过了一遍 C++11/14 的内容,109页ppt都是关于语言本身,不包括标准库。想走马观花来一遍的同学可以看一下。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/The%20Canonical%20Class" target="_blank" rel="noopener" >The Canonical Class - Michael Caisse</a> 讲解了在现代 C++ 中,实现一个类应该注意哪些问题。俺认为这个选题的立意很有趣,讲解也很不错。值得一看。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Boost%20-%20a%20Bridge%20from%20C%2B%2B98%20to%20C%2B%2B11" target="_blank" rel="noopener" >Boost - a Bridge from C++98 to C++11 - Michael VanLoon</a> 介绍了 boost 库在 C++ 新标准形成过程中发挥的作用,以及哪些新特性是化生自 boost 的,内容较浅显。</p> <p>[游戏开发] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Quick%20game%20development%20with%20C%2B%2B11-C%2B%2B14" target="_blank" rel="noopener" >Quick game development with C++11-C++14</a> 非常初级的游戏开发介绍性材料</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Unicode%20in%20C%2B%2B" target="_blank" rel="noopener" >Unicode in C++ - McNellis</a> Visual C++ Team 的同学介绍 Unicode 的前因后果及 C++ 实现的方方面面的考虑,值得一读。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Lightning%20Talks/Anatomy%20of%20a%20Smart%20Pointer" target="_blank" rel="noopener" >Anatomy of a Smart Pointer - Michael VanLoon</a> 解释了一下智能指针内部实现的一些方案。</p> <h2 id="思维和理念-intermediate">思维和理念 (intermediate)</h2> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Back%20to%20the%20Basics!%20Essentials%20of%20Modern%20C%2B%2B%20Style" target="_blank" rel="noopener" >Back to the Basics! Essentials of Modern C++ Style - Herb Sutter</a> Herb Sutter 的发言,描述了现代 C++ 如何帮助程序员简化头脑模型 (mental model) 的,值得一读。说到 B.S. 的发言 &ldquo;Make Simple Tasks Simple&rdquo; 中提到 &ldquo;&amp;&amp;&rdquo; 的次数为——0次;说到大家达成共识——&ldquo;T&amp;&amp;这个东东的确需要个新名字&rdquo;,只是不接受 Scott Meyers 的 &ldquo;universal reference&rdquo; 提法,倾向于使用 &ldquo;forwarding reference&rdquo; 这个词。俺认为,这个词的确更准确一些,精确地描述了它的行为(把参数的引用性(reference-ness)恰当转发进函数内部),不像所谓 universal reference 只是在概念上概括了一下。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Data-Oriented%20Design%20and%20C%2B%2B" target="_blank" rel="noopener" >Data-Oriented Design and C++ - Mike Acton</a> 面向数据的设计,注意不要跟“数据驱动开发”搞混了。这想法不新鲜,只是日常开发中通常更容易强调方法的比较和筛选,围绕数据本身的思维方式容易被忽视。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Make%20Simple%20Tasks%20Simple" target="_blank" rel="noopener" >Make Simple Tasks Simple - Bjarne Stroustrup</a> 运用新特性来简化开发。BS的唯一一个 session。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/The%20Implementation%20of%20Value%20Types" target="_blank" rel="noopener" >The Implementation of Value Types - Lawrence Crowl</a> 值类型的实现。感觉说得抽象了点。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/The%20Philosophy%20of%20Google%27s%20C%2B%2B%20Style%20Guide" target="_blank" rel="noopener" >The Philosophy of Google&rsquo;s C++ Style Guide - Titus Winters</a> Google 的同学出来谈 Google 的 C++ 编程规范背后的哲学。Google 有四千个 C++ 程序员。要点:#1. 为读代码的人优化(Optimize for the Reader, not the Writer) #2. 规则要体现出价值,不要陷入琐屑的小细节 (Rules Should Pull Their Weight) #3. 尊重标准,但不过分地膜拜 (Value the Standard, but don&rsquo;t Idolize) #4. 一致性 (Be Consistent) #5. 如果有啥特殊情况,一定要在代码中明确地说清楚 (If something unusual is happening, leave explicit evidence for the reader) #6. 少玩花样,不要搞奇技淫巧 #7. 不要污染全局 namespace #8. 必要的时候,为性能让步。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Viewing%20the%20world%20through%20array-shaped%20glasses" target="_blank" rel="noopener" >Viewing the world through array-shaped glasses</a> 微软的同学讲解现代 C++ 的一些设计时可供考虑的方案,内容比较丰富,值得一看。</p> <h2 id="工程实践-intermediate">工程实践 (intermediate)</h2> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Making%20C%2B%2B%20Code%20Beautiful" target="_blank" rel="noopener" >Making C++ Code Beautiful - Gregory and McNellis</a> 来自 Visual C++ Team,一些比较偏向于基础的工程实践,有一点点价值观上的引导。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Modernizing%20Legacy%20C%2B%2B%20Code" target="_blank" rel="noopener" >Modernizing Legacy C++ Code - Gregory and McNellis</a> 同样来自 Visual C++ Team,改良已有代码,易读,有用,推荐。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/C%2B%2B%20in%20Huge%20AAA%20Games" target="_blank" rel="noopener" >C++ in Huge AAA Games - Nicolas Fleury</a> 来自育碧的蒙特利尔工作室 (Ubisoft Montreal 现有 2600+ 人,是世界上最大的游戏工作室) 演讲内容主要涉及编译链接时间优化,性能调整,调试辅助增强等方面,非常实用。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/How%20Ubisoft%20Montreal%20Develops%20Games%20for%20Multicore" target="_blank" rel="noopener" >How Ubisoft Montreal Develops Games for Multicore - Before and After C++11 - Jeff Preshing</a> 主要讲了蒙特利尔工作室利用多核的几种传统模式,以及在 C++11 中利用 std::atomic 改善原有的同步机制</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Practical%20Cross-Platform%20Mobile%20C%2B%2B%20Development%20at%20Dropbox" target="_blank" rel="noopener" >Practical Cross-Platform Mobile C++ Development at Dropbox - Alex Allain</a> 讲了 Dropbox 是如何使用 C++ 来开发移动端 Android/iOS 的 app 的,跟具体平台的交互方面讲得比较多。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Hourglass%20Interfaces%20for%20C%2B%2B%20APIs" target="_blank" rel="noopener" >Hourglass Interfaces for C++ APIs - Stefanus Du Toit</a> 沙漏型接口设计,使用C89做中间的接口层,回避了 ABI 问题;避免了二进制接口出现 C++ 类型;隐藏了内部数据布局;跟其他语言的绑定更为简单一致。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/0xBADC0DE" target="_blank" rel="noopener" >0xBADC0DE</a> 如何着手处理手头的 Bad Code,谈了一下维护,重构和重新设计。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Adventures%20in%20Updating%20a%20Legacy%20Codebase" target="_blank" rel="noopener" >Adventures in Updating a Legacy Codebase - Billy Baker - CppCon 2014</a> 主要是讲 FlightSafety 从老系统迁移到 C++11 的过程。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Hardening%20your%20code" target="_blank" rel="noopener" >Hardening your code - Marshall Clow</a> 一些改良代码的工程实践,主要有 sanitizer, static/dynamic analyzer 等</p> <h2 id="专题-general">专题 (general)</h2> <p>[科学应用] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/C%2B%2B%20on%20Mars%20-%20Incorporating%20C%2B%2B%20into%20Mars%20Rover%20Flight%20Software" target="_blank" rel="noopener" >C++ On Mars - Mark Maimone</a> 介绍了火星漫游者上的 C++ 应用。火星车的资源非常有限;系统的基础环境(大部分驱动)都是C++写的;大部分逻辑用于处理采集的火星表面立体图像(17个摄像机);周围环境测量和分析,路径规划和地表导航,防碰防摔(这一段感觉有点像扫地机器人);两个机器人通过互相采样和分析,实现协调和帮助;嵌入式C++的定制内存管理;强调真实环境测试(Test As You Fly);大多数传回的数据都使用C++分析和处理后发到手机;介绍了C++在各种航空器和人造卫星上的应用 这个演讲的关键字是“<strong>实时</strong>”和“<strong>资源受限</strong>”。演讲的图片很丰富,大部分是真实运作中的火星漫游者(真的很酷),推荐。</p> <p>[科学应用] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Gamgee" target="_blank" rel="noopener" >Gamgee - A C++14 library for genomics data processing and analysis - Mauricio Carneiro</a> 使用 C++14 处理基因组数据的一些应用。基因数据的特性(对任意一个样本充分理解需要与十万份样本进行对比);对比了用 java 时受到的一些限制(如不同数据结构间内存连续性的控制);使用 auto 来解耦模块间 API 调用(避免大量被动修改);一些真实的科学运算方面的简化的代码样例。</p> <p>[军事应用] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/The%20Joint%20Strike%20Fighter%20Coding%20Standard" target="_blank" rel="noopener" >The Joint Strike Fighter Coding Standard - Bill Emshoff</a> 使用 C++ 在 JSF 联合战机上的系统及应用实现。战斗机的环境约束有:硬实时,内存有限,极高的安全性(出问题可能会导致坠机和生命危险),可移植性(适应不同的模拟系统),可维护性(数十年的维护成本)</p> <p>[并行] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Overview%20of%20Parallel%20Programming%20in%20C%2B%2B" target="_blank" rel="noopener" >Overview of Parallel Programming in C++ - Pablo Halpern</a> 描述了并发 (concurrency) 和并行 (parallelism) 的区别及相关的语言支持。</p> <p>[并行] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Parallelizing%20the%20Standard%20Algorithms%20Library" target="_blank" rel="noopener" >Parallelizing the Standard Algorithms Library - Jared Hoberock</a> 来自 nVidia 的并行实践,主要讲述了把标准库中的算法部分并行化的一些实践。</p> <p>[异步] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Asynchronous%20Computation%20in%20C%2B%2B" target="_blank" rel="noopener" >Asynchronous Computation in C++ - Hartmut Kaiser - CppCon 2014</a></p> <p>[异步] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/await%202.0%20-%20Stackless%20Resumable%20Functions" target="_blank" rel="noopener" >await 2.0 - Stackless Resumable Functions - Gor Nishanov - CppCon 2014</a> 微软在 VS &ldquo;14&rdquo; 中实现的可恢复协程(可能会入 C++17)</p> <p>[并发] [Lock-Free Programming (or, Juggling Razor Blades) - Herb Sutter](<a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Lock-Free%20Programming%20%28or%2C%20Juggling%20Razor%20Blades%29%29" target="_blank" rel="noopener" >https://github.com/CppCon/CppCon2014/tree/master/Presentations/Lock-Free%20Programming%20(or%2C%20Juggling%20Razor%20Blades%29)</a> 使用 C++11 提供的并发支持实现无锁的一般方法。</p> <p>[优化] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Where%20did%20my%20performance%20go" target="_blank" rel="noopener" >Where did my performance go - Fedor Pikus</a> 怎样剖析并发程序的性能热点,非常值得一看。</p> <p>[优化] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Optimization%20Tips" target="_blank" rel="noopener" >Optimization Tips - Andrei Alexandrescu</a> 一些细节上的优化实践。</p> <p>[跨平台] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Multiplatform%20C%2B%2B" target="_blank" rel="noopener" >Multiplatform C++ - Edouard Alligand</a> 归纳了一些跨平台开发的要点。</p> <p>[设计模式] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Accept%20No%20Visitors" target="_blank" rel="noopener" >Accept No Visitors - Yuriy Solodkyy</a> 比较了 Visitor 模式的两个替代物 Pattern Matching 和 Open Multi-Methods,值得一读。</p> <p>[Web] <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Using%20C%2B%2B%20to%20Connect%20to%20Web%20Services" target="_blank" rel="noopener" >Using C++ to Connect to Web Services - Steve Gates</a> C++ 在 Web Service 下的应用,来自微软。</p> <h2 id="工具和库-general">工具和库 (general)</h2> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Sanitize%20your%20C%2B%2B%20code" target="_blank" rel="noopener" >Sanitize your C++ code - Kostya Serebryany</a> 来自 Google 的工具集,ASan / TSan / MSan / UBSan 各种自动侦测,各种辅助调试,真的强大,推荐一看。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/STL%20Features%20And%20Implementation%20Techniques" target="_blank" rel="noopener" >STL Features And Implementation Techniques - Stephan T. Lavavej</a> 来自 Visual C++ Team 的同学手把手教你实现 STL 的功能,值得一看。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Embind%20and%20Emscripten" target="_blank" rel="noopener" >Embind and Emscripten - Blending C++11, JavaScript, and the Web Browser - Chad Austin</a> C++ 和 Javascript 的绑定。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Implementing%20wire%20protocols%20with%20Boost%20Fusion" target="_blank" rel="noopener" >Implementing wire protocols with Boost Fusion - Thomas Rodgers</a> boost.fusion 的一个应用——实现一个数据协议。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Introduction%20to%20C%2B%2B%20AMP" target="_blank" rel="noopener" >Introduction to C++ AMP</a> 来自微软的 AMP 简介,介绍性材料。俺认为在一众类似的异构计算工具中,这个是相对比较有前途的,没听说过 AMP 的同学值得一看。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Writing%20Data%20Parallel%20Algorithms%20on%20GPUs" target="_blank" rel="noopener" >Writing Data Parallel Algorithms on GPUs - Ade Miller</a> 又是一篇 AMP 的,这篇比较深入一点。讲了如何把一些算法分解到 GPU 上跑。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/sqlpp11%2C%20An%20SQL%20Library%20Worthy%20Of%20Modern%20C%2B%2B" target="_blank" rel="noopener" >sqlpp11 - An SQL Library Worthy of Modern C++</a> 使用现代 C++ 实现的 sql 库,语法已经简洁到几乎是在 C++ 里写 sql 语句了。不过这种重 template 库的缺点是编译速度慢,出错时信息晦涩,调试时各种符号信噪比太低,有时候 expand 一个类型就能把 watch 窗口给累趴下了。</p> <h2 id="其他">其他</h2> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Type%20Deduction%20and%20Why%20You%20Care" target="_blank" rel="noopener" >C++ Type Deduction and Why You Care - Scott Meyers</a> Scott Meyers 同学谈类型推导。这个内容基本上跟俺前段时间翻译的 EMC 前几条有点重复了,可移步<a class="link" href="http://gulu-dev.com/emc/emc-item-1-template-type-deduction.md" target="_blank" rel="noopener" >这里</a>查看。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/C%2B%2B%20Test-driven%20Development" target="_blank" rel="noopener" >C++ Test-driven Development - Peter Sommerlad</a> TDD,没啥新意,比较基础。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/C%2B%2B11%20in%20the%20Wild%20-%20Techniques%20from%20a%20Real%20Codebase" target="_blank" rel="noopener" >C++11 in the Wild - Techniques from a Real Codebase</a> 几个小技巧。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Decomposing%20a%20Problem%20for%20Parallel%20Execution" target="_blank" rel="noopener" >Decomposing a Problem for Parallel Execution - Pablo Halpern</a> 任务分解和并行化,提到了一个数据分块分区来增强局部性的想法。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Types%20Don%27t%20Know%20%23" target="_blank" rel="noopener" >Types Don&rsquo;t Know # - Howard Hinnant</a> 一种简单的合并哈希的方法,很简单但也很实用的组织形式。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Generic%20Programming%20with%20Concepts%20Lite" target="_blank" rel="noopener" >Generic Programming with Concepts Lite - Andrew Sutton</a> 泛型编程里的 Concept 和 Constraints。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Practical%20Type%20Erasure" target="_blank" rel="noopener" >Practical Type Erasure - A boost::any Based Configuration Framework</a> <a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Pragmatic%20Type%20Erasure" target="_blank" rel="noopener" >Pragmatic Type Erasure: Solving Classic OOP Problems with an Elegant Design Pattern</a> 两篇跟 Type Erasure (类型抹除) 有关的演讲,一个是实际应用,一个是设计探讨,感兴趣的同学可以看一下。俺的感觉是类型抹除虽然能帮助简化接口,但是更容易使用不当,形成 ugly code,造成不相干的耦合。</p> <p><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Lightning%20Talks/Cheap%2C%20Simple%2C%20and%20Safe%20Logging%20Using%20Expression%20Templates" target="_blank" rel="noopener" >Cheap, Simple, and Safe Logging Using Expression Templates - Marc Eaddy</a> 表达式模板的一个技巧性应用,有点意思。</p> <hr> <h1 id="十大推荐阅读列表">十大推荐阅读列表</h1> <ul> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Make%20Simple%20Tasks%20Simple" target="_blank" rel="noopener" >Make Simple Tasks Simple - Bjarne Stroustrup</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/The%20Canonical%20Class" target="_blank" rel="noopener" >The Canonical Class - Michael Caisse</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Back%20to%20the%20Basics!%20Essentials%20of%20Modern%20C%2B%2B%20Style" target="_blank" rel="noopener" >Back to the Basics! Essentials of Modern C++ Style - Herb Sutter</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Modernizing%20Legacy%20C%2B%2B%20Code" target="_blank" rel="noopener" >Modernizing Legacy C++ Code - Gregory and McNellis</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/C%2B%2B%20in%20Huge%20AAA%20Games" target="_blank" rel="noopener" >C++ in Huge AAA Games - Nicolas Fleury</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/C%2B%2B%20on%20Mars%20-%20Incorporating%20C%2B%2B%20into%20Mars%20Rover%20Flight%20Software" target="_blank" rel="noopener" >C++ On Mars - Mark Maimone</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Sanitize%20your%20C%2B%2B%20code" target="_blank" rel="noopener" >Sanitize your C++ code - Kostya Serebryany</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/The%20Philosophy%20of%20Google%27s%20C%2B%2B%20Style%20Guide" target="_blank" rel="noopener" >The Philosophy of Google&rsquo;s C++ Style Guide - Titus Winters</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Unicode%20in%20C%2B%2B" target="_blank" rel="noopener" >Unicode in C++ - McNellis</a></li> <li><a class="link" href="https://github.com/CppCon/CppCon2014/tree/master/Presentations/Where%20did%20my%20performance%20go" target="_blank" rel="noopener" >Where did my performance go - Fedor Pikus</a></li> </ul> <hr> <p>本文链接较多,有可能会出现 GitHub 上原库目录名有变动的情况。如果有链接失效,请不吝指出,俺在此先谢过了。</p> 2014.08 C++ template 为什么不能推导返回值类型? https://gulu-dev.com/post/2014-08-04-type-deduction/ Mon, 04 Aug 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-08-04-type-deduction/ <p>这个问题是周末在知乎上看到的一个问题,有点意思,俺觉得可以讨论一下。</p> <hr> <h3 id="原问题">原问题</h3> <p>[C++ template 为什么不能推导返回值类型?] (<a class="link" href="http://www.zhihu.com/question/24671324" target="_blank" rel="noopener" >http://www.zhihu.com/question/24671324</a>)</p> <p>补充说明: 例如:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">template</span><span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="n">T</span> <span class="n">value</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// for expample </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">T</span><span class="o">*</span> <span class="n">ptr</span> <span class="o">=</span> <span class="n">static_cast</span><span class="o">&lt;</span><span class="n">T</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">_ptr</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">*</span><span class="n">ptr</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>希望能有透彻一点的解释。如果有什么解决方案(如C++11和boost的一些高级用法),也希望能一并回答出来。</p> <hr> <h3 id="我的答案">我的答案</h3> <p>讨论之前先说一下,结合上面的补充说明来看,这个问法其实有一点点瑕疵,这也导致了大家在回答时的一些误会。题主这么问,是因为 C++ 确实提供了函数模板的参数类型推导(通过<strong>调用方提供</strong>的信息,自动推断并填充到模板参数,从而避免用户手动指明模板参数)。</p> <p>这样看,这个问题的正确提法应该是这样的:</p> <p><strong>“C++ template 为什么不能像典型的参数类型推导那样,通过判断调用方提供的返回值类型,将其自动填充到模板参数,从而避免用户手动指明模板参数?”</strong></p> <hr> <p>看了一下现有的回答,发现大家在 “类型推导” (Type Deduction) 上,其实没有在说同一件事,所以我觉得有必要先澄清一下这个概念。</p> <p>@Milo Yip 同学在 @黄柏炎 同学答案的评论中提到:“并不是从调用方推导。C++14可以靠return的类型推导。” 那么 <strong>&ldquo;C++14 返回值类型推导&rdquo;</strong> 和题主问到的 <strong>&ldquo;函数模板参数类型推导&rdquo;</strong> 是一码事吗?</p> <p>答案是否定的。我这里详细说明一下。</p> <p>题主的例子中,所谓 &ldquo;推导&rdquo; 指的是编译器在<strong>某些情况</strong>下,可以根据调用方提供的信息来补全用户未提供的模板参数,是模板实例化 (template instantiation) 的一个步骤,发生的时机是在函数模版的 <strong>调用时</strong> (invoke time of function template)。也就是说,当需要的时候,每次模版函数的调用,均会 (根据调用方提供的信息) 触发一次潜在的模板参数类型推导。</p> <p>而 @空明流转, @vczh @Milo Yip 等同学在答案或评论中提到的 &ldquo;C++14 返回值类型推导&rdquo;,则分为普通函数和模板函数两种情况:</p> <ol> <li>当为普通函数时,返回值类型推导是函数体的一部分,发生在函数定义 (function definition) 时。举个栗子,形如 auto foo(int typedArg) { return typedArg; } 的函数,在定义时已可完全确认返回值类型为 int 了。</li> <li>当为模板函数时,返回值类型推导<strong>仍为</strong>函数体的一部分,但需根据其&quot;是否依赖模板参数类型&quot;来决定发生于定义时还是实例化时。当返回值类型依赖模板参数类型时,情形正如 @vczh 同学举的例子;当返回值类型不依赖模板参数类型时,则退化为 1. 中的普通函数调用情况。</li> </ol> <p>请注意,对于 2. 中提到的 @vczh 同学举的例子,调用方仍需提供模板参数类型,无论是借助编译期推导还是手工填充。</p> <p>总得来说,&ldquo;C++14 返回值类型推导&rdquo; 是一个正向过程,只是语法上的一种简化 (syntax simplification),而语义上与原来的函数完全一致。而题主问到的 &ldquo;函数模板参数类型推导&rdquo; 是一个反向过程,在语义上,返回值的类型&quot;接受推导&quot;和&quot;不接受推导&quot;会导致截然不同的函数特化和调用。</p> <hr> <p>好了,辨别清楚了概念,现在我们来正面回答这个题目:</p> <p>为了尽可能与 C 保持语法和语义上的兼容性,在 C++ 中,对于函数的调用方而言,返回值总是可以忽略的。</p> <p>也就是说,对于给定的函数</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">foo</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>调用方可以这么写:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">foo</span><span class="p">();</span> <span class="c1">// 忽略返回值 </span></span></span></code></pre></td></tr></table> </div> </div><p>对于模版函数而言,如果依赖返回值做模板的类型推导,就会出现由于调用信息不全导致的二义性。</p> <p>还是刚才这个例子,我们改为对应的函数模版,</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">template</span> <span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="n">T</span> <span class="n">foo</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">T</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>假如我们允许借助返回值来推导(如下所示)</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">a</span> <span class="o">=</span> <span class="n">foo</span><span class="p">();</span> <span class="c1">// 特化为 foo&lt;int&gt;() </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">double</span> <span class="n">b</span> <span class="o">=</span> <span class="n">foo</span><span class="p">();</span> <span class="c1">// 特化为 foo&lt;double&gt;() </span></span></span></code></pre></td></tr></table> </div> </div><p>那么当调用方像之前的例子那样调的时候,编译器就没办法处理了:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">foo</span><span class="p">();</span> <span class="c1">// 报错,因为缺乏足够信息做模板实例化 </span></span></span></code></pre></td></tr></table> </div> </div><p>正如@黄柏炎同学所提到的,函数重载时,情况虽略有不同,导致了语义上的处理稍有不同,但最后也产生了类似的效果。</p> <p>那么总结一下,一句话结论——<strong>“为了与C保持兼容,返回值并非是调用函数时的必要条件,因此函数模版类型推导和函数重载都不能且不应依赖返回值。”</strong></p> <hr> <p>如果你只想了解这个问题本身,那么到刚才的一句话结论就可以结束了。然而,对模板而言,函数返回值与函数签名之间的关系实际上要更复杂一些。咱们刚刚也提到,函数模版类型推导和函数重载,看起来在语法上具有某种<strong>形式上的一致性</strong>,两者在语义上是有所不同的。如果您感兴趣,可以接着往下读,我们刨根问底一下,看看返回值究竟在函数签名中扮演了什么角色,顺便弄清楚两者究竟有何不同。</p> <hr> <p>先解释一下函数类型 (Function Type) 和函数签名 (Function Signature) 吧。</p> <p>在 C++ 中,函数类型 (Function Type) 与函数签名 (Function Signature) 是两个完全不同的概念。在我的理解中,前者主要是给程序员用的,通常用来定义函数指针 (形如 void(*)() ) 和函数对象 (形如 std::function&lt;void()&gt;);后者主要是给编译器用的,通常用于重载决议 (Overloading Resolution),模版特化 (Template Specialization) 及相关的类型推导 (Type Deduction),链接时生成独一无二的全局标识 (Name Mangling)。</p> <p>标准规定 (见 1.3.11 对函数签名的说明和 14.5.5.1 对模版函数特化时签名的补充说明):</p> <ol> <li>对于普通函数(非模版函数),函数的签名包括未修饰的函数名 (function name) ,参数类型列表 (parameter type list)和所在类或命名空间名 (class and namespace name)</li> <li>对于类成员函数,函数的签名除了 1 中提到的以外,还包括 cv 修饰符 (const qualifier and volatile qualifier) 和引用修饰符 (ref qualifier)</li> <li>对于函数模板,函数的签名除了 1 和 2 中提到的以外,还包括返回值类型和模板参数列表</li> <li>对于函数模板的特化 (function template specilization),函数的签名除了 1, 2 和 3 中提到的以外,还包括为该特化所匹配的所有模板参数(无论是显式地指定还是通过模板推导隐式地得出)</li> </ol> <hr> <p>下面,我们先来挨个看看如何用标准来解释上面的几种行为,再来看看标准为什么对函数的签名做这样的规定。</p> <p><strong>Q1: 普通的函数重载时发生了什么?</strong> A1. 函数的重载决议机制,依赖了函数签名的独特性。标准的 1 和 2 中,并没有提到返回值类型,因此我们可以认为,仅有返回值不同的函数重载是无效的,因为根据标准,它们签名是完全一致的。</p> <p>例如下面两个函数:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">bar</span><span class="p">()</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">bar</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>在函数定义(不用等到调用)的时候就无法通过编译,因为同一个编译单元 (translation unit) 中出现了两个签名一致的函数。</p> <p><strong>Q2: 函数模板实例化时发生了什么?</strong> A2. 根据 3 和 4 可以知道,通过在签名中包含返回值类型和模板参数列表,一个函数模板及其若干特化得到了某种程度上的强类型保证,当所提到的类型不一致时,编译器有机会报出对应的错误。</p> <p><strong>Q3: 函数模板实例化时,如果触发了类型推导,发生了什么?</strong> A3. 当类型信息提供不完全,需要编译器推导时,从 3 可以知道,由于签名中已经包含了所有必要的信息,编译器有能力借助签名本身得知必要的类型信息并进行补全。</p> <p><strong>Q4: 函数模板实例化时,跟返回值相关的行为是什么?</strong> A4. 返回值是签名的一部分,这个事实导致了下面的定义方式成为可能:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">template</span><span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> <span class="kt">int</span> <span class="n">f</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="n">template</span><span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> <span class="kt">double</span> <span class="n">f</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="mf">0.0</span><span class="p">;</span> <span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>请注意,跟 Q1 中 &ldquo;定义时就无法通过编译&rdquo; 不同的是,这两个同名同参的函数的定义是可以通过编译的,因为根据 3 可以知道,返回值是签名的一部分,这两个函数的签名是不同的。但实际使用时,根据我们之前的“一句话结论”中提到的,(为了与C保持兼容,返回值并非是调用函数时的充分必要条件),<strong>当真正的调用发生时</strong>,编译器有可能缺乏足够的信息去了解返回值的类型,也就不知道该把函数调用决议到哪一个函数定义上去。这个错误理论上来讲可以是一个链接错误,但由于在函数定义的编译阶段已经可以得到了两个不同的函数,那么实际结果是在<strong>调用方的编译阶段</strong>就可以报出错误了。</p> <p><strong>Q5: 模板特化和重载决议同时触发时,会发生什么?</strong> A5. 喜欢刨根究底的同学肯定会产生这个疑问,这里我们举两个例子:</p> <p>例子1,这个例子中,我们不仅期望函数模板会自动推导模板参数,而且期望编译器能够选择正确的重载版本去调用</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">template</span><span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">f</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">template</span><span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">f</span><span class="p">(</span><span class="n">T</span><span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">2</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">main</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">f</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">f</span><span class="p">((</span><span class="kt">int</span><span class="o">*</span><span class="p">)</span><span class="mi">0</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>例子2,这个例子中,我们重载了模版函数和非模板函数,和例子1一样,我们不仅期望 (在必要时) 函数模板会自动推导模板参数,而且期望 (在必要时) 能够选择正确的重载版本去调用:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;string&gt; </span><span class="cp"> </span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;iostream&gt; </span><span class="cp"> </span></span></span><span class="line"><span class="cl"><span class="cp"></span> </span></span><span class="line"><span class="cl"><span class="n">template</span><span class="o">&lt;</span><span class="kr">typename</span> <span class="n">T</span><span class="o">&gt;</span> </span></span><span class="line"><span class="cl"><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">f</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;Template&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">f</span><span class="p">(</span><span class="kt">int</span><span class="o">&amp;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;Nontemplate&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">main</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">f</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p>这里我就卖个关子,不给出解释了,大家也先不要急着到编译器里去验证,根据我们前面讲述的知识,可以先试着通过思考,回答下面几个问题:</p> <ol> <li>这两个例子中的函数,在定义能通过编译吗?调用时能通过编译吗?</li> <li>如果能够运行的话,编译器会做出我们期望的重载决议和类型推导吗?</li> </ol> <p>弄明白了这两个例子,Q5的问题自然也就得到解答了。</p> <hr> <p>好了,通过这一系列的追问,我们总算把相关的行为给解释清楚了。想清楚了上面这些细节,我们也就可以很轻松地认识到标准这么规定的理由,说穿了非常简单,就是两点:</p> <ol> <li><strong>始终保证签名的全局唯一性。</strong></li> <li><strong>始终保证同一个模板的本体和其所有的特化,在签名上的相关性。</strong></li> </ol> <p>具体地说,</p> <ol> <li>使得函数签名这个机制被用于函数重载的决议成为可能</li> <li>使得函数签名这个机制被用于模板特化时的类型推导成为可能。</li> </ol> <hr> <p>嗯,这个问题还是蛮有趣的,不知不觉也讨论了这么多。 应该没有落下什么吧。那么先这样吧,有问题的话再补充。</p> 2014.07 如何判断一个技术(中间件/库/工具)的靠谱程度? https://gulu-dev.com/post/2014-07-28-tech-evaluation/ Mon, 28 Jul 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-07-28-tech-evaluation/ <img src="proxy.php?url=https://gulu-dev.com/post/2014-07-28-tech-evaluation/desc_image.jpg" alt="Featured image of post 2014.07 如何判断一个技术(中间件/库/工具)的靠谱程度?" /><p>前段时间周末闲来无事,写了几段小程序。在第三方库上兜了个圈子(悲催地挨个折腾了 <a class="link" href="https://github.com/vbuterin/pybitcointools" target="_blank" rel="noopener" >pybitcointools</a>, <a class="link" href="http://bitcore.io/" target="_blank" rel="noopener" >bitcore</a>, <a class="link" href="https://github.com/libbitcoin/libbitcoin" target="_blank" rel="noopener" >libbitcoin</a>, <a class="link" href="https://github.com/MatthewLM/cbitcoin" target="_blank" rel="noopener" >cbitcoin</a>, 挣扎了半天最后又回到<a class="link" href="https://github.com/vbuterin/pybitcointools" target="_blank" rel="noopener" >pybitcointools</a>),回想起以前看过的 《Game Engine Gems I》的第一篇就是关于这个主题的(“评估和集成中间件的时候应该考虑什么”,<a class="link" href="http://www.gameenginegems.net/gemsdb/article.php?id=1" target="_blank" rel="noopener" >&ldquo;What to Look for When Evaluating Middleware for Integration&rdquo;</a>),赶紧拿起来翻了翻,顺便总结一下自己的教训,形成文字长点记性。</p> <p>###主观因素</p> <ol> <li> <p>首先说明的是,不管承不承认,个人品味对选择的影响非常大。即使是同一个程序员,随着视野的开阔和水平的提高,也会不断地推翻自己以前的品味。例子就不举了,这种非常主观的因素不宜多说,知道有这个重要因素在影响着判断,并随时去提醒自己,在技术和工具的选择上不要有过分的成见,傲慢与偏见,俺认为就可以了。</p> </li> <li> <p>其次,比普通程序员更爱折腾的文艺程序员,往往还有一个困扰——究竟是该用现成的库,还是自己造轮子呢。如果是自己的 pet project,当然无需多言,怎么折腾都行,甚至换个姿势多来几次都无所谓。但是对于稍微大一点的需要多人协作或控制进度的项目来讲,这个问题就难下定论了。项目类型,成员能力分布,成员熟悉程度,对代码的掌控力,对排期的影响,都是不那么直观的软性待考察因素。</p> </li> <li> <p>第三点,很多时候也是最靠谱的一点——朋友的推荐。很多时候我们用一个东西,不是说这个东西有多好,而是那些我们觉得非常靠谱的人(或组织)在用这个东西。这种“安全感”说白了就是一种对他人的经验和判断力的信赖。对创业团队这一点尤其重要,因为跟大公司里按部就班的正规军相比,他们的生存环境要残酷得多,试错机会要少得多。如果不能很好地借力,什么都自己去尝试,风险就会难以控制。</p> </li> </ol> <p>###客观因素</p> <p>说完了这些模糊的,难以度量的主观因素,终于可以说些明确的客观指标了,这也是那篇文章的重点。这里我择要简单说一下,括号里是我加的备注,见谅。</p> <ol> <li> <p>集成复杂度。好的中间件的特点是高度模块化(就是说不随便暴露无关的接口),最小侵入(普通的使用不需要你使用继承之类的强耦合关系),容易集成到完全不同类型的代码库(比如尽可能地使用可移植代码而不是直接调用平台API),对外部环境有极少的假定,极力与其他系统的实现细节解耦。设计良好的第三方库应尽可能地具有有效的默认行为,需要最小量的配置就可以工作起来。集成的代码接触面积应该最小化(这个接触面积越大越深入,评估周期和维护工作量就会成倍增长)。如果一个中等水平的程序员做了两天还没让集成能初步跑起来,集成复杂度就值得怀疑了。(需要两天以上时间去集成的库,通常需要n个两天去维护) (俺认为,配置繁杂、接口凌乱的库,往往意味着作者<strong>还没想清楚这个库应该被怎么用</strong>,应该用于什么场景,该遮的没遮住,不该露的到处走光,还是很容易识别的)</p> </li> <li> <p>内存管理。关心两点:a. 内存占用是否有不合理的开销 b. 内存所有权和分配释放的职责是否清楚。理论上,库的作者应该是对库的内存使用情况最了解的人,他应该定制明确的分配和管理策略。当超出预算时,应该能明确地通知使用者,从而有机会去处理这类事件,而不是任由自己想分多少就分配多少。(更好的设计是把内存使用设计为可伸缩的,这样调用方有机会在运行时根据需要动态去指定内存用量) 如果一个库不能独立管理一个独立堆或内存池,那至少也要把 alloc/free 的接口开放出来。(这样至少可以接管一下,有机会决定转发到系统堆还是定制的堆,以及插入一些统计和调试性的代码)最糟糕的情况是,直接到处随意地 new/delete。这样的话资源的消耗情况就很难控制了。</p> </li> <li> <p>对大量 I/O 访问的处理。硬盘/光盘/网络都是有延迟的,所以访问的策略非常重要。一个第三方库永远不应该直接调用系统API去访问外部资源(如声音文件等),调用方应该有机会去捕获文件和数据的请求,提供定制的方案,从定制的数据源(如已经缓存到内存中的打包数据)读取。最灵活的方式是库完全不提供任何文件或流的读取,只访问给定的内存块,把资源如何获取完全交给调用方开发者处理。最糟糕的中间件,总是假定 POSIX 文件系统在目标平台是有效的,直接依赖C语言运行时的 FILE 和 fopen() 之类的东东。</p> </li> <li> <p>日志系统。设计优良的库能够一致地处理运行时的消息,警告和错误,也很容易集成到你已有的日志系统。更好的设计给你一个选项能从高到低(从完全静默到最少量的关键错误到完全的调试信息)逐级开启各类信息。当开启静默的选项时,编译期就能把这些额外的调试输出字符串干掉,以避免额外的开销。当开启 &lsquo;Noisy verbosity&rsquo;之类的选项时,系统的各项指标都不断地被输出,通过某段给定的输出,基本能够了解系统当时运行的上下文细节,一些不当的使用也能被及时捕获。</p> </li> <li> <p>错误处理,稳定性,性能开销,工具。通常评估一个技术,这些都是必须考察的指标,俺就不一一赘述了。</p> </li> <li> <p>客户支持,维护工作量,可移植性等等,这些属于延伸的需求,都很直观,也就略去不提。</p> </li> </ol> <p>最后说一下这里面一条俺认为比较重要的,也是当年带队的MMO项目里,被我列为头条编程规范的原则:**绝对,绝对,绝对不要使用没有100%提供源码的第三方技术。**这是一条红线,不管这个技术有多强大,都绝对木有例外。程序猿们或多或少都有感触,在编程的世界里,CPU时序的不确定,存储IO的阻塞,其他进程对CPU/内存资源占用造成的扰动,后台进程如杀毒软件偶尔的锁定文件访问,公网路由的拥塞,都为运行着的程序施加了太多不可预知,不可控制的因素。而在这些不可控制的因素里面,允许在自己进程的地址空间内运行一些无法得知其本来面目的代码,是其中最危险也是最容易失控的那一类。反面例子太多,俺就不举了,也免得触物伤怀,影响心境。</p> <hr> <p>基本上主观和客观待考察因素就是这些了,能坚持看到这里,说明您对这个话题确实是感兴趣。为了对得起您的这份耐心,俺特地准备了一点不干不湿的杂货,还请笑纳。</p> <hr> <p>###“望,闻,问,切”</p> <p>在平常的开发活动中,俺总结(山寨)出了“望,闻,问,切”的四字真言,可以用来在一个相对较短的时间里,判断一个技术的适用性。注意,俺说的这些方法,用来鉴别好坏倒还在其次,重点是辨别其是否适用于当下的需求。</p> <p><strong>“望”</strong></p> <p>跟传统医学里的驻足远观对方的体态、神态、步态不同,“望”咱们这里取声望口碑之意,也就是间接的评价。上面提过,这里不再细说。总之一句话可以作为底线:“大家都说好的,不一定真的好;大家都说不好的,那基本上好不了。”前半句不解释,后半句是说如果别人都掉到过坑里,那就不要再主动往下跳了。这方面惨痛事例很多,俺也就不举了。</p> <p>通常这个步骤是不需要花费时间的,一般来源于平日的认识和积累。</p> <p><strong>“闻”</strong></p> <p>“闻”可以看作是对其的“第一印象”。正如合格的侦探一眼扫过目标人物就能获取大量的相关信息,训练有素的程序员,从首次了解到某个库的名字和简介,和网站主页面上大致一过,已经可以有一个清晰明确的第一印象了。</p> <p>俺对一个技术的第一印象通常包括(但不限于):</p> <ul> <li>(这个技术) 所试图解决的问题是否清晰,明确。</li> <li>(这个技术) 所依赖的工具/语言/方法是否可靠,通用。</li> <li>Google 搜索第一页的质量情况。(注意,是一整页,不是仅仅官方网站那一条。有时,你会发现除了官方网站,其他条目都是抱怨,那就该“呵呵”了。)</li> <li>网站的响应速度,专业度和品味。</li> </ul> <p>这个步骤通常时间花费在一到两分钟之内。</p> <hr> <p>通过“望”和“闻”一般就可以决定,是不是需要继续深入去 “问” 和 “切” 了。</p> <hr> <p><strong>“问”</strong></p> <ul> <li>现在开发是否活跃?上一次更新是在什么时候?</li> <li>有人提交 issue/pull-request 吗?有来自开发者的回复吗?</li> <li>有明确的 branching model 吗?有发布流程吗?是从master分支发布吗?</li> <li>是允许 Fork,还是源码下载,还是只有二进制文件?</li> <li>是否有依赖库?有的话体量有多大,是否为功能所必不可少的?需要单独地维护吗?</li> <li>是否有简明的 build 步骤?build 工具是你常用常维护的吗?</li> <li>build 流程能够自动化并整合入你的 CI 服务器吗?</li> <li>对于之前的发布,网站上有汇总的 release note 吗?</li> <li>对于之后的发布,网站上有清晰的 roadmap 吗?</li> <li>针对个人/组织,分别是什么 License?如果 License 不同,对功能是否有影响?</li> <li>信息源(在线文档/教程/作者blog)的质量如何?浮夸吗?样例工程多吗,具体吗?</li> </ul> <p>这些问题虽然看起来略杂乱,但还是能反映一个库的总体质量的,通常五分钟左右就可以有一个基本判断了。</p> <p><strong>“切”</strong></p> <p>拿到代码,开始庖丁解牛式地查看,可以称之为“切”。</p> <p>这个部分其实完全可以独立出一篇文章了——&ldquo;How to effectively read code&rdquo;,也有一些书是讲这个的,比如 <a class="link" href="http://www.amazon.com/Code-Reading-Open-Source-Perspective/dp/0201799405" target="_blank" rel="noopener" >“Code Reading - The Open Source Perspective”</a>之类的,感兴趣的同学可以去翻翻。</p> <p>题目太大,俺就不展开细说了,只说一个实践之中俺摸索出来的窍门吧:找到这个库最重要的暴露给外部的接口文件(通常是以那个库命名的头文件,如 <a class="link" href="http://www.lua.org/source/5.2/lua.h.html" target="_blank" rel="noopener" >lua.h</a>,<a class="link" href="http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/libkern/libkern/zlib.h" target="_blank" rel="noopener" >zlib.h</a> 等等),就像读文档一样从头到尾(注意,这个顺序很重要)通读一下。看看自己是否能在没什么阻滞的情况下,基本了解这个库的大部分行为。</p> <p>优秀的接口设计,读起来行云流水,错落有致,当缓处则缓,当急处则急,信息密度均匀,命名平易近人,符合人的直觉和思维习惯,让人读来不费脑力,心情愉悦;而不良的接口设计,读来往往乱作一团,东拉西扯,前后矛盾,概念冲突,甚至于夹三夹四,啰嗦重复,没头没脑,不知所云,或者有悖常识,命名奇葩,又或卖弄学问,哗众取宠,治经弄典,艰深晦涩,搬弄奇技淫巧,极尽冷僻深奥,令人蹙眉扼腕,基本读不下去,自不必提。 (从上面这段话本身结构上的前后对比里,大家应该能感受到区别了吧,呵呵)</p> <hr> <p>对中小规模的技术而言,上面的“望,闻,问,切”已经足以应付了。而对大型代码库/框架/引擎而言,又有一套不大一样的评估标准,另有曲径可探,咱们择日另行讨论,此处暂且按下不表。</p> <ul> <li>[2017-03-28] 更新:对大型代码库/框架/引擎的评估见此文:<a class="link" href="http://gulu-dev.com/post/2017-01-15-game-engine-talk-2016" target="_blank" rel="noopener" >游戏引擎技术点滴</a></li> </ul> <hr> <p>写到这里,本文的内容已经基本完整,可以收尾了。不过结束之前,俺来透露一个小秘密吧(思维敏捷的开发者,可能已经想到了):此文明面上为探讨“如何甄鉴一个技术”,实则另有一层涵义——对于程序库的开发者,这其实也是一份可供参考的对照——如果能对上面的视角,方法和手段了然于心,就可以设计出更好,更易用,更为使用者考虑的系统。</p> <hr> <p>看完了俺的介绍,您有什么独特的方法来评估一个技术是否靠谱呢?欢迎在本文后留言,跟大家一起分享和讨论,一定会有更大的收获 : )</p> <hr> <p>[2014-07-29] Update,</p> <p>下面的评论区有同学提到,</p> <pre><code>可以通过“看github的star數量以及fork的次數”来了解。 </code></pre> <p>俺的回复:</p> <p>是的,这是一个可资参考的指标。只是要注意,如果说 fork 的次数还能在某种程度上反映出项目的品质和实用价值的话,那 star 数量通常只反映了一个项目的**“流行程度”**,而流行的东东往往不一定贴合实际项目的需求。须知流行风向会因时而变,因势而变,到那时再去应变,就比较被动了。</p> <hr> <p>[2014-07-29] Update,</p> <p>@wingc 同学在微博上提到,</p> <pre><code>反面例子略过不举,但完全符合客观因素六条的第三方库,正面例子来几个哟?有种现实之中很难找出来的感觉... &quot;绝对,绝对,绝对不要使用没有100%提供源码的第三方技术&quot;,此说法未必太武断些。软件大厂的库,哪怕没有源码,一般出问题真的是因为没有仔细看文档自己用错了。 </code></pre> <p>俺的回复:</p> <p>俺觉得&quot;完全符合&quot;的项目,数量上是趋近于零的。通常来讲,技术由于适用面的不同,都会在某种程度上有所折衷。我还是那句话,我们的目标不是寻找最好的,而是通过对这些因素的考察,找到综合来说最适合自己需求的。</p> <p>设计优良的库,还是不少的,比如俺曾在 blog 上介绍过的 zmq/nanomsg ,但是正如俺开头所说,这玩意受主观影响过大,说多了容易无端被喷,所以先这样吧,日后慢慢介绍不迟。</p> <hr> <p>此外,关于“三个绝对”的问题,俺专门补充说明一下,</p> <ol> <li> <p>如果所谓的“软件大厂”是第一方,那通常咱们也没啥选择。就比如要在 Windows 上开发 3D 游戏,用闭源的 DirectX 也是理所当然。正如文中所说,所谓“三个绝对”是针对那些几乎总是有得选择的第三方库而言的。</p> </li> <li> <p>使用软件大厂的闭源技术,也会带来不小的潜在隐患。大公司通常是不太搭理小团队的,如果你掉进的坑恰好在朋友圈里和网上找不到类似的案例或方案,那尝试跟所谓“软件大厂”交流或反馈一般都是做无用功。最终要么花更多的时间吭哧吭哧workaround,要么去掉对应模块了事。</p> </li> <li> <p>作为程序员,当遇到问题时,你希望的是 a) 通过一路在源码中前后追溯,在解决问题之余,弄清楚前因后果,实实在在地增加自己相关领域的经验和认识呢,还是 b) 对着文档反复猜测和校验自己哪个参数有没有误传?</p> </li> </ol> <p>其实到了关键时刻,有代码在手上,就是一颗定心丸,正所谓**“源码之前,了无疑惑”**。当遇上奇怪的症状时,什么文档都比不上正在运行着的唾手可得的鲜活的代码。</p> 2014.07 (译) 单元测试之迷思 https://gulu-dev.com/post/2014-07-19-unit-test-fetish/ Sat, 19 Jul 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-07-19-unit-test-fetish/ <p>[译者按] 此文为 &ldquo;<a class="link" href="http://250bpm.com/blog:40" target="_blank" rel="noopener" >Unit Test Fetish</a>&rdquo; 一文的摘要。因为读到此文之前,俺只是在实践中模糊地发觉和秉承此观念,只是隐隐觉得单元测试并非改善工程质量之良方,也曾用邮件与异地的同事激辩过单元测试之实质作用,但并未找到会心一击直接KO对方,这其实也说明俺还没点到问题的实质。直到读到此文俺才强烈共鸣,作者把俺没有想通想透的东西,用浅显的话解释得非常清楚,俺是边读边与自己的想法一一印证,阅读带来的愉悦感无逾于此。择要编译于此,这样更多的同学也可从中受益。</p> <hr> <h3 id="正文">正文</h3> <p>我听说,现在有些同学克制不住自己写单元测试的欲望,根本停不下来。要是你也是这么想的,那我建议你花几分钟看看下面这些“不该写单元测试”的理由:</p> <p><strong>a) 从程序员的投入产出比来说,单元测试与端对端测试(end-to-end test)相比,有(若干个)数量级的差距。</strong></p> <p>一个端对端测试对整个代码库的覆盖率差不多是这样的: <img src="https://gulu-dev.com/post/2014-07-19-unit-test-fetish/fet1.png" width="162" height="149" srcset="https://gulu-dev.com/post/2014-07-19-unit-test-fetish/fet1_huf2d98a40629306566f48fd4dd30cf8c6_8386_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-07-19-unit-test-fetish/fet1_huf2d98a40629306566f48fd4dd30cf8c6_8386_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p1" class="gallery-image" data-flex-grow="108" data-flex-basis="260px" ></p> <p>而一个单元测试对整个代码库的覆盖率差不多是这样的: <img src="https://gulu-dev.com/post/2014-07-19-unit-test-fetish/fet2.png" width="160" height="145" srcset="https://gulu-dev.com/post/2014-07-19-unit-test-fetish/fet2_hu4a19eae39cb3585a0f8662e7f8ea9f32_1713_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-07-19-unit-test-fetish/fet2_hu4a19eae39cb3585a0f8662e7f8ea9f32_1713_1024x0_resize_box_3.png 1024w" loading="lazy" alt="p2" class="gallery-image" data-flex-grow="110" data-flex-basis="264px" ></p> <p>拿刷墙来打比方的话,我们应该总是拿最大号的刷子把墙刷个七七八八之后,再用小号的刷子补一下间隙,勾勒一下细节。如果事无巨细地要求提供单元测试,那实际上就是拿小号的中国毛笔来刷整个屋子的墙。</p> <pre><code>[Gu Lu] 这里俺稍解释一下什么是端对端测试。End to End Test 是用来检查程序的主要流程是否正常工作的一种自动化测试手段。说白了,如果是游戏的话,就是运行某个脚本或命令把游戏自动跑一遍,看看主要模块是否能正常工作,跟冒烟测试基本是一回事。着眼点主要在于程序在真实的生产环境(而非单元测试所惯常使用的 faked/mocked 等测试环境)下的运行状况。 </code></pre> <p><strong>b) 端对端测试可以测试关键路径,单元测试做不到这一点。</strong></p> <p>正如刚刚提到的,端对端测试模拟了真实情境下的运行。因此,能够成功地运行端对端测试,通常意味着产品某种程度上是可用的。</p> <p>可是,如果只有一堆单元测试,那最多只能说明对应的一堆零件能正常工作,而拼到一起后的情况是未知的,说不定连最简单的任务都无法完成。</p> <p>的确,单元测试能保证一个组件考虑到各种边边角角的情况,不过用户通常更关心的是正常流程下能不能用。如果正常流程下都会出问题,这就连产品也算不上了。如果程序在正常流程之外的一个很特殊的情况下出问题,那么通常稍晚些修复也问题不大。</p> <p><strong>c) 单元测试让内部的架构僵化。</strong></p> <p>假设你有三个组件 A, B 和 C,全部写有完备的单元测试。现在你觉得架构出现了一些问题,不能适应新的需求了,希望能做一下重构,把 B 拆开,打散到 A 和 C 里面去(B提供的接口都去掉了,A 和 C 的接口都改变了)你会发现所有相关的单元测试都废了,有少量的代码还能打捞出来重用,不过大部分肯定是要重写了。(因为新代码的思路,目的,使用方式,调用接口和内部实现都变了)</p> <p>完备的单元测试会<strong>使产品产生对内部变化的抗拒。</strong> (赞此句) 有经验的程序员在考虑为系统重构预留时间时,如果需要把整个测试套件的重构都考虑进去,通常就会下意识地把本应重构的任务放到“不值得做”那一栏去了。</p> <p><strong>d) 有些东西是无法做单元测试的。</strong></p> <pre><code>[Gu Lu] - (本段原文已略) 这里作者举了个协议解码的例子,意在说明(有时)单元测试本质上等于在测试 1+1=2。对某种协议的实现而言,真正有价值的测试是测试两个实体是否真能用此协议去沟通,而不是对着 spec 照葫芦画瓢出一堆等价的单元测试。 </code></pre> <p><strong>e) 有些东西没有明确严苛的接受标准(acceptance criteria)。</strong></p> <pre><code>[Gu Lu]- (本段原文已略) 这里作者拿 GUI 为例说明有些东西是靠感觉的,是强调用户体验的,这类东西是没法(也不应该)使用逻辑判断来断定其为 Passed 还是 Failed 的。单元测试的焦点在于验证逻辑的有效性,而整体的交互式测试才能有效地协助开发者发现这类问题。 </code></pre> <p><strong>f) 单元测试的覆盖率很容易被用来衡量代码的整体质量,而这一点是一个危险的信号(你应该对其感到畏惧)</strong></p> <p>如果你是在一个层层汇报,等级明确的传统公司工作,那你就要小心了! (在这种公司里),项目的进度会被一级一级地上报,(直至做策略性决策的人手中)。 而软件开发从来都是高度弹性的,很难被度量的活动。 程序员写了多少行代码,QA发现了多少bug,都无法作为衡量其工作表现的指标。</p> <p>现在有了单元测试在手就好办了——汇报单元测试的数量和代码覆盖率就行了。</p> <p>可是这其实是个坑:一旦开始把单元测试的覆盖率作为汇报的指标,程序员实际上就受到一种压力去提高这个指标,目标天然就是尽可能接近100%的代码覆盖率(就好像查错的天然目标就是把 bug 数降到0一样)</p> <p>可是正如前面所说,完全的单元测试覆盖就是高品质的代码吗?这是很可疑的。</p> <p>“把单元测试覆盖率作为一种指标去汇报”向组织的管理者和决策者提供了<strong>虚假的信心</strong>,从而导致错误的局势判断和决策。</p> <p>如果不幸身处这样的组织和制度之中,是很难想到对策与之相抗的。嗯,有条路倒是可以试试——把单元测试的覆盖率搞得足够低,这样就没人会好意思向上级汇报了……(-_-!)</p> <p><strong>g) 对于那些较复杂的,有明确而严格的行为定义的,有一堆 corner case 的计算任务而言,单元测试还是很有用的。</strong></p> <p>如果徒手实现一个红黑树,没有完备的单元测试是很难做到的。 不过,说老实话,你经常要去实现一个红黑树吗?</p> <p>对那些源源不断产生的一批批单元测试而言,究竟有没有一个合理的,有效的理由呢?</p> <p>还是仔细想想吧,(这里的讨论)没准能帮你节省点儿无谓的工作量。</p> <p>(正文完)</p> <hr> <p>原文链接:<a class="link" href="http://250bpm.com/blog:40" target="_blank" rel="noopener" >Unit Test Fetish</a> (Martin Sústrik, Jun 4th, 2014)</p> <pre><code>[Gu Lu] 单元测试并非一无是处,此文的价值在于让开发者更清醒地看待其不足。我自己就感到,自己写过的一些测试基本上就是在测1+1=2,没有体现出测试代码真正的价值。择要摘此文以自省。 另,出于效率考虑,本文省略了部分原文内容(已注明),感兴趣的同学,请自行前往原文对应处查看。 </code></pre> <hr> <p>[2014-07-19] Update:</p> <p>wingc 同学在微博上提出不同看法,俺和回复一起转过来以供对照 :)</p> <p>@wingc: &hellip;单元测试最基本的功能其实是test driven development和prevent regression最直接最快的手段。文中都在说单元测试如何废,却没有谈其最有用的地方。论点A/B我赞成,C/D/E若把&quot;UT&quot;换成&quot;E2E&quot;一样说得出一堆E2E没用的地方来,F嘛根本就不是UT本身的问题。</p> <p>我的回复: C 中明确指出“内部的”架构,而e2e是不关心内部实现的;D 只需注意“真正有价值的测试”一句即可;E 中的大部分确实不是测试能解决的问题;F 是不是 unit test 本身的问题,我觉得不是重点,重点是揭示运用时潜在的风险哦 :)</p> <p>至于单元测试最有用的地方,最后一条 G 应该是一个解释。</p> 2014.06 昔时因 今日意 侃侃微软的CRT https://gulu-dev.com/post/2014-06-28-microsoft-crt/ Sat, 28 Jun 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-06-28-microsoft-crt/ <p>[作者按] 此文的原名本来是 &ldquo;<strong>Visual C/C++ Runtime (CRT)的历史演化,及其即将发布的大型重构</strong>&quot;,但写下这个冗长枯燥的标题后,我感到很不好意思——题目都这么乏味,内容就可想而知了。果断删了重拟一个,也趁机往人文的路口挪上两步。</p> <hr> <p>多年来,Visual Studio 几经沉浮,一直是为数不多的有竞争力的开发工具之一。而其提供的 C 语言运行时环境(C Runtime,简称 CRT),是其中一块至关重要却又默默无闻的基石。某种意义上讲,庞大的 Windows 帝国和上面运行着的大部分应用和游戏,正是构建在这薄薄的一片运行时之上。而所谓“昔时因,今日意”,正是意在正本清源,循着脉络将 CRT 的来龙去脉梳理一下,也就能回答“<strong>从何处来,向何处去</strong>”这个问题。全文分为两部分,“昔时因”回顾 CRT 与 Windows 相生相伴的历史,“今日意”则着眼于当下正在进行的重大重构,于未来趋势亦可管窥一二。</p> <hr> <pre><code>“昔时因,今日意,胡汉恩仇,须倾英雄泪。虽万千人吾往矣,悄立雁门,绝壁无余字。” —— 《苏幕遮》,《天龙八部》回目名 </code></pre> <hr> <h1 id="昔时因"><strong>昔时因</strong></h1> <p>在很久很久以 前,曾经有一个 dll 叫做 MSVCRT.DLL,故事就是从它开始的。</p> <p>在 Windows 95 的洪荒年代,这个 dll 就是 Visual C/C++ (确切地说是VC++ 4.2)的运行时库。在那时,每当 Visual C++ 团队有新的版本要发布时,Windows 团队就同步更新操作系统的 msvcrt.dll 来跟 Visual C++ 团队保持一致。这样,开发人员使用新版VC写出来的程序,才可以在更新后的系统上正常工作。而假如 Windows 团队想修一个 crt 的 bug,他们不能手一挥直接改掉,而是必须得通知 Visual C++ 团队也对同一份代码做对应的改动。俺估计呢这也实属寻常,主要有两个原因:a) 因为他们总是新 dll 的头一批用户,为了配合对应的 Windows 新特性的市场宣传,只能给 Visual C++ 当小白鼠,做做义务 QA 了。 b) 微软收到的用户反馈,比如“某游戏在某次系统更新之后无法运行了”这种事,往往不能第一时间确认是不是 crt 的兼容性问题,这些黑锅都只能 Windows 团队先背着了。</p> <p>大家可能觉得 Visual C++ 团队很惬意,只管自顾自往前做就行了,反正出了事有 Windows 团队兜着(我估计事实上还真是)。在 *&quot;<a class="link" href="http://blogs.msdn.com/b/oldnewthing/archive/2014/04/11/10516280.aspx" target="_blank" rel="noopener" >Windows is not a Microsoft Visual C/C++ Run-Time delivery channel</a>&quot;*一文中, Raymond Chen 就举了个例子,有一次 Visual C++ 这边修了个 Y2K(千年虫)问题,结果系统一更新之后客户的程序就都崩溃了。一查之后发现,这个 crt 的修复影响了栈的行为,把客户代码里一个没初始化的变量给暴露出来了。</p> <p>其实现在回过头去看看 Win95/98 的兼容性就知道,像这样的事情数不胜数。微软的每次发布都忙着四处救火,也就罢了,软件开发者们提心吊胆,生怕自己写的程序在某次系统更新后就不能用了。但凡 Visual C++ 出了新 Patch(往往还是重大安全更新),负责 Windows 系统更新的团队就得以最快的速度把这份最新的 msvcrt.dll 部署(其实就是直接把老的 msvcrt.dll 直接覆盖掉)到数以百万计的客户机器上,而客户环境配置千差万别,各种程序还能不能正常运行,只能看老天爷的眼色了。而某个中二程序直接拿它自己开发测试环境里的一个 msvcrt.dll(实际上是个相对较老的) 在安装过程中把系统的 msvcrt.dll 给偷梁换柱的事情更是屡见不鲜,导致的各种蓝屏死机随机故障也就随处可见了。</p> <hr> <p>说了这么多大家可能已经明白了,考虑到这两个团队的规模,这种高度的同步是很难搞的;而且在那个动态链接都还是新鲜玩意的年代,像 <a class="link" href="http://en.wikipedia.org/wiki/Component_Object_Model" target="_blank" rel="noopener" >COM</a> 这样相对规范的模型被应用的范围就更小了。总的来看,微软还未演化出有效的机制和流程,去缓和 <a class="link" href="https://en.wikipedia.org/wiki/DLL_hell" target="_blank" rel="noopener" >DLL Hell</a>,系统地维护兼容性。<strong>强制高度协同</strong>(人的因素)和<strong>缺乏控制手段</strong>(技术因素)两个因素交织起来,足以搞垮任何为了维护兼容性所作出的努力。这种维护难度体现在什么地方呢?举个栗子吧,某 C++ 语言功能要求改进一下 ostream 这个类,为了不破坏二进制兼容性,维护人员不能改变对象尺寸,不能改变任何成员的偏移,他们的改动都要经过精心的设计,谨慎地绕过各种沟沟坎坎,避免打乱已有的虚函数的调用时机,还要注意避免效率上的损失。</p> <p>有过程序经验的同学应该了解,即使是一段不起眼的“Hello World”程序,从语义到运行环境,内部暗含的上下文约束的信息量也是非常大的。维护代码时,要想不干扰明面上的逻辑很容易做到,但要想保证各种暗含的假定完好如初可就难了,有时甚至根本就<strong>不可能有效地证明自己是否已经做到</strong>(俺认为,这一点才是致命的)。如果你觉得对于一个像 CRT 这样规模的程序库来讲,这简直是痴人说梦的话,那你说对了,微软确实最终没做到这一点—— Win95 和 Win98 最终就有两个相互不兼容的 msvcrt.dll。</p> <hr> <p>经过了无数个系统更新导致的鸡飞狗跳和西雅图夜未眠之后,微软的经理们终于想通了——按Windows怪兽般的膨胀速度,系统里鸡零狗碎的组件数量的增长情况,再这么搞下去, Win98 之后……就木有然后了。经过某次决定性的会议后,经理们手挽着手,一脸沉痛地向全世界的开发者们郑重宣布:兄弟们,msvcrt.dll 这个 dll 已经被我们玩废了,以后仅供我们 Windows 内部使用(OS DLL),大家就不要再 cue 它了,Let it go 吧。</p> <p>自那以后,每个新版本的 Visual C++ 都带有一个拥有独立版本号的 crt dll ——这就是 msvcr71.dll, msvcr80.dll, msvcr90.dll 等文件的由来。他们彼此独立更新,微软也终于撇下了历史包袱。</p> <hr> <p>世事难料,尽管距离 msvcrt.dll 被声明为非公开的系统 dll 已经过去很多年了,可仍然有很多不明真相的同学孜孜不倦前仆后继地 cue 这个库;并且由于 crt 的代码是公开的(为了满足那些喜欢 hacking,依赖 undocumented behavior 的同学的需要),大量历史遗留代码(和源源不断新写出来的代码)依赖了 crt 的实现细节。这些年来,微软的技术团队为这群人操碎了心(像不像 IE6 和 WinXP?),例子我就不详述了,大家如果感兴趣的话,可以去 MSDN 的 <a class="link" href="http://www.microsoft.com/en-us/search/SupportResults.aspx?q=kb" target="_blank" rel="noopener" >Knowledge Base</a> 里翻翻,那里是微软工程师们的200,000部伤心的历史记录汇总。唉,满纸荒唐言,一把辛酸泪,都云作者痴,谁解其中味。程序猿们,你们懂的。</p> <hr> <p>总之对于多年来微软在保持向后兼容性这个方面做出的努力来看,俺是真心佩服的。以俺的见识,也的确很难找到另一家能如此做到<strong>尽量为自己的黑历史负责任</strong>的公司。正如一句评论所说</p> <pre><code>“I continue to be amazed at the level of effort Microsoft go to in order to accommodate other people's stupid design and implementation choices.” </code></pre> <p>大家可能时常会觉得,微软背负了太多历史包袱,像是一头行动迟缓的年迈骆驼。可是对于历史遗留问题和历史决策失误,微软的确没怎么逃避过买单。虽然姿势可能一贯不怎么优雅,品味和情怀也是一贯被嘲讽的水准,不过这种负责任的精神,俺认为仍然是相当值得肯定和称道的。</p> <hr> <h1 id="今日意"><strong>今日意</strong></h1> <p>说完了过去的种种,那么现今的情况如何呢?</p> <hr> <p>自那以后,Visual Studio 在十多年间发布了若干个版本,微软的后缀版本号策略也一路用到了最新版本的 Visual Studio 2013(也就是对应的 msvcr120.dll 和 msvcp120.dll)。这个模型虽然把单个库文件成功地扩编成了一个加强连,但也的确解决了不少问题,至少 Visual C++ 可以放心大胆地加新功能了,不用担心破坏已有软件的行为。</p> <p><strong>可是</strong>(重点来了),在这个模型上,微软收到的负面反馈越来越多。越来越多的大型软件项目发现,系统内诸多组件对不同版本的老旧 crt 的依赖,让他们很难迁移到新版本的 crt 上;有时,还需要付出不菲的精力去支持一些古老的使用某个特定版本的 crt 的 dll 插件(有的甚至连代码都没有)。</p> <p>作为软件开发者,俺也发现,在一个大型系统里,很难快速明确地查知,不同的二进制文件分别依赖了哪个版本的 crt 。考虑到一些 server 组件会在远端的机器上神不知鬼不觉地更新,没事儿就用 <a class="link" href="http://www.dependencywalker.com/" target="_blank" rel="noopener" >Dependency Walker (depends.exe)</a> 去彻查一遍也不现实,此其一。当发现大量的不一致后,要把它们改成一致,更是难以充分预估的工作量。</p> <p>口说无凭,俺就举个俺被坑过的例子吧。较新的 CRT 中,对 <code>time_t</code> 的定义默认情况下是 <code>__time64_t</code>,而这个值以前曾经有很长一段时间内是 <code>__time32_t</code>。也就是说,如果你用到的第三方库使用了较老的 32 位 time_t 而你用了较新的 64 位版本,那么不会有编译和链接错误,只会在运行时冒出一些难查的 bug,诸如(微小几率) sprintf() crash,(微小几率)附近的内存被写坏,等等等等。这类问题的麻烦之处在于,即使你已经费了很大劲把出错的区域定位到一段不长的局部代码,也很难联想到是 CRT 的兼容性在捣乱。</p> <p>与此同时,由于受到了强大的外部压力,微软加快了 Visual Studio 的发布周期,再加上一些特定用途的定制版 crt,比如主机版,移动版,等等,需要维护的版本线以排列组合的方式加速增长。微软自己也发现,维护和支持所有老版本的 crt,把一些重要的补丁向后移植回大部分已经冷却的老版本,带来的开发和测试成本已经大的吓人。</p> <p>终于又到了不得不变的时候了。</p> <hr> <p>在 Visual C++ Team Blog 的 <em>&quot;<a class="link" href="http://blogs.msdn.com/b/vcblog/archive/2014/06/10/the-great-crt-refactoring.aspx" target="_blank" rel="noopener" >The Great C Runtime (CRT) Refactoring</a>&rdquo;</em> 一文中,我“惊喜”地发现,下一代的 Visual Studio &ldquo;14&rdquo; (实际产品名称可能会是 Visual Studio 2015) 中的 crt 又回到了早先单个 &ldquo;msvcrt.dll&rdquo; 的模式。VC团队决定不再添加新的版本号后缀,从那以后将一直使用同一个 dll 并自始至终保持它的兼容性。</p> <p>历史在这里兜了个圈,又回到了原点。</p> <hr> <p>我想此刻大家最好奇的问题可能是:那个方案不是早就被淘汰了吗,为什么这回又重走老路呢?</p> <p>这篇 blog 里没有正面回答这个问题。俺来试着答一下吧:</p> <ul> <li>第一,这些年来 crt 的特性集变动不大,整体趋于稳定,在可预见的将来,不太会出现架构性的变化,给了微软的工程师足够的信心保持整个协议层(俺把所有公开给程序员的接口统称“协议层”)的兼容度。</li> <li>第二,从工程师们的行文笔调看,微软这些年来在无数的口水声中积累了一套相对完整的测试套件,覆盖各种边边角角的情况(这甚至包括巨量的对以往的各种未修复的 bug 的行为的兼容性),这个套件的价值难以估量,可以帮助微软把绝大多数破坏兼容性的情况消弭于日常开发之中。</li> <li>第三,也是最重要的一点,就是时代变了。现在已经不再是那个通过在内存里共享代码段来节省内存的年代了。任何一个应用程序,只要它愿意,可以部署若干个 crt 的副本而不用考虑任何边际的开销。说白了,即使程序需要某个特定版本的 crt,它自己部署一个就好了。微软已经发展出各种成熟的通道把每个应用程序用到的运行时环境跟其他程序隔离开来,甚至同一个应用程序,也可以在启动时指定运行在不同的运行环境下(所谓的 WinXP / Win98 兼容模式)那么,通过版本号来区分就显得画蛇添足了。</li> </ul> <p>需要说明的是,这不是官方的说明,只是俺的个人理解,欢迎讨论。</p> <hr> <p>好了,解释清楚了这些情由,我们还是一起来看看新的 crt 有什么特色吧。</p> <p>新的 CRT 将被切成三块:VCRuntime,AppCRT 和 DesktopCRT。如下图所示:</p> <p><img src="https://gulu-dev.com/post/2014-06-28-microsoft-crt/crt-01.png" width="497" height="313" srcset="https://gulu-dev.com/post/2014-06-28-microsoft-crt/crt-01_hufe7237045eca161f5d5821388252a88e_10389_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-06-28-microsoft-crt/crt-01_hufe7237045eca161f5d5821388252a88e_10389_1024x0_resize_box_3.png 1024w" loading="lazy" alt="crt-01" class="gallery-image" data-flex-grow="158" data-flex-basis="381px" ></p> <p>功能也很明确:VCRuntime 是基础模块,是核心中的核心,运行时的运行时(the runtime of the runtime);AppCRT 是兼容移动设备的模块;DesktopCRT 是传统的桌面环境的模块。</p> <p>从介绍中我们看到后两者大体上是包含关系,但我倾向于认为它们将会发展成为下面的结构:</p> <p><img src="https://gulu-dev.com/post/2014-06-28-microsoft-crt/crt-02.png" width="499" height="316" srcset="https://gulu-dev.com/post/2014-06-28-microsoft-crt/crt-02_hu70253714ef4d24e7d45b5e31f324d480_10340_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-06-28-microsoft-crt/crt-02_hu70253714ef4d24e7d45b5e31f324d480_10340_1024x0_resize_box_3.png 1024w" loading="lazy" alt="crt-02" class="gallery-image" data-flex-grow="157" data-flex-basis="378px" ></p> <p>也就是说,俺推测,随着时间的推移,AppCRT 和 DesktopCRT 之间的公共部分会被下推到 VCRuntime 中,而 AppCRT 可能会发展出一些移动平台独有的特性,与DesktopCRT的功能集将不再重叠。当然了,无责任随便说说,大家看看就行。</p> <hr> <p>说完了架构,再来说说实现吧。在新版本的 CRT 里,微软的工程师做了大量的重构来简化代码。</p> <p>按惯例,那些吐槽以往的实现有多难维护的巴拉巴拉巴拉咱们还是跳过吧——慢着,这个关于 printf 的吐槽还是蛮有趣的,跳过了怪可惜的。据说大家都很熟悉的 printf 系列函数有 142 般变化,基本都实现在 output.c 里。别看这个文件不长(2696行)——里面有223个 #ifdef 条件编译开关(其中一大半都在一个1400 行长的函数里),更骇人听闻的是,这个文件以不同的编译开关反复编译达12次来生成所有的142般变化。</p> <p>12次!</p> <p>大家可以在自己机器上,找到 <code>C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src\output.c</code> 去瞅瞅看。顺便说一下,那个1400行的函数是 <code>_output</code> 函数族,这个函数光是 Signature (就是函数体前的声明)就长达59行,预编译宏多达36个,相当壮观,绝对值得你前去拜一下。</p> <hr> <p>大家可能想不到的是,在新的 CRT 里,</p> <ul> <li>呃,绝大部分代码现在都改用 C++ 实现了(然后在头文件里 extern &ldquo;C&rdquo; 出去)。</li> <li>呃,绝大部分手工资源管理现在都改为某种形式的智能指针了。</li> <li>呃,大部分的 #ifdef 都改成模板和重载了。</li> </ul> <p>微软大概可以机智而自豪地宣称,俺家的 C 语言运行时,是用 C++ 写的,独此一份哦。</p> <p>唉,冤冤相报何时了啊。</p> <hr> <h1 id="明天的明天---写在最后的话"><strong>明天的明天 - 写在最后的话</strong></h1> <p>好了,关于CRT的故事,到这里也快要结束了。不过临走之前,请先留步,且容俺把话题稍微扯远一点吧。</p> <p>曾经有段时间(新世纪的头几年),微软向开发者全力兜售托管平台(.net),希望籍此一举摆脱掉各种历史包袱,在一个崭新的,统一的,规范的,和谐的,幸福的,最重要的是,由微软主导的平台上,重建整个生态环境。奈何若干年过去了,买账的人十分有限不说,相当的 Windows 平台开发者反而被吸引到了竞争对手的移动平台。可见光是把房子盖好还不够,周边配套也要跟上。当然了,事后诸葛谁都会当,不过在我看来,.net 吃亏就吃亏在没有与之相配的,清晰明确的商业模式。当年微软宣传了无数次的“.net 大法好”,开发者们还是云里雾里,没看出来这玩意究竟好在哪儿,也没看出来微软究竟葫芦里卖的是什么药。苹果那边一声吆喝“AppStore + 三七分成”,开发者纷纷响应,用脚投票,直接光速构造了应用生态圈。商业模式之威力竟至于斯!</p> <p>看到这里大家可能会想,这跟你这儿说了半天的 CRT 有什么关系,这不跑题了吗?别急,就要来了。这些年来,微软先是被软硬兼施的苹果弄得没脾气干瞪眼,又被 Google 领头的一帮九零后杀马特(你还真别急,那年头,微软眼里,这群互联网新贵还真就是这个层次)三拳两脚给打蒙。最近两年,微软逐渐回过味来,开始收复失地。新的 CEO 上任以来,也的确有了新的气象。对于广大开发者对 Visual Studio 在 Native 方面急需强化的呼声,微软终于敞开怀抱,开展了轰轰烈烈的 <a class="link" href="http://channel9.msdn.com/Shows/C9-GoingNative" target="_blank" rel="noopener" >GoingNative</a> 运动(2012和2013两年的 GoingNative Conference 全部是可移植C++的内容),<a class="link" href="http://channel9.msdn.com/events/build/2014?wt.mc_id=build_hp" target="_blank" rel="noopener" >Build</a>大会上Native相关的内容迅速增加,对新标准C++11/14的支持也以前所未见的速度展开(<a class="link" href="http://blogs.msdn.com/b/vcblog/archive/2014/06/11/c-11-14-feature-tables-for-visual-studio-14-ctp1.aspx" target="_blank" rel="noopener" >C++11/14 Feature Tables For Visual Studio 14 CTP1</a>),包括最近的 <a class="link" href="http://blogs.msdn.com/b/vcblog/archive/2014/04/16/parallel-stl-democratizing-parallelism-in-c.aspx" target="_blank" rel="noopener" >Parallel STL</a>,以及将于2014年9月份举办的首届 <a class="link" href="http://cppcon.org/" target="_blank" rel="noopener" >cppcon</a> (GoingNative的组织者和Visual C++团队<a class="link" href="http://blogs.msdn.com/b/vcblog/archive/2014/04/03/cppcon-the-cpp-conference.aspx" target="_blank" rel="noopener" >一起操办</a>),等等等等。</p> <p>这几年 Visual C++ 也新增了各种新玩意,包括工具链诸如 MSBuild 的改进,光是 <a class="link" href="https://nuget.org/" target="_blank" rel="noopener" >nuget</a> 带来的方便就值得击节了,那也不必多提,俺有机会抽时间写个经验贴吧。不过这里俺还是要忍不住啰嗦一句,凡是在nuget上配置好的第三方库,在你按下F5开始调试的时候,nuget 会自动帮你下载对应VS版本的库,缓存到 Solution,配好所有 Include/Lib 等依赖关系,并将对应的 DLL 放入你的分发目录。咱 C++ 土著这些年来谁享受过这包管理呀。</p> <hr> <p>在 Apple 和 Google 的步步进逼下,在移动互联的阵阵浪潮之中,微软这艘驶过了无数风雨的巨轮,仍在不断调整自己的航向,缓缓前行。作为前互联网时代整个软件行业最大的开拓者,微软承受了许多其他的组织从未有机会面对过的考验,挫败,彷徨和挣扎,也催生出很多外人难以想像的成熟(或曰老迈?)机制和文化。是的,人们总是习惯于追捧,著迷于精美,新奇,有趣的流行科技,而蔑视,唾弃一个多年陪伴的老面孔。这是社会风尚的天性使然,却并不是专业人员用来评价好坏的标准。社会风尚自然会随着流行风向而变化,而真正的价值却来源于时间的考验和岁月的沉淀。</p> <p>谨以此文向微软表达一个普通开发者发自内心的敬意。</p> <p>好吧,气氛好像严肃了点。那么,最后这句话,我来替微软问了吧——明天的明天,你还会用俺家的 CRT 吗?</p> <hr> <p><strong>修订历史</strong></p> <ul> <li><code>2022-07-04</code> 修正文中部分链接格式,顺便对字句做了些微的调整</li> <li><code>2014-06-28</code> 本文初次发布</li> </ul> <hr> <p>date: 2014-07-02 03:48:07 author: wingc email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: <a class="link" href="http://spaces.wingc.net" target="_blank" rel="noopener" >http://spaces.wingc.net</a> ip: 131.107.147.48</p> <p>全文都是干货,虽然行文调侃,但从回顾历史到剖析现状到展望未来,都是有大量的实料可读。如果读者不知CRT为何物,我劝还是关闭浏览器吧,此文不适合你。如果读者依然看得云里雾里,我劝只抓一个重点就行: “大家可能想不到的是,在新的 CRT 里,</p> <pre><code>呃,绝大部分代码现在都改用 C++ 实现了(然后在头文件里 extern &quot;C&quot; 出去)。 呃,绝大部分手工资源管理现在都改为某种形式的智能指针了。 呃,大部分的 #ifdef 都改成模板和重载了。 </code></pre> <p>微软大概可以机智而自豪地宣称,俺们家的C语言运行时是用C++写的,独此一份哦。”</p> <p>这一步步子真的有点大,肯定蛋被扯得很疼。至少处理C++异常就够头大了,C++异常不能抛到extern C那一层,全都要转换成errno反馈到C的调用者。等到这重写的CRT被静态链接到kernel代码里,那就是更进一步,kernel都是C++的咯!</p> <hr> <p>date: 2014-07-02 05:35:24 author: gemfeeling email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: ip: 116.19.107.97</p> <p>说一下俺的认识(和推测)吧 :)</p> <ol> <li>由于微软断然不会更改整个库的接口的错误汇报方式,正如您所说,CRT不会使用异常作为向外汇报错误的手段,这一点是确然无疑的(否则就不能叫CRT了)。</li> <li>然而,假使不把异常看作是语言提供的汇报和处理错误的一种机制,而看作是一个单纯地用于在必要时安全地 stack unwinding 的工具,那么在改善程序可读性上(替换掉已有的 goto/longjmp 之类),异常机制是有一定实用价值的。可参阅此文<a class="link" href="http://www.boost.org/community/error_handling.html" target="_blank" rel="noopener" >Error and Exception Handling</a> 的 &ldquo;When should I use exceptions?&rdquo; 一节。</li> <li>然而,假如决定在内部使用异常来处理错误,并总是在对外时转为 errno ,这是合情合理的实践,也是看起来有些啰嗦,但实际上益处多多的实践。 对于一个设计良好的程序库而言,内部运行产生的错误,在<strong>通过库的边界</strong>汇报给外部用户时,应该总是被统一转换为某种统一的外部错误。因为被汇报的错误也是一个库对外接口协议的一部分。不经转换直接报出的内部错误,会有在接口层暴露实现细节的风险。当然了,更简单的做法是内部和外部统一使用同一套公开的错误信息,但这样的话,公开的东西修改阻力很大,内部逻辑就少了很多自由度,有时会很被动。更多的策略性讨论可参阅 C++ Coding Standards 的第69条。</li> <li>如果用的话,应该不太会直接用C++异常(std::exception)而是用结构化异常 (SEH),道理跟“不直接使用 C++ 标准库里的智能指针而是自己实现了一个简略版”类似(避免未来潜在的循环依赖)。</li> </ol> <p>其实如果从思路上讲,Kernel 的很多组件在二十年前就是基于对象思路去设计和实现的了,咱们大可不必纠结其用哪种语言实现(因为差别不大)。</p> <hr> <p>date: 2014-07-02 08:27:28 author: wingc email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: <a class="link" href="http://spaces.wingc.net" target="_blank" rel="noopener" >http://spaces.wingc.net</a> ip: 131.107.159.48</p> <p>找了好久直接回复评论的按钮,居然找不到&hellip;</p> <p>第1/2/3条有保留同意。仔细考虑了你说的认识和推测,我倒觉得实现起来都不要C++异常更好。如果处处都智能指针了,真不一定需要C++ 异常来做stack unwinding。再考虑CRT有errno,而Windows API又有system error code和HRESULT error code平行两套,code到code做个mapping总比exception对象到code来的简单。而且,考虑到CRT要有静态库放出,若代码实现用到异常处理,但用户产品链接时却不小心link到了非异常实现的其他lib岂不是大乱(好吧,最可能出现的是用new时假设bad_alloc异常,却不小心链接到了nothrownew的实现只返回空指针)。再加有C++异常的代码编译出来后真的多了些累赘。若选实现方案,我情愿连新CRT的C++实现都不要用C++异常。我们都不是其实现者,不知道具体会怎么做,到时候放出来时再看如何?</p> <p>第四条,呵呵呵,完全不同意SEH啊。那玩意是pain in the ass,从语法上来讲破坏了C和C++,从实现上来讲紧密依赖平台。我理解的“不直接使用 C++ 标准库里的智能指针而是自己实现了一个简略版”应该是把C++异常拿掉的标准库,而不是把C++异常换成SEH。同样,这是基于我的个人感受和理解。</p> <p>Kernel内部组件实现是早就基于面向对象设计,但实现就未必了。没有编译器支持C++对象模型和模板元编程,靠纯C的若干奇技淫巧也是能搭出基于接口的实现。但是若直接走上了C++,再整上RAII,用模板和trait去掉了宏,那叫个高大上啊!我还是很纠结的。不过话说回来,若不小心跟踪调试进去,看不到源码,还是一样的痛苦。</p> <hr> <p>date: 2014-07-02 14:47:04 author: gemfeeling email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: ip: 113.106.106.98</p> <p>是啊,这种尴尬事(假设bad_alloc异常却 nothrownew)向来就是微软的强项 :)</p> <p>我也不喜欢 SEH,只是考虑到既然已经大范围用了 (应该是在微软 crt 能跑的平台上都有实现) 就没那么强的动力去再做一套。 而且在<a class="link" href="http://msdn.microsoft.com/en-us/library/swezty51.aspx" target="_blank" rel="noopener" >这里</a> 也可以看到,SEH 本身就是微软为C语言做的语言扩展异常机制,现有的 crt 中也被大量运用,所以我倾向于认为微软会继续用这个,不过世事无常,谁知道呢。</p> <p>呵呵,内核调试俺不熟,不敢妄言,就不多嘴了 :)</p> <hr> 2014.06 nanomsg - zmq 的华丽转身 https://gulu-dev.com/post/2014-06-08-nanomsg/ Sun, 08 Jun 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-06-08-nanomsg/ <p>开始之前先简单说明一下,对 <a class="link" href="http://zeromq.org/" target="_blank" rel="noopener" >zmq</a> 不熟悉的同学可以看<a class="link" href="http://program-think.blogspot.com/2011/08/opensource-review-zeromq.html" target="_blank" rel="noopener" >这篇</a> (中文,需翻墙)文章了解一下。<a class="link" href="http://nanomsg.org/index.html" target="_blank" rel="noopener" >nanomsg</a> 是其作者两年前开始设计和实现的下一代 zmq,因为基本上完全 re-engineer 了,所以名字也变了。</p> <p>这是一篇快速笔记,原文标题为 Differences between nanomsg and ZeroMQ(<a class="link" href="http://nanomsg.org/documentation-zeromq.html" target="_blank" rel="noopener" >原文链接</a>)。我在泛读此文时,本来计划是把它简明地摘录一下,因为读来深觉其文(包括其引用的各篇文章)信息量较大,充满了作者对之前的开源作品 zmq 的经验教训的各种提炼和反思,不是那种可以快速压缩成三两句干货的水文,于是边读边记录下了原文的要点,以备自己日后参考。</p> <p>[GL] 开头的行,都是我夹带的私货,见谅。</p> <p><strong>POSIX 兼容性的实现 (与zmq不同,nanomsg 目标是保持完全的 POSIX 兼容性)</strong></p> <ul> <li>发送/接收函数的语法和语义与POSIX一致</li> <li>Context这个概念被去掉了,掉了&hellip;</li> <li>Sockets由void*改为int (句柄化)</li> <li>[GL] 我能说 zmq 原本就只有三个核心概念(context/socket/message)吗,这就嗖的一声干掉了一个。</li> <li>[GL] 再一次印证了zmq doc里一句点32个赞的话:<strong>&ldquo;We add power by removing complexity rather than exposing new functionality.&rdquo;</strong></li> </ul> <p><strong>与使用C++的zmq不同的是,nanomsg使用C实现</strong></p> <ul> <li>不依赖 C++ runtime 降低了内存需求总量和内存分配的数量</li> <li>从效果上看,降低了内存碎片程度,提高了缓存命中率</li> <li>[GL] 至于为什么从C++转向C,作者写了两篇雄文(<a class="link" href="http://250bpm.com/blog:4" target="_blank" rel="noopener" >这里</a>和<a class="link" href="http://250bpm.com/blog:8" target="_blank" rel="noopener" >这里</a>)还是蛮值得一读的</li> <li>[GL] 对了,上面两篇文章大半价值都在下面的评论里,粗粗过滤一下水分,还是蛮精彩的,特此说明一下。</li> </ul> <p><strong>更方便扩展的传输协议</strong></p> <ul> <li><a class="link" href="https://raw.github.com/nanomsg/nanomsg/master/src/transport.h" target="_blank" rel="noopener" >transport</a> 和 <a class="link" href="https://raw.github.com/nanomsg/nanomsg/master/src/protocol.h" target="_blank" rel="noopener" >protocol</a> 相关的实现被单独提到两个对应的头文件里,并提供标准API支持</li> <li>已经有一些新的 protocol 在实现中(SURVEY / BUS 等)</li> <li>[GL] 可以想见的是,有了标准API,会有 user-defined protocol 逐步出现,值得期待。</li> </ul> <p><strong>线程模型改进</strong></p> <ul> <li>zmq 有一个基本设计是为库内每个独立对象维护一个 worker thread,这个设计带来了很多局限性,新的设计是核心对象不再和特定线程绑定</li> <li>在 nanomsg 里面 REQ 支持 retry, REQ/REP 支持 cancelling</li> <li>inproc 协议使用起来,行为(bind, auto-reconnect)跟其他的协议也更一致了</li> <li>在新的线程模型下,nanomsg 正在尝试实现 thread-safe socket</li> <li>[GL] 其实我倒觉得实践中这个帮助不大,有了zmq里大量定义良好的协议组合,不同线程混合读写一个 socket 几乎可以看作是一种 bad smell 了。</li> </ul> <p><strong>IOCP 的支持</strong></p> <ul> <li>在 Windows 平台上酌情使用 iocp/NamedPipes 提高性能,而不是始终使用 BSD socket</li> <li>[GL] 优雅的抽象重要,群众的呼声更重要啊。</li> </ul> <p><strong>Routing 优先级的支持</strong></p> <ul> <li>就是往外发的消息在一些特定情况下可以 fallback 到不同的目标去</li> </ul> <p><strong>其他小改进</strong></p> <ul> <li>tcp 连接可以指定具体的本地接口(&ldquo;tcp://eth0;192.168.0.111:5555&rdquo;)</li> <li>全库范围 DNS 查询异步实现</li> <li>真正的零拷贝 (zmq只保证到内核边界前是零拷贝)</li> <li>PUB/SUB 协议在150M量级的订阅情况下性能改进</li> </ul> <p><strong>协议层的设计改进</strong></p> <ul> <li>不同协议之间被完全隔离(比如REQ和PUB是不互通的)</li> <li>协议的完整行为被规范化(spec 在 rfc 目录),目标是被 IETF 标准化</li> <li>[GL] 野心很大啊,这是要在通讯协议上一桶浆糊的架势啊。</li> </ul> <p>总的来看,nanomsg 作为一个进化版的 ZMQ,(至少看起来)是相当有诚意的。经过多年的发展和简化(注意,不是演化,是简化),zmq 已经在&quot;结构简单&quot;和&quot;功能强大&quot;之间有了令人发指的平衡了,因此上俺对 nanomsg 的后续还是非常期待的。话说 nanomsg 是真的还在 beta 早期吗,这货只出过 0.1-alpha,0.2-alpha,0.3-beta 这区区三个版本,已经有了15种语言的绑定了。这&quot;0.3-beta&quot;的版本号和网页上无所不在的&quot;WARNING: nanomsg is still in beta stage!&ldquo;是要闹哪样?俺深深地迷惑了。</p> <hr> <p>date: 2014-07-04 04:14:16 author: JackyW email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: ip: 76.21.8.163</p> <p>期待看到有关于这个项目的更多更新!:)</p> <hr> 2014.06 独立域名 gulu-dev.com 已经可以访问 https://gulu-dev.com/post/2014-06-02-gulu-dev-com-available/ Mon, 02 Jun 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-06-02-gulu-dev-com-available/ <p>昨天申请了一个独立域名(<a class="link" href="http://gulu-dev.com" target="_blank" rel="noopener" >gulu-dev.com</a>),经过一个小时的折腾,现在已经可以正常访问了,目前指向托管于 FarBox 的 blog。</p> <p>「<strong>blog迁移</strong>」</p> <p>这一次顺便将 blog 从 LogDown 迁移到了 FarBox,主要是由于以下这些原因:</p> <ul> <li>LogDown 的访问速度较慢</li> <li>LogDown 的编辑器有一些 bug(不过绝大部分可以 workaround)。</li> <li>跟 LogDown 相比,FarBox 的文章直接以文件形式保存到 Dropbox,更方便一些。</li> <li>当我打算做域名绑定时,发现服务偏贵($50)</li> </ul> <p>而 FarBox 这边呢</p> <ul> <li>访问速度实测相对理想 (200ms)。</li> <li>编辑器有待考察,暂时没有发现问题。</li> <li>域名顺利绑定(虽然文档有点绕不过还是看明白了并顺利绑定成功)现已可以使用 <a class="link" href="http://gulu-dev.com" target="_blank" rel="noopener" >gulu-dev.com</a> 顺利访问</li> <li>价格比较公道( 5年期Basic对应为¥20,一年期Senior账户对应为¥120,一年期Pro账户对应为¥300。)如果我没理解错的话 Basic 一年只需 4 元,比一个 $.99 的 app 还便宜。如果这一两个礼拜用起来没有问题的话就用这个了。</li> </ul> <p>「<strong>兼容性</strong>」 迁移非常顺利(除了一些小问题,比如代码段落引用支持“指定代码文件名”),这说明不同写作平台之间对 markdown 的兼容性还是做的不错的,这也增强了我以后更多使用这个格式的信心。</p> <p>「<strong>自建平台</strong>」 对了,中间也考虑过 GitHub+jekyll / GitHub+Octpress / GitHub+Hexo 等方案,也都进去折腾了一下,不过考虑到日后拿来折腾的时间有限,不如回归写字发文的本质。</p> <p>date: 2014-06-02 21:34:46 author: Hepo email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: <a class="link" href="http://doc.farbox.com" target="_blank" rel="noopener" >http://doc.farbox.com</a> ip: 123.158.58.187</p> <p>Thanks, Gulu, 欢迎来到FarBox.</p> <p>我们的服务器也在海外,但跟别人(单服务器单线路)不一样的是,我们底层有线路自动选择的机制,基本上可以保障大陆这边访问的速度稳定在200-300ms之间的延时。另外,页面渲染的引擎也非常快,目前控制在10ms左右的平均耗时。</p> <p>两个基本引擎的优化,可以让海外访问的速度极其的快,而国内,也不至于糟糕。 :)</p> <hr> <p>date: 2014-06-02 21:42:01 author: Hepo email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: <a class="link" href="http://doc.farbox.com" target="_blank" rel="noopener" >http://doc.farbox.com</a> ip: 123.158.58.187</p> <p>btw, Gulu, 不清楚国内的域名是否要备案,建议下次续费前可以迁移到国外的注册商。</p> <p>居我们所知,LogDown应该在日本的Linode上的VPS,而我们的几台服务器则是在美国的中部,物理距离实际上更远,但相信我们在“海外对国内”线路优化上的经验,是少有几个团队可以做到的。 :)</p> <hr> <p>date: 2014-06-03 11:39:58 author: gemfeeling email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: ip: 113.106.106.98</p> <p>谢谢你 Hepo,我申请域名的时候忘了备案这一茬了:P 要是会有麻烦的话,我可以迁到 GoDaddy 。</p> <p>对了有个小问题,(这个模板下) 怎么在导航栏添加添加一个 About Me 静态页面? (类似这个 <a class="link" href="http://gulu.logdown.com/pages/about-me" target="_blank" rel="noopener" >http://gulu.logdown.com/pages/about-me</a>)</p> <hr> <p>date: 2014-06-03 13:51:11 author: Hepo email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: <a class="link" href="http://doc.farbox.com" target="_blank" rel="noopener" >http://doc.farbox.com</a> ip: 123.158.57.166</p> <p>@gemfeeling 对FarBox来说没有麻烦。 :)</p> <p>一般都是由模板控制的,这个模板,可能增加个about.md就会出现对应的导航了。</p> <p>如果要做成/pages/xxxx的类型,则要自己在template目录里定义一个pages.html来控制这个URL。</p> <hr> <p>date: 2014-06-03 20:34:53 author: gemfeeling email: <a class="link" href="mailto:[email protected]" >[email protected]</a> site: ip: 23.239.5.110</p> <p>多谢你 @Hepo,可以了 :)</p> <hr> 2014.05 全民飞机大战 - 简评和碎碎念 https://gulu-dev.com/post/2014-05-31-review-of-weixin-air-combat/ Sat, 31 May 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-05-31-review-of-weixin-air-combat/ <img src="proxy.php?url=https://gulu-dev.com/post/2014-05-31-review-of-weixin-air-combat/combat.jpg" alt="Featured image of post 2014.05 全民飞机大战 - 简评和碎碎念" /><p>这段时间在手机上玩全民飞机大战,今天正好有空,写一些游戏时想到的零星想法吧。(每一段前面的 +n 可以理解为我对某个游戏特色的认可度)</p> <ul> <li><strong>[+3] 关卡间衔接用星空背景的大量星星奖励过渡</strong></li> </ul> <p>这个过渡,在设计上流畅连接了风格迥异的前后关卡,技术上也避免了加载地图带来的中断感,可以给 double 赞。但对游戏来讲远远不止这些——大量星星奖励不仅增强了推倒 boss 的成就感,也是紧张的 boss 战完成后的一个心理的缓冲。这样一整个周期下来,由于衔接做得很好,(闯关 -&gt; 拿分 -&gt; 闯关 -&gt; 拿分)就像人的呼吸一样,整个游戏进程就非常有 <strong>节奏感</strong> ,也能充分调动玩家的情绪。为什么形成节奏感这么重要?当开始游戏后,玩家被带入这个节奏越深,在玩家飞机被爆的那一瞬间情绪冲击就会越大。什么情绪呢?就是你正在舒畅地呼吸的时候,突然被捏住鼻子的感觉。情绪冲击越强,玩家就越会在此时充钻消费。(跟日常充钻的理性算性价比相比)此时的消费是 <strong>纯粹的情绪化消费</strong> 。行文至此,我感觉其实这里还可以更激进一点,就是复活钻的消耗动态化。比如 boss 剩一口气的时候玩家爆了,可以适当压低复活钻需求量,满足玩家的补偿心理,反之亦然。当然了,非专业人士,也就是顺口一说。</p> <ul> <li><strong>[+2] 改版前的僚机(宠物)系统做到了简单和灵活</strong></li> </ul> <p>全民打飞机改版前的僚机系统倒是值得一说(改版后新增的圈钱套路不提也罢)。回顾我以往对空战游戏的制作考虑,还是程序员的思路,总想着找机会把AI写炫一点,搞搞护航,包围的花样什么的,现在反省一下,花样太多了就会华而不实,下达指令难以做到简单直观,操控本体和指挥僚机的节奏感一般玩家也较难接受,从设计实现到实际体验都不够成熟,反不如老老实实的跟在玩家的飞机后面跑跑龙套。还是那句老话,不应该为了技术而技术。当然话又说回来,时代在进步,没有一成不变的设计,当时不合适也不能说明以后也不能这么干,呵呵。</p> <ul> <li><strong>[+3] 社交化的飞行距离</strong></li> </ul> <p>社交化的元素不少,为什么要把这一条单独拎出来说呢?社交化是个大家喜欢挂在嘴边的热门词汇。仿佛微信就是万能催化剂,不管什么游戏,一导入朋友圈,做做积分排行榜,就社交化了,就用户黏性了。这种看法是不科学的。你的朋友圈里那些跟你一起打飞机的朋友,都是那种大家比着充钻,你一千我就两千,就为了在排行榜上傲视群雄的人吗?(什么,你说的确如此?嗯土豪我们交个朋友吧)我相信对大部分人来说,虽然排行榜占据了最醒目的主屏中心位置,来自积分排名和比较的消费激励实际上是偏低的。还记得老罗说什么吗?“我不是为了输赢,我就是认真”从这里引申出来的一个比较普遍而又隐秘的心理就是“要是充钻拼积分,就为了比这帮人分高,多傻x多low啊,又花钱又降逼格的事儿我可不干”老罗看来很清楚这一点,拿捏得也很到位(注意,他选的词是“我不是为了输赢”,而不是“我不在乎输赢”)嗯,回过味来了吧。绕了一圈还是回到原来的主题上,全民飞机里,每当你超越一个好友的飞行距离的时候,系统会用醒目的水平虚线提示你超越了xxx,屏幕左上方会出现“下一个可以超越的对象+不断缩短的距离显示”。那为什么这个社交化就比那个好呢?因为这种设计把你的好友(在不需要实时互动的前提下)很自然地 <strong>融合在你当前的情境</strong> 下了,这就是“比距离”和“算分数”之间微妙又显著的不同。这种持续的情绪引导有别于boss战,但也是游戏整体节奏感的重要一环。这一技法是哪个游戏最先实现的我不知道,不过好的设计,被这类游戏纷纷仿效也是很自然的了。</p> <p>其他的内容,比如活动,PK什么的,由于没啥特色,也就略去不提。 作为飞机游戏,可选的主打飞机在差异性上做的不够,难以激发玩家的收集欲,这些不足,不说也罢。 嗯,先这样吧。</p> <p>[注]</p> <ul> <li>2016-07-09 在把此文放到<a class="link" href="https://cowlevel.net/article/1827487" target="_blank" rel="noopener" >奶牛关</a>的同时,简单修订了一下,顺手配了张图。</li> </ul> 2014.05 基于插件的引擎设计 https://gulu-dev.com/post/2014-05-16-plug-in-based-engine-design/ Fri, 16 May 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-05-16-plug-in-based-engine-design/ <p>这篇小文是我读“<a class="link" href="http://bitsquid.blogspot.jp/2014/04/building-engine-plugin-system.html" target="_blank" rel="noopener" >Building an Engine Plugin System</a>”(需翻墙)随手留下的一些笔记。</p> <p>开始正文前,先无责任评论两句吧。<a class="link" href="http://www.bitsquid.se/index.html" target="_blank" rel="noopener" >bitsquid</a>是一个强调动态(各种reload),极简设计(接口知识最小化),轻量级(核心不到20万行)的引擎,跟俺口味很贴近,所以俺一向比较关注他们的动向。这是个<a class="link" href="http://www.bitsquid.se/company.html" target="_blank" rel="noopener" >小团队</a>,虽然网站上没啥信息,引擎也未公开放出,可是blog的水分很少,质量比较高,是俺的菜。</p> <p>按照传统套路,先啰嗦一番“非插件化设计”的弊端:</p> <ul> <li>想扩展现有行为,就得修改代码,重编引擎。(隐含工作量太大——获取全部代码和依赖的库,架设所有build环境)</li> <li>一旦开始有了本地改动就得维护之,一旦pull了新版本就得merge,这个merge的工作量,随着本地改动的增加和时间的推移,不断上涨。</li> <li>本地的改动是基于当时那个snapshot的代码逻辑,有很多隐含的假设,随着时间的推移和引擎的重构,这些假设会被破坏,导致各种莫名的问题。(最常见的是本地行为依赖了一个内部数据结构,结果那个结构被重构甚至被去掉了)</li> <li>所有本地改动很难与其他人共享。实践上,最多就是打个patch发一下,难以整包发布。</li> </ul> <p>插件的好处就不细说了,基本上因为双方依赖的是显式的API,这种紧密关系就被解耦了,各种好处。</p> <p>先说下naive的做法——插件定义和导出一些标准的函数入口,引擎在特定时间调用。 看上去基本上是下面这样的:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kr">__declspec</span><span class="p">(</span><span class="n">dllexport</span><span class="p">)</span> <span class="kt">void</span> <span class="n">init</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="kr">__declspec</span><span class="p">(</span><span class="n">dllexport</span><span class="p">)</span> <span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="kt">float</span> <span class="n">dt</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="kr">__declspec</span><span class="p">(</span><span class="n">dllexport</span><span class="p">)</span> <span class="kt">void</span> <span class="n">shutdown</span><span class="p">();</span> </span></span></code></pre></td></tr></table> </div> </div><p> <br> 当插件要用引擎的功能时,通过SharedDLL来实现。</p> <p><img src="https://gulu-dev.com/post/2014-05-16-plug-in-based-engine-design/plugin_shared_dll.png" width="315" height="191" srcset="https://gulu-dev.com/post/2014-05-16-plug-in-based-engine-design/plugin_shared_dll_hu10d186f027107cd7634d56769afb000e_5801_480x0_resize_box_3.png 480w, https://gulu-dev.com/post/2014-05-16-plug-in-based-engine-design/plugin_shared_dll_hu10d186f027107cd7634d56769afb000e_5801_1024x0_resize_box_3.png 1024w" loading="lazy" alt="plugin_shared_dll.png" class="gallery-image" data-flex-grow="164" data-flex-basis="395px" ></p> <p>一些传统引擎(比如Ogre)就是这么实现的,某种意义上讲,Windows本身也是如此。</p> <p>这么设计的问题是引擎的设计者很被动——插件要啥功能,就得把啥放到SharedDLL里去。搞着搞着发现所有东西都进去了。暴露出去的接口变得臃肿,很多小秘密也被插件知道了。这样一来重走老路,修改的破坏性风险又急剧增加。</p> <p>改良的办法是通过lua等脚本来作为天然的api屏障,这也是各种MMO的典型做法。但这种也有局限性,就是插件需要取得一些底层的东西时比较难办(给还是不给呢?)。对游戏引擎的设计来讲,大部分情况下写插件的人是程序,两边都是C++,中间用lua会很啰嗦和低效,有点儿没事找抽的感觉。</p> <p>本文的方案是基于一种C的接口查询的方法(C++的ABI,唉,不提也罢)。 与让插件链接到一个提供引擎API的DLL,不如直接在插件初始化的时候把引擎API传过去,如下所示: (小声说一句,这思路其实是抄Linux的)</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="s">&#34;plugin_api.h&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">EngineApi</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">spawn_unit</span><span class="p">)(</span><span class="n">World</span> <span class="o">*</span><span class="n">world</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">float</span> <span class="n">pos</span><span class="p">[</span><span class="mi">3</span><span class="p">]);</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">EngineApi</span><span class="p">;</span> </span></span></code></pre></td></tr></table> </div> </div><p>一个头文件搞定,插件只要包含这个文件就行,不用链接任何dll。插件只需要在入口处获得(引擎提供的)这个结构的指针,然后想要什么功能就可以直接调进去了。</p> <p>嗯,为了方便演化和兼容性,加个版本控制。为免牵一发而动全身,可把这些接口根据逻辑上的相关性,拆成多个结构,各自独立演化,如下所示:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="s">&#34;plugin_api.h&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="cp">#define WORLD_API_ID 0 </span></span></span><span class="line"><span class="cl"><span class="cp">#define LUA_API_ID 1 </span></span></span><span class="line"><span class="cl"><span class="cp"></span> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">World</span> <span class="n">World</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">WorldApi_v0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">spawn_unit</span><span class="p">)(</span><span class="n">World</span> <span class="o">*</span><span class="n">world</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">float</span> <span class="n">pos</span><span class="p">[</span><span class="mi">3</span><span class="p">]);</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">WorldApi_v0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">WorldApi_v1</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">spawn_unit</span><span class="p">)(</span><span class="n">World</span> <span class="o">*</span><span class="n">world</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">float</span> <span class="n">pos</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="kt">float</span> <span class="n">rot</span><span class="p">[</span><span class="mi">4</span><span class="p">]);</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">WorldApi_v1</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">lua_State</span> <span class="n">lua_State</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="nf">int</span> <span class="p">(</span><span class="o">*</span><span class="n">lua_CFunction</span><span class="p">)</span> <span class="p">(</span><span class="n">lua_State</span> <span class="o">*</span><span class="n">L</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">LuaApi_v0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">add_module_function</span><span class="p">)(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">module</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="n">lua_CFunction</span> <span class="n">f</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">LuaApi_v0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="kt">void</span> <span class="o">*</span><span class="p">(</span><span class="o">*</span><span class="n">GetApiFunction</span><span class="p">)(</span><span class="kt">unsigned</span> <span class="n">api</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="n">version</span><span class="p">);</span> </span></span></code></pre></td></tr></table> </div> </div><p>现在插件只要提供模块ID和版本号,就可以用 GetApiFunction 来获取对应的引擎功能了,具体代码可看原文,比较长就不贴了。</p> <p>反过来插件暴露给引擎的API,也是如法炮制。各种小好处和细节,略去不提。</p> <p>以上就是 <strong>基于C的API协议定义技术</strong>,在插件设计里的应用。</p> <p>这个结构本质上很简单,A也不依赖B,B也不依赖A,咱俩都依赖协议,私下里就可以没负担地随便改了——只要别破坏协议就行。 这种方式比所谓的 DLL 显式链接(GetProcAddress + &ldquo;FunctionName&rdquo;)依赖更少,因为显式链接除了签名一致以外,还依赖函数名一致。 这样也有动力保持协议最小化了——协议越简洁,双方的发展自由度也就越大,将来潜在的矛盾和冲突也就越小。(小一点,再小一点。各种这一类的梗可见 <a class="link" href="http://blogs.msdn.com/b/oldnewthing/" target="_blank" rel="noopener" >The old new thing</a>,微软历史上各种被坑的经历,不要重演,呵呵)</p> 2014.04 为什么从本质上讲,渲染逻辑不适合放到子线程中去? https://gulu-dev.com/post/2014-04-01-threaded-rendering/ Tue, 01 Apr 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-04-01-threaded-rendering/ <p>按: 本文成文于一年前 [2013-01-13],相关的 bug 修起来没完,遂成文,[2014-03-20] 整理旧文档时稍作修订。</p> <p>为什么从本质上讲,渲染逻辑不适合放到子线程中去?</p> <p>因为渲染是一个需要大量的 CPU 和 GPU 同时参与的操作,无论如何组织架构,在事实上均难以避免高频和实时的较大数量的数据的更新和传输,这是渲染(尤其是 <strong>大量动态物体的数据在不断被改变</strong> 的情况下)的本质决定的。</p> <p>从实践上看,那些线程化渲染(threaded rendering)的常见做法中,勉强去做每帧同步,或隔帧同步,或延迟1-N帧同步,均会导致大量的细节被暴露给整个架构。此亦从根本上违反了软件开发封装的基本思想——任何一点底层的改动(只要涉及到数据同步和交换),均需要对相关系统的大部分运行机制有充分的了解,否则极易造成线程间的延迟,starving(可勉强译为饥饿,不知正解)或不必要的等待。整个系统充斥着不相干系统间的隐形依赖,从而变得僵化和脆弱,及不可避免的复杂度剧增。</p> <p>请注意,我不是在说线程化渲染性能不好。正相反,把渲染从主线程拆出去能迅速看到效果,毕竟跟数据同步带来的开销相比,节省下来的hard stall(可勉强译为“硬卡”,指难以从调用方角度优化的卡顿)从毫秒数上要大得多。为了获得这种好处,人们通常很难抗拒这么做(正如 Unreal 引擎的实现)。与之相对的是,由于绝大部分项目的主线程代码是一整坨游戏的逻辑,几乎毫无阻碍地直接访问所有的子系统和数据,这就意味着在一个已有系统中把游戏逻辑切割后分出去(若不从头设计的话)事实上近乎不可能。</p> <p>摘录一段 John Carmack 的观点(<a class="link" href="http://fabiensanglard.net/doom3/interviews.php" target="_blank" rel="noopener" >原文链接</a>)吧:</p> <blockquote> <p>The Doom 4 codebase now jumps through hoops to create the game window from the render thread and pump messages on it, but the better solution, which I have implemented in another project under development, is to leave the rendering on the launch thread, and run the game logic in the spawned thread.</p> </blockquote> <p>显然,卡神在新的项目(Doom 4之后)里做到了把逻辑从主线程里切出去。从实现上讲,这会需要更高的开发人员的素质(做具体逻辑的同学需要有较强的并发意识,消息传递和数据访问的控制力);而从架构上讲,这对整个系统是有很大好处的,主要的子系统被逐步地解耦合,从根本上降低了整个项目的复杂度。考虑到随着时间推移,单个机器上处理器的数目不断在增长,提高各个子系统的独立性和内聚性(相比“一个需要繁重数据同步的渲染线程和一个臃肿的主线程”而言)在将来会带来更多的性能优势。</p> <p>关于在这种情形下模块的设计,云风也曾有过很不错的论述(<a class="link" href="http://blog.codingnow.com/2012/01/libuv.html" target="_blank" rel="noopener" >原文链接</a>):</p> <blockquote> <p>接口的最小知识表达就是用一致的 C 函数调用约定&hellip;应该是无全局相关状态的。这不仅仅是为了线程安全,而是可以保证没有隐式约定(额外的知识)。 &hellip;一个独立模块需要解决的问题,通常对外界的信息交换应该是低频的,它应该是可以独立工作解决更复杂的问题的。而不应该是不断的要求外部告知它新的状态变换。</p> </blockquote> <p>此处所谓的隐式约定即是前文中提到的“不相干系统间的隐形依赖”。而上文提到的“切割主线程的逻辑模块”这一过程,可以促使程序员对“数据是如何在子系统间交换和更新”有更深的认识(是把人家的数据直接拿来用,拿来改?还是经由统一定义的接口函数调用?亦或是异步收发消息?)对这些问题的考察和理解,将有助于程序员持续地做出有利于整体设计的重构。</p> <p>简单地说,我们希望放进线程里的东西有两个特点:</p> <ol> <li>高度内聚的独立模块(如封装良好的物理系统或AI)这样良好定义的系统与外界数据交换的特点是低频低带宽(low frequency, low bandwidth)。</li> <li>高强度地使用某个单一的系统资源 (如CPU密集或IO密集)。</li> </ol> <p>按照这个特征的符合度,常见子系统大致可按如下排列</p> <p>音频 &gt; 硬盘IO &gt; 网络IO &gt; 物理模拟 &gt; 一般游戏逻辑的模拟 &gt; 输入设备响应(对应键盘鼠标事件)和用于渲染的视频输出</p> <p>这意味着(理想情况下)我们应该只在主线程保留“输入设备的响应”和“屏幕渲染输出”这两样。巧合的是,这两样恰恰是游戏内玩家对延迟最敏感,对实时性要求也最高的系统。整个系统的设计也变得足够简单和清晰:主线程运行在所能达到的最佳帧率上;输入和输出无延迟;实时性的资源在主线程分配和释放(线程间的数据同步最小化);每个子系统以各自理想的更新频率工作(线程间的消息通信最小化)。</p> <p>当然,利用闲置的GPU资源进行通用处理,则不在此列。但在设计时仍需非常小心,这类运算 <strong>更宜降低数据交换的频率(尤其是写回)</strong> ,避免对总线的长时间占用,避免经由颠簸效应影响到GPU的本职工作——渲染。</p> <p>最后再说一句,从系统设计的角度讲,上述讨论也基本适用于 <strong>独立出单独的进程</strong> 的情况。 再多罗嗦一句,对于有些年头的项目,这样的重构可能不太现实,性价比不高;而对于新开的项目,可以是一个供参考的思路。</p> 2014.03 (译) 不要说谎 (Don't lie.) https://gulu-dev.com/post/2014-03-22-dont-lie/ Sat, 22 Mar 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-03-22-dont-lie/ <p>按:本文是两年半前翻译的一篇文章,译于[2011-11-05](修订于[2014-03-22]),原文发表于[2011-10-04],出自 DEADC0DE 的 <a class="link" href="http://c0de517e.blogspot.com/" target="_blank" rel="noopener" >blog(需翻墙)</a>,原文在<a class="link" href="http://feedproxy.google.com/~r/C0de517e/~3/nga0PFlxMcw/dont-lie.html" target="_blank" rel="noopener" >这里(需翻墙)</a>。</p> <p>现在我仍记得,初读此文时,脑海中反复回响着的林锐博士的一句话:“做一个真实,正直,优秀的科技人员。”</p> <p>在漫长的职业生涯中,一个程序员会不断面临各种主观和客观的压力,前者比如视野,品味,学习能力和天然惰性,后者比如项目进度,主管偏好等等。能不能在面临这些实实在在的压力时,仍有一丝不苟的操守,认真对待自己写下的每一行代码,这对于有追求的程序员几乎总是一个艰难的选择。我相信,凡是热爱编程的程序员,都曾有过这个体验——必要的时候,哪怕小到给一个临时变量命名,却还反复斟酌,直至茶饭不思,甚至于“吟安一个字,捻断数根须”。是的,不是每个程序员都能成为卡马克,但并不妨碍他为这个世界增加一段 <strong>真正</strong> 有用的代码。</p> <h1 id="不要说谎dont-lie">不要说谎(Don&rsquo;t lie.)</h1> <p><strong>代码的签入并不意味着任务的完成。</strong></p> <p>对产品而言,“完成”是指:</p> <ul> <li>签入</li> <li>足够稳定(通过自动化测试)</li> <li>符合指标(内存和性能符合要求)</li> <li>可用性(配套工具和参数的提供)</li> <li>已通过验证(相关的美术,策划和你的程序主管)</li> </ul> <p>如果忽略掉后面的四条,想发布产品就是痴人说梦了。必然的,你会被没完没了的加班带来的压力逼疯,最终发布一个始终达不到质量标准的次品。 这也意味着在开发产品的时候,我们总是从一个稳定的,可靠的,符合性能指标的原型出发,并在整个开发过程中不断地(通过测试等手段)去保证它始终不被打破。 没错,照这样来,会让你会花双倍的时间完成同一个任务。有时候,为了“真正地”完成某个任务,你甚至必须花时间去调整各个相关的子系统,另外腾出地方来把它塞进去。</p> <p>当然,逃避所有这些很简单——说谎。</p> <p>游戏行业是个充满创造力的行业。身处这个行业中,由于时常面对各种变化,我们很少循规蹈矩地事先做好所有的计划。 通常我们在开发一个产品时,没法制定一个确切的时间表。更常见的是,手里有个原型(demo)我们就抄起家伙兴冲冲地开始干了。</p> <p>想象一个团队一同画一副画的情境。如果一个人画手指,一个画眼睛,还有个在画鼻子,你就不该抱着侥幸心理,指望这些东东能正正好好拼到一起。让他们全部在一个指定的日期前完工就更难了。要是少个耳朵怎么办?项目发布前让所有人一起加班到深夜画这只耳朵?</p> <p>好吧,傻子才这么干。可事实上很多游戏公司都是这么干的。他们不明白各种因素是相互作用的。任何改动都不是局部的,静态的。 <strong>整个系统是有机的,每个改动都要贴合它对应的上下文情境</strong> 。在画布上,你画上去的每一笔都要有个明确的意义。</p> <p>在绘画的时候,你总是从一个草图开始,慢慢加工成一件艺术品,自始至终,不管完成了多少,它都是一副(概念上)完整的画,不是堆在一起的一堆不相干的零件。在这个过程中的任何时候你停下来,它都是一幅画,也许不够精细,细节也不够多,但它是完整的一幅画。</p> <p>游戏也是一样,只消看它能不能正常运行。如果它时常宕机或者吃光内存(亦或性能达不到指标),就算不上是游戏,只是一堆冒着烟的二进制文件。</p> <p>项目开发中的历次迭代(在任何时候)都不应打破产品的质量标准。</p> <p>自始至终,它都应能够顺利运行。</p> <p>补记: 有同学可能会觉得这位老兄不太现实。坦率地讲,我也很清楚,要是按这标准去衡量的话,国内的团队基本都得歇菜。就一条吧,敢问哪个团队能做到,在研发过程中,产品的关键性能指标始终能维持在“可对外”的水准?我承认,在如今游戏行业的土壤里,这种做法的确缺乏可操作性和可推广性,可这并不代表这种理念本身是错的。如果阅读这篇文章能让你思考自己所在团队的开发模式,利弊得失,进而萌生改进的意愿和思路,那我翻译这篇文章的目的就达到了。</p> <p>附原文:</p> <h1 id="dont-lie">Don&rsquo;t lie.</h1> <p>c0de517e|DEADC0DE</p> <p>A task is not done when its code gets checked-in. In production, &ldquo;done&rdquo; is:</p> <ul> <li>Checked-In</li> <li>Stable (passes automated tests)</li> <li>In-budget (memory and performance)</li> <li>Usable (tools, parameters)</li> <li>Verified (art-directior or designer or lead programmer)</li> </ul> <p>If you ignore the last four, you&rsquo;re living in a dream. A dream in which you will ship the game. Instead, you&rsquo;ll die under the pressure of crunch time and ship a flawed product that does not match the quality standards it should.</p> <p>That also means that in production we start with a stable, in-budget product. And that we do have means of verifying that this is true for the entire production (tests).</p> <p>Yes, it will take twice as much time to finish a task. Yes, it will mean that some tasks can&rsquo;t be declared done until you make more space somewhere else, to fit them in.</p> <p>Of course you can avoid all this. All it takes, is to lie.</p> <p>We are a creative industry. We have to deal with change, we don&rsquo;t plan things up ahead and then waterfall until they are all done (not <em>most</em> of the times at least, there are situations in which that applies).</p> <p>We don&rsquo;t craft a product by following a plan, we make drawings and sketches (prototypes) and then take a canvas and start painting. You don&rsquo;t have a person detailing a finger, another one refining an eye, another working on the nose and then hope that everything will stick together just right. Or hope that you will have all the parts done by a given date! And what if it then misses one of the ears? What do you do, take ten artists working on that ear till midnight every day near the deadline?</p> <p>Only an idiot would do that, and still many game companies work like that, they don&rsquo;t consider that everything has to fit just right, and that change is not local, every change has to fit with the entire context, every brush stroke has to make a sense in the entire painting. You start with a painting, a rough one, and then refine it, and at all time the painting is a painting, it&rsquo;s not a collection of unrelated pieces. You can stop at any moment and it will be still a painting, maybe not that refined, maybe not as intricate and detailed as you wanted, but it&rsquo;s a painting.</p> <p>A game is such only if it runs. If it crashes or goes out of memory on the target platform, it&rsquo;s not a game, it&rsquo;s some binary that crashes. If it does not hold its performance, it&rsquo;s not a game, we can&rsquo;t burn it to a disc and call it a game, it won&rsquo;t be shipped, it won&rsquo;t pass certification. Iteration should not break this quality, it should go from a &ldquo;shippable&rdquo; game to a &ldquo;shippable&rdquo; game. Especially during production.</p> 2014.03 客户端动态预测技术和延时补偿技术 https://gulu-dev.com/post/2014-03-15-dynamic-prediction-and-latency-compensation/ Sat, 15 Mar 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-03-15-dynamic-prediction-and-latency-compensation/ <p>以前在做飞机游戏 (Wings of Fate) 的时候,实现过一个改善同步的技术,用来降低高ping客户端的延时感受(客户端的动态预测和延时补偿技术)。前两天跟朋友L聊天时表达了一下,觉得有记录下来的价值,遂有此文,以备日后参考。</p> <p>有A和B两个客户端,在B移动时,A的客户端上会以一定频率收到B的移动信息同步。如果总是等到收到B的信息后再去被动地移动,那就会导致在A的机器上,B的移动总是滞后的。这一般是典型MMO如WOW的做法,而对某些类型的游戏(fps,空战)来讲,这种延迟是难以接受的。</p> <p>一个做法是,A总是通过B之前的移动去预测其接下来的移动情况(Q3的做法),这样有极大的可能(实测至少90%以上的情况下)在服务器把B的真实移动信息发过来时,双方是匹配的(也就是预测准确)。在这种完美情况下,在A机器上体验到的B的移动就没有滞后(这个“没有滞后”是相对服务器而言的,因为所有的计算以服务器为准,因此此处暂不考虑服务器跟B之间的延时)。预测本身可以通过缓存之前若干秒的操作队列来实现,注意仅依靠当前的状态是不够的。</p> <p>这种预测会在B有新的操作事件发生的时候失败,而这里的处理与Q3稍有不同。比如B正在往前飞,突然松开了W键开始减速直到停下。在未收到B的减速及停止消息前,A仍保持了B在全速往前飞的预测,此时若收到了减速的信号(同步过来的加速度突然变为负向的,速度开始变化)此时A可以意识到,自己坐标系中的B已然偏离了正确的位置。那么可以采用一个补偿算法去修正B。修正的幅度可以参考当前客户端的延迟情况。这个算法可以是激进的(尽量迅速地校准,牺牲平滑性)或保守的(保证飞行的平滑性,牺牲修正速度)。</p> <p>当然了,前文中服务器与B(消息发送端)之间的延时,仍然对玩家体验有决定性影响。当客户端把自身的操作发给服务器时,这时的延时真正的决定了客户端的最快反应时间。这时网络环境的好坏,会直接影响玩家(在游戏中实际表现出)的反应速度。</p> <p>[2014-03-16] Update:</p> <p>补充几句话,怎么判断什么时候用这个预测和补偿,用的时候强度有多大呢?仍以前面的AB客户端为例的话,一般来讲约 100ms 的阙值即可(延时越敏感,客户端B的avatar移动速度越快,这个值应越低)。当A与服务器之间的延时(可定期roundtrip测得)高于阙值时,就可以开始缓存操作序列来做预测了。</p> 2014.03 C 语言学习的经典书籍有哪些? https://gulu-dev.com/post/2014-03-14-zhihu-c-books-recommendation/ Fri, 14 Mar 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-03-14-zhihu-c-books-recommendation/ <p><a class="link" href="http://zhi.hu/Z33t" target="_blank" rel="noopener" >[知乎回答] - C 语言学习的经典书籍有哪些?</a></p> <p>首先声明一下,我不是黑。如果是几年前,我会推荐 @王潜升 同学推荐的这几本(毕竟确是经典),但这几本书也不是啥都好,我具体谈一下吧:</p> <ul> <li>《C程序设计语言》 - 比较短小,跟《C++程序设计语言》的洋洋千页,娓娓道来的风格比较而言,应该说也是C的简洁凝练的体现吧。此书适合 <strong>有一定实践经验的人</strong> 作为一个全面熟悉和巩固语言的工具书, <strong>不是很适合初学者</strong> 用来了解和学习。当然了,本书有一定的历史意义,可以买一本纯收藏,亦或安慰一下自己,对K&amp;R稍表敬仰之寸心。总得来说,K&amp;R网上评价一直非常高,这一点我个人持保留意见。</li> <li>《C和指针》 - 不要被书名骗了,此书看似专说指针,实则是C语言较完整的语言和运行环境的描述。虽然有一些复制粘贴充篇幅的嫌疑(后面的字符串,数据结构,IO,标准库什么的显得有点大杂烩,好吧我真的不是黑, <strong>不过真的有必要把F1一下就能看到的文档都弄进来吗?</strong> ),不过对一些编译器的实现细节有一些探讨还是值得一读的。对了,有的练习题还不错。总得来说,值得买来略扫一遍。</li> <li>《C专家编程》 - 好吧,这个书里面有不少八卦和无厘头,适合宅男们消磨时间用。举个栗子吧,卡耐基梅隆大学的计算机系经常搞活动,有一次搞了个编程竞赛,实现功能巴拉巴拉巴拉,要求就一条——尽可能的快。我会随便说第一名消耗的时间是负数吗? <strong>是的,你没看错!这厮写的程序消耗了负数的时间!!</strong> 想知道真相吗?去找一本来翻翻吧,呵呵。再来一个吧,大家知道MIT人工智能研究室的宅男们整天都在研究什么吗?这些热情的家伙们用LISP去控制自己楼上的电梯升降( <strong>据说这货还能自检自己是不是真身</strong> ,免得被黑客利用了让自己人卡在电梯里出不来)。他们还专门设计了个网络协议(运行在七十年代的互联网上),用来查询楼道里的可乐机里有没有货,够不够凉(就为了少跑点路)。我勒个去,宅到这个地步,家里人知道吗?这下大家知道为啥人工智能多年来都没啥进展了吧。唉,控制不住啊,一说起八卦就停不下来。你是不是已经看到了谢耳朵同学的影子了?是的,我看这个书就有看《生活大爆炸》的感觉,可以买来乐一下。</li> <li>《C陷阱与缺陷》 这本书是典型的挑刺党了,不过在我看来,(可能是成书比较古老的缘故),它挑的刺 <strong>普遍不够硬,没啥杀伤力</strong> ,基本上都属于初级(勉强中级)错误。实打实写过几年C语言的同学应该明白我的意思, <strong>想看那些真正的缺陷和陷阱还是得在实际项目里找啊</strong> (我是说的那种一枪把自己的脚轰碎了的那种)。这书没啥好看的,谁要的话我五毛卖给他。</li> </ul> <p>有人问,你巴拉巴拉说这么半天,难道就没有本正常点的书,能够囊括以下所有特征的吗?</p> <ul> <li>完整翔实,细腻丰满</li> <li>不复制粘贴有凑字数嫌疑</li> <li>不是专业搞怪和无厘头</li> <li>不是专业挑刺党</li> </ul> <p>好吧我说的当然不是谭浩强。</p> <p>如果是几年前我只能双手一摊——我也不知道。不过有一次偶然间翻阅到这一本书,就产生了这种感觉——如果我是一开始读这个书入门的就好了,呵呵。</p> <ul> <li><a class="link" href="http://book.douban.com/subject/2280547/" target="_blank" rel="noopener" >C语言程序设计现代方法</a></li> <li><a class="link" href="http://www.amazon.com/Programming-Modern-Approach-2nd-Edition/dp/0393979504" target="_blank" rel="noopener" >C Programming: A Modern Approach, 2nd Edition: K. N. King: 9780393979503: Amazon.com: Books</a></li> </ul> <p>此书基本符合并超越了俺前面总结的四条,俺随便说几条吧:</p> <ul> <li>完整覆盖C99超越了K&amp;R。( <strong>够新</strong> )</li> <li>内容丰满不亚于《C++程序设计语言》( <strong>够厚</strong> )</li> <li>习题质量平均水准比较高。( <strong>比上面诸位高出不少</strong> )</li> <li>提供PPT讲义和在线教师资源( <strong>就是说你看不懂可以直接问教授,啧啧啧</strong> )</li> <li>探讨现代编译器的实现,揭穿了各种古老的C语言神话和信条( <strong>适合程序员的纯干货</strong> )</li> </ul> <p>这五条基本超越上面所有的前辈了。( <strong>用&quot;横扫&quot;这个词可能有点大不敬不过管他呢</strong> )</p> <p>另一本很短小的书,看没人提到我也说一下吧</p> <ul> <li>Writing Solid Code ── Microsoft Techniques for Developing Bug-free C Programs</li> </ul> <p>很久以前的,可以随便看看反正也不长,半个小时到一个小时就能看完。</p> 2014.03 人类历史上有哪些思维能力特别强的人?他们有哪些独特的思考方法? https://gulu-dev.com/post/2014-03-13-zhihu-answer-thinking-method/ Thu, 13 Mar 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-03-13-zhihu-answer-thinking-method/ <ul> <li><a class="link" href="http://zhi.hu/TwZv" target="_blank" rel="noopener" >[知乎] 人类历史上有哪些思维能力特别强的人?他们有哪些独特的思考方法?</a></li> </ul> <h3 id="john-carmack">John Carmack</h3> <p>John Carmack, 现代3D游戏的启蒙者,最杰出的程序员之一。 <a class="link" href="http://en.wikipedia.org/wiki/John_D._Carmack" target="_blank" rel="noopener" >John D. Carmack</a></p> <p>从他的代码来看,我认为他的逻辑思维能力是相当罕见的。 <a class="link" href="https://github.com/id-Software" target="_blank" rel="noopener" >id-Software (id Software) GitHub</a></p> <p>关于他的 Methodology(严格来讲,方法论比题主所说的思维方法更宽泛一些,但应该也不算偏题),卡马克曾说过一段话(出处:<a class="link" href="http://www.aeflash.com/2013-01/john-carmack.html" target="_blank" rel="noopener" >http://www.aeflash.com/2013-01/john-carmack.html</a>):</p> <blockquote> <p>Focused, hard work is the real key to success. Keep your eyes on the goal, and just keep taking the next step towards completing it. If you aren&rsquo;t sure which way to do something, do it both ways and see which works better.</p> </blockquote> <p>这段话曾使我受益良多,也许对你也会有所启发。</p> <hr> <h3 id="albert-einstein">Albert Einstein</h3> <p>[2014-03-13] 添加:</p> <p>今天又读了一下这个题目,发现题目问的是“有哪些”,就想到应该把爱因斯坦补充进来,让这个答案更完善。</p> <p>爱因斯坦,德国物理学家,相对论的奠基者。</p> <p>相关信息:(这三个链接的信息量都很丰富,我在里面不知不觉就逛了两个小时,慎入)</p> <ul> <li>(官方网站) <a class="link" href="http://einstein.biz/index.php" target="_blank" rel="noopener" >http://einstein.biz/index.php</a></li> <li>(Wikipedia介绍) <a class="link" href="http://en.wikipedia.org/wiki/Albert_Einstein" target="_blank" rel="noopener" >http://en.wikipedia.org/wiki/Albert_Einstein</a></li> <li>(Wikiquote引言) <a class="link" href="http://en.wikiquote.org/wiki/Albert_Einstein" target="_blank" rel="noopener" >http://en.wikiquote.org/wiki/Albert_Einstein</a></li> </ul> <p>我在下面的每一段前加了标签和我的简短理解,以便阅读和整理</p> <p>凡是引言均以&quot;引用+斜体&quot;的方式注明</p> <p><strong>(著名的公式)</strong></p> <p>爱因斯坦常对人说:学习时间是个常数,它的效率却是个变数,单独追求学习时间是不明智的,最重要的是提高学习效率。他认为必须通过文体活动,才能获得充沛的精力,保持清醒的头脑,爱因斯坦还根据自己的亲身体会,总结出一个公式,即A=X+Y+Z。A代表成功,X代表正确的方法,Y代表努力工作,Z代表少说废话。他把这个公式的内容,概括成两句话:工作和休息是走向成功之路的阶梯,珍惜时间是有所建树的重要条件。</p> <p><strong>(直觉-演绎思维方法)</strong></p> <p>爱因斯坦不仅是位物理学大师,也是一位研究思维的专家,在他的文集中有大量研究科学思维方法的论述。他考察了从亚里士多德的演绎推理到培根的归纳推理,再到牛顿的归纳和演绎、分析与综合相统一的思维方法后,提出了一种新的思维方法。他认为从特殊到一般的道路是没有逻辑的,是直觉的方法,从一般到特殊的道路是逻辑的方法。这样,爱因斯坦在逻辑方法与非逻辑方法之间保持了必要的张力思维,他本人称为&quot;直觉-演绎思维方法&quot;。</p> <p><strong>(张力思维方法)</strong></p> <p>张力思维方法与其说是一种科学研究中的方法,不如说是关于科学研究方法的评论,而不仅仅只有方法论的意义。&ldquo;直觉-演绎&quot;本身是无法操作的。它要求我们善于在归纳与演绎、推理与直觉等对立的两极保 持必要的张力,但没说清楚怎样保持这种张力,可以 把它形容为走钢丝一样的高超技艺,稍一偏离平衡,就会发生&quot;翻车&rdquo;。它与科学家个人学术素养及对问题的认识深度等因素相关。但在提出这种方法时,爱因斯坦对逻辑推理作用的评价,值得我们重视。</p> <p><strong>(聚焦思维模式)</strong></p> <p>(与前面提到的卡马克的方法颇有共通之处,把思维始终聚焦在要点上,专注,砍掉细枝末节,才能探查到问题的本质)</p> <p>在所阅读的书本中找出可以把自己引到深处的东西,把其他一切统统抛掉,就是抛掉使头脑负担过重和会把自己诱离要点的一切。</p> <p><strong>(想象力的重要性)</strong></p> <blockquote> <p><em>逻辑带你从A点到达B点,想象力带你去任何地方。</em><br> <em>Logic will get you from A to B. Imagination will take you everywhere.</em></p> </blockquote> <p><strong>(想象力的重要性)</strong></p> <blockquote> <p><em>想像力比知识更重要,因为知识是有限的,而想像力概括着世界上的一切,推动着进步,并且是知识进化的源泉。严肃地说,想像力是科学研究中的实在因素。</em></p> </blockquote> <p><strong>(提出问题的重要性)</strong></p> <blockquote> <p><em>提出一个问题往往比解决一个问题更重要,因为解决问题也许仅仅是一个教学上或实验上的技能而已。而提出新的问题、新的可能性,从新的角度去看旧的问题,都需要有创造性的想像力,而且标志着科学的真正进步。</em></p> </blockquote> <p><strong>(非常有洞察力的观点)</strong></p> <blockquote> <p><em>我们能够经历的最美和最深奥的感情就是神秘,它是所有真正的艺术和科学的源泉。</em><br> <em>The most beautiful and profound emotion we can experience is the mystical. It is the source of all true art and science. -Albert Einstein</em></p> </blockquote> <p><strong>(非常有洞察力的观点)</strong></p> <blockquote> <p><em>Innovation is not the product of logical thought, even though the final product is tied to a logical structure.</em><br> <em>创新不是由逻辑思维带来的,尽管最后的产物有赖于一个符合逻辑的结构。</em></p> </blockquote> <p>(此处译文似有误,似应译为“创新往往不是经由逻辑思维产生的果实,然而最终产物却能 <strong>展现出一副契合逻辑的构造</strong>。”)</p> <p><strong>(一个非常有效的用来检验自己学习成果的标准)</strong></p> <blockquote> <p><em>If you can&rsquo;t explain it simply, you don&rsquo;t understand it well enough.</em><br> <em>如果你不能把它简单地解释出来,那说明你还没有很好的理解它。</em></p> </blockquote> <p>这里我还挑选了几条跟三观方面的引言,严格来讲不属于思考方法之列,但三观的塑造会影响一个人的思维方式,这似乎可算是间接的启发吧。</p> <blockquote> <p>(世界观)<br> <em>The most incomprehensible thing about the world is that it is comprehensible.</em><br> <em>这世界最无法理解的事情是它是可理解的。</em></p> <p>(人生观)<br> <em>Life is like riding a bicycle. To keep your balance you must keep moving.</em><br> <em>人生就像骑单车。想保持平衡就得往前走。</em></p> <p>(价值观)<br> <em>Try not to become a man of success, but rather try to become a man of value.</em><br> <em>试着不去做一个成功的人,而去做一个有价值的人。</em></p> </blockquote> <p>以上。</p> 2014.03 迁居 · 珠海 https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai/ Wed, 12 Mar 2014 00:00:00 +0000 https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai/ <img src="proxy.php?url=https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai/zhuhai.jpg" alt="Featured image of post 2014.03 迁居 · 珠海" /><h2 id="正文">正文</h2> <p>谢谢关心我们的朋友们。自从决定迁居南方以来,这段时间我已经向不同的朋友们解释过不少次“为什么我们决定搬家到珠海”,想了想觉得还是写篇blog方便答复,也算是一个简单的记录。</p> <p>要不是同事在一起开玩笑,我还真的没发觉,到后天 <code>2014-03-14</code> 为止,不知不觉在上海已经待了整整九年了。</p> <p>九年前的此刻 <code>2005-03-14</code> ,我刚刚拿到育碧的 offer,心怀期待地来到魔都这个服务器,新建了个“游戏开发”的角色,开始漫漫的打怪升级路。也曾一个人打怪,掉过宝箱捡过装备;也曾乱点技能树,翻过九阴残篇;也曾跟朋友组队下副本被灭团。一晃这么些年,当年的懵懂少年现在已是眼镜大叔。蓦然回首,成功的喜悦没怎么体验,失败的教训倒是攒了满满几大筐(好吧P叔的原话俺改了改借用一下)。</p> <p>呃,写着写着就跑题了,快把自己带到沟里了。</p> <p>一到三十就回忆,停不下来啊。</p> <p>其实决定搬家的过程并不长,算不上是“一个艰难的决定”。原因也很简单,主要是为了全家人的健康,为了一份更好的阳光,空气,和水。魔都这两年的气候每况愈下,雾霾日渐增长,我想在这边生活着的朋友们,或多或少都有体会。随着小家伙一天一天长大,给孩子一个更好更健康的成长环境的愿望,也变得越来越强烈。毕竟,我们 80 后这一代,在外多年,早已习惯异乡的水土,可是孩子还小,理应拥有一个呼吸着新鲜空气,看得清满天星空的童年。</p> <p>生活上的变化嘛,可能通勤时间会少一些。现在在上海乘地铁上下班,每天两个小时妥妥的(开车的话更久)。在魔都,这个时间开销也不算夸张。记得刚到上海时,租住在离公司不到一公里的公寓里,这回可以重温一下了。相比魔都的喧嚣浮华,珠海是个简单而恬静的海滨小城。因为一向不习惯都市的夜生活,对我来说,闲暇时,反正也是读读书写写代码,其实变化不大。唯一的区别,是会比以前更有动力出去爬山跑步和骑车了,想想还蛮期待的,呵呵,希望自己不要犯懒就好。</p> <p>在魔都工作的这些年,认识了不少行业内的朋友,其中的一些让我满心钦佩,也有一些令我心怀感激,当然也少不了一些谈得来的深交和意气相投的兄弟。从你们身上,我得到了很多的启发和经验,这些你们给予的力量,让我时常心怀感激。来日方长,行业却很小,我深信,将来有的是并肩作战的机会。</p> <p>晚安,上海,谢谢侬。</p> <h2 id="补记">补记</h2> <ul> <li><code>2014-03-15</code> Update <ul> <li>有同学询问工作方面的情况,因为正在处理交接事宜,所以目前可说的也不多。唯一想说的是离开CCP这家伟大而疯狂的公司(前同事 Horace 语,深表认同),让我非常的留恋和不舍。在我曾工作过的组织中,CCP作为一家来自冰岛的游戏公司,反而是一个真正地给了像我这样的程序员家一般感觉的地方。想到在这里结识并一起工作的朋友们,觉得很开心,也祝他们顺利。</li> </ul> </li> <li><code>2022-06-11</code> Update <ul> <li>时光飞快,来珠海转眼八年了。这个博客之所以诞生,纯粹是因为当年迁居珠海,亟待逐一向好友解释,索性写一篇 blog 可以从容把话说清楚。结果没想到,转眼八年间,这里的文字竟未间断,不经意间积累了约六十万字,共一百多篇博文,主要涉及到游戏,互联网和区块链等不同行业的技术和管理等驳杂不一的内容。我自己也从一个游戏行业的程序员,逐渐蜕变为一个成长进取中的小公司管理者。这个博客记录下了这些年里我的成长轨迹,希望它透过时光,能对自己和他人提供有价值的信息。</li> </ul> </li> </ul> <p><img src="https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai/zhuhai2.jpg" width="720" height="400" srcset="https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai/zhuhai2_hu0b0e4a95bedc5fdc221c86e5ec852d61_50221_480x0_resize_q75_box.jpg 480w, https://gulu-dev.com/post/2014-03-12-moving-to-zhuhai/zhuhai2_hu0b0e4a95bedc5fdc221c86e5ec852d61_50221_1024x0_resize_q75_box.jpg 1024w" loading="lazy" alt="美丽的珠海渔女" class="gallery-image" data-flex-grow="180" data-flex-basis="432px" ></p>