MoreRSS

site iconCodingNow | 云风修改

coder ( c , lua , open source ),不用微信和QQ的大佬。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

CodingNow | 云风的 RSS 预览

对数和自然对数的底

2025-04-10 22:52:40

最近读了一本书:《数学的雨伞下》。阅读体验非常好,这本书用浅显的语言,科普了许多深刻的道理。这本书所介绍的知识结构比较类似我挺喜欢的另一本《从一到无穷大》,但讲解更为细致一些,以至于如果事先明白这些知识,甚至会觉得有些冗长。但细细品味,会觉得理解能更深一层。

我在通读完一遍之后,这几天带着儿子精读。重读第一章中“对数之桥”一节时,我思考了一个问题:当年纳皮尔 Napier 到底出于什么动机制作一张高精度对数表,他制表的计算思路是怎样的。书中并没有答案,所以我又在互联网上翻看了当年 Napier 原著 Mirifici Logarithmorum Canonis Descriptio 的介绍,感觉收获颇丰。

制作对数表的直接原因当然是为了简化大数乘除法的计算。对数概念的提出在幂概念建立之前,而现在的数学教学中,一般却是从幂自然推导到对数的。似乎后者才是自然而然的。这应该是因为,古人研究数学,最初是为了解决现实中的问题。所以,乘法必须有对应的几何意义。比如,计算正方形面积需要把计算边长的平方;立方体的体积需要计算边长的立方。更高次的幂却难有对应的几何意义,有理数幂则更为抽象。

现实中,也很难碰到极大的数字,超乎寻常的精度需求也很小,除了天文学。

人无法以上帝视角在宇宙空间中做测量,只能以地球为基点。所以,天文尺度的计算都依赖三角学。把天文(以及地理这种地球尺度的)数字问题化为三角函数,然后再加以计算。比如,测量地球到太阳的距离、地球到月球的距离、地球的直径都是这样。因为这些尺度都非常大,如果计算精度不够,就容易失之毫厘,差之千里。

为了测算太阳系内天体的距离,可以在地球表面找两个尽可能远的点(最多相距地球的直径),观察天体,记录下天体在视野中的角度。这样,地球表面的两个端点和被观察的天体,就构成了一个三角形。三角形的底边就是两地的距离,而顶角则可以对比两地观测的结果得到。这就是三角视差法。可想而知,对于太阳系内的天体,这个视察角度非常小,需要极高的观测精度和计算精度才能计算出距离(远大于地球直径)。

甚至,这个方法可以运用到测量附近恒星到地球的距离。这几乎是人类利用三角法能测算的最大尺度。在地球表面找两个点已经不够了,因为那最长不超过地球的直径。更长的标尺只有地球绕太阳的轨道:在一年中隔半年做一次观察,这两个观测点在宇宙空间中就隔了地球和太阳距离的两倍长,这总该够长了吧?其实不然。在这个尺度上,古人依然观察不到星星的位置相隔半年的星图中有所不同。这也是为什么日心说提出后,不光是神学家不接受,连天文学家(比如第谷)也不接受。

如果地球围着太阳转,而地球距离太阳如此之远,那么就算是恒星离得再远,地球位于太阳两侧时,总能观测到某些明亮(离我们相对较近)的星星位置有些许偏差吧?人类难以相像太阳系外的宇宙如此空旷。事实是,太阳以外的恒星离我们真的太远了,即使以地日这种天文距离为底边,和附近的恒星形成的等腰三角形的顶角也不到一秒。过去的测量工具的精度是完全不够用的。直到 19 世纪中叶(哥白尼死后 200 多年)人类才真正观测到天鹅座61/贝塞尔星 有 0.3 个秒视差,从而估算出离地球大约 10 光年左右。

测量精度是一方面,计算精度也很重要。在三角公式里算几个乘法,若是通过对数方法转换为加减法计算,而精度不够的话,恐怕结果会差上一个数量级。

纳皮尔在没有幂概念的基础上就发展出了对数概念,靠的还是寻求其几何意义。他的灵感来源并不是幂运算,而是三角公式。三角和差公式中,角度相加被转换为三角函数的乘法运算,这提示着,乘法和加法之间可以相互转换。纳皮尔的对数表也并不是现在意义的列一系列数字,逐个列出它们的对数。而是给出角度的三角函数值的对数。它可以看成是当时已存在的三角函数表的拓展。这也是为什么,纳皮尔的表只有 90 * 60 = 5400 项(对应四分之一圆周在分精度下的所有角度值),但数字精度却有小数点后 7 位。因为当时最精确的三角函数表是 7 位精度。

在没有计算机的年代,计算对数必须查对数表。那么最初的对数表怎么得到的呢?如果是按幂函数的逆去计算,那就涉及高次开方,人肉计算显然是不可能的。而且当时,还并没有发现对数和幂的互逆关系(那要等到 100 年后的欧拉),甚至连幂的概念都没有。

《数学的雨伞下》这本书为了让读者更容易理解对数表,举例子使用的是以 2 为底的对数。对数列是一个自然(等差)数列:1,2,3,4,5... ;真数列是 2, 4, 8, 16, 32 .... 这样一个等比数列。但实际这样制作对数表会难以实用,因为真数数列膨胀的太快了。如果要实用,最好真数数列的间隔不要太大。如果间隔太大,在利用它做乘法运算的时候,很多数字会偏差很大。

把对数用于快速计算乘法,选用怎样的底并不重要。当等差数列的差距为 0.00000001 时(因为当时的三角函数表有 7 位精度),等比数列的差值选为 1.0000001 或 0.9999999 最方便计算。因为这样,列出等比数列时,就不需要连续计算乘法,而只需要移位相加即可。一个十进制数乘以 1.0000001 只需要把这个移动 7 位的小数点,再加上原数即可。如果等比数列的公比为 1.0000001 ,其实是给对数表选择了一个以 (1+1/n)^n (n = 10^7) 的底。当然,这是现代数学的看法,在纳皮尔的时代,还没有发展出底这个概念。

纳皮尔研究的是三角对数,真数范围在 0 到 1 之间。当时的人并没有完整的小数和数级的概念。过去研究圆,使用的是一个超大的(10^7)的半径而不是今天流行的 1 。因为这样,三角函数才能近似为整数(对于 7 位精度,使用10^7 的圆半径,相当于比今天的三角函数放大了一千万倍)。btw, 纳皮尔在制作对数表的过程中,发明了小数点,用来保留计算过程中的精度。

他先构造了一个等比数列,再通过几何定义去计算其对数对应的等差数列。从现代观念看,纳皮尔选择的底约为 0.9999999^1000000 ,非常接近 1/e 。

在今天来看,如果我们想制作一张好用的对数表,真数列自然是越密越好。如果我们把 n 取无穷大,让 1/n 足够小,(1+1/n)^n 的极限即为欧拉数 e 。我想,这也是为什么欧拉数 e 被称为自然对数的底。纳皮尔的时代,无法通过“视对数函数为幂函数的逆函数”来建立这种直观的认识,人类深刻认识 e,要到百年后的欧拉。

关于纳皮尔如何制作对数表的,他自己写过构造方法一书。300 年后的 1914 年 EW Hobson 写了 John Napier and the invention of logarithms, 1614 纪念纳皮尔,详细讨论了纳皮尔原著中的方法。这篇文章在网上可以找到中文翻译。另可以参考这一篇文章

纳皮尔在计算过程中,充分考虑了计算的误差区间,严格保证他计算的对数表满足 7 位精度。他首先计算了 0.9999999 的 0 到 100 次方,然后计算 0.99999999^100 = 0.99999 的 0 到 50 次方。虽然计算这个等比数列只需要把前一个数字在十进制上移位并计算减法,这个计算工作并不难(并不需要算乘法),但纳皮尔在这一步把最后一项算错了:本应该是 0.999500122480 ,而他计算成了 0.9995001222927 。这个 bug 导致了使用最终的对数表会产生微小的误差(影响最后一位数字),纳皮尔自己觉得这个误差是三角函数表不精确导致的,并建议用 8 位精度重制三角函数表。

卡牌构筑类桌游核心规则之六

2025-04-08 21:10:54

这次介绍两款在国内人气不高的卡牌构筑类桌游。游戏都还不错,可能是因为没有中文版,所以身边没见什么朋友玩。

首先是 XenoShyft 。它的最初版全名为 XenoShyft: Onslaught (2015) ,后来又出了一个可以独立玩的扩展 XenoShyft: Dreadmire (2017) 。

故事背景有点像星河舰队:由人类军士抵抗虫子大军。简单说,这是一个塔防游戏:游戏分为三个波次,每个波次三轮,一共要面对九轮虫群的冲锋。

游戏中有四类卡片:部队、敌人、物品、矿物,另有一组表示玩家所属部门的能力卡,每局游戏每个玩家可以分到一张,按卡片上所述获得能力

矿物就是游戏中的货币,用来在市场购买部队卡和物品卡。敌人卡分为三组,对应到三次波次,洗乱后形成系统堆。玩家需要在每轮击败一定数量的敌人,撑过三个波次就可以取得游戏胜利。

玩家基础起始牌组 10 张,4 张最低级的士兵和 6 张一费的矿物。根据玩家的部门,还会得到最多 2 张部门所属的特殊卡。

市场由部队卡和物品卡构成,其中部队卡是固定的,分三个波次逐步开放购买。物品卡一共 24 种(基础版),但同时只会有 9 种出现在市场上。玩家部门可能强制某种物品一定出现在市场上,其它位置则是每局游戏随机的。在游戏过程中,当一种物品全部买空后,会在市场中随机补充一堆新的物品卡。普通敌人卡按波次分为三组洗乱,然后根据波次再从 6 张 boss 随机分配到三个波次中。

每个轮次,玩家先抽牌将手牌补齐到 6 张,然后打出所有的矿物卡,并根据波次额外获得 1-3 费,然后用这些费用从市场购买新卡片,花不完的费用不保留。新购得的卡片直接进入手牌(而不是弃牌堆)。这是一个合作游戏,所以玩家可以商量后再决定各自的购买决策。

然后,玩家把手牌部署到战区。每个玩家把部队卡排成一行(最多四个位置),物品中的装备可以叠在部队卡上增强单位的能力。玩家可以给队友的部队卡加装备(但不可以把自己的部队卡部署在队友战区)。部署环节玩家之间可以商量,同时进行。

之后进入战斗环节。这个环节是一个玩家一个玩家逐个结算。翻开敌人队列上的敌人卡(在部署环节是不可见的)、在敌人卡片翻开时可能有一次性能力,发动该能力、然后(所有)玩家都有一次机会打出手牌中的物品卡或使用部署在战场上的卡片能力。之后,双方队列顶部的两张卡片结算战斗结果。卡片只有攻击和 HP 两个数值,分别将自己的 HP 减去对手的攻击点。一旦有一方(或双方)的 HP 减到 0 ,战斗结束,把战斗队列卡片前移,重复这个过程。直到一方队列为空。

如果己方部队全灭,每场战斗的反应阶段(每个玩家都可以打出一张手牌或使用战斗卡片能力)依然有效,但改由基地承受虫子的攻击。基地的 HP 为所有玩家共享,总数为玩家人数乘 15 。可以认为基地的攻击无限大,在承受攻击后,一定可以消灭敌人。一旦基地 HP 降为 0 ,所有玩家同时输掉游戏。

游戏中的死亡效果有两种,毁掉(burning)和弃掉(discarding)。毁掉一张卡指把这张卡片退回市场(如果市场上还有同类卡)或移出游戏(市场上没有对应位置),而弃掉一张卡指放去玩家的弃牌堆。

通常,敌人卡片效果一次只会结算一张(即当前战斗的卡片)。但有些卡片效果会对场上敌人队列中尚未翻开的卡片造成伤害。这种情况需要先将所涉及的敌方卡片都翻过来,并全部结算卡片出场能力。对于需要同时结算多张敌人卡片出场能力时,玩家可以讨论执行次序。

如果对这款游戏有兴趣,又找不到人玩的话,可以试试它的电子版,在 steam 上就有。不过看评论,据说电子版 bug 有点多。


另一个游戏是 G.I. JOE Deck-Building Game (2021) 。G.I. JOE 特种部队是孩之宝(也就是变形金刚品牌的拥有者)旗下的一个品牌,除了玩具,有衍生的漫画、电影和动画片。这个桌游也是这个玩具品牌的衍生品。我认为这个 DBG 里的某些设定(不同的游戏剧本、同一剧本中不断推进的故事任务、队员的多种技能)也影响了星际孤儿那个电子游戏。

游戏有很多剧本、以及若干扩展。不同的剧本在规则细节上有所不同(这一点和星际孤儿很相像),这里只减少核心共通的规则。

这是个多人协作游戏。当然,只要是协作游戏,就一定可以单人玩,只需要你轮流扮演不同角色的玩家即可。每个玩家一开始有一张特殊的领袖卡,然后配上 9 张固定的初始牌组成了起始卡组。每个回合摸 5 张卡,用不完的卡会弃掉,不能保留到下一回合使用。每张领袖卡都对应了一个升级版本,可以在游戏进程中购买替换。

市场由一组卡片洗乱,随机抽出 6 张构成。每当玩家购买一张卡,就会补充一张新卡。但如果卡堆耗尽尚未结束游戏,游戏失败。在游戏过程中,可能有敌对卡片出现,会盖掉市场中的卡。玩家需要解决掉敌人,否则盖掉的卡片无法购买。如果 6 张市场卡片都被盖掉也会导致游戏失败。

当每个玩家执行完一轮行动,即为一大轮游戏。在每大轮开始,都会推进一个全局的威胁指示条。一旦威胁指数上升到某一程度,就会发生一些固定事件。维护指数走到头会导致游戏失败。

游戏故事由三幕构成,每幕随机选取两张对应的故事任务卡和一张固定的终局局故事卡,一共 9 张故事任务卡构成了整局游戏。永远有一个故事任务呈现在场景中,它有可能触发一个回合效果,需要在每个玩家回合开始时结算。

每一幕开始都会洗混所有的系统事件卡堆(包括之前解决完弃掉的事件卡),故事卡和威胁进度条会触发这些事件。这些事件会给玩家增加一些负面效果,或是在场上增加一些任务让玩家解决。

游戏任务分两种:团队任务和支线任务。故事卡一定是团队任务,事件产生的 boss 卡也是团队任务。团队任务可以在当前玩家决定去进行任务时,其他玩家提供协作;而支线任务只能由当前玩家独立完成。任务有地形、难度、技能需求、持续效果等元素构成。

地形指玩家开启任务需要使用怎样的载具,分陆海空三类。技能要求则限制了玩家可以派出的队员。难度数字决定了玩家最终需要在此技能上获得多少点才能完成任务。持续效果则会在该任务完成前,对玩家造成的负面效果。

开启一个任务需要玩家从机库派出一个对应地形的载具以及至少一个队员(从手牌打出)。该载具是在玩家回合开始时从手牌打在机库中的,VAMP 作为默认载具总可以保证一个回合使用一次。高级载具可以从市场购买。对于团队任务,所有玩家都可以协商派出队员,但队员总数不能超过载具的容量。

任务上标注了所需技能,派出的队员卡如果有符合的技能,则可以把技能点加到任务中。如果技能不匹配,也可以视为一个通用技能。任务卡最多要求两种技能,如果是 & 标记,则表示可以只要符合两种技能中的任意一种都可以生效;如果是 or 标记,则需要当前玩家选择其中一种技能,所有队员都需要匹配这种技能。

最终参与任务的技能总数决定了最终可以用几个六面骰。每个六面骰有三个零点,两个一点,一个两点;扔出对应数量的骰子,把最终点数相加,如果大于等于任务的难度值,则任务成功并或许成功奖励,否则任务失败。对于故事任务,失败需承受任务卡上的失败惩罚,并结束任务;对于其它任务,失败会让任务继续保留在场上。

有一类叫做 Precision Strikes 的任务,在翻出时需玩家讨论后决定放在谁的面前,变成它代做的支线任务。每个玩家最多只能放两张 Precision Strikes 在面前,到他的行动回合,必须先处理掉 Precision Strikes 任务。

在玩家做完所有想做的任务后,剩余的手牌可以作为够买新卡的费用。每张卡都标有一个自身的价格,以及一个在购买阶段可以当成几点费用使用。没有用完的费用不会积累到下个回合。购买的载具卡会在购买后直接进入机库,供后续回合所有玩家使用。其它卡片则放在抽牌堆顶。

整个回合结束后,弃掉所有手牌,以及回合中使用过的卡牌以及载具,重新抽五张。

玩家可以共建一个基地。这个基地由五部分构成,在游戏过程中逐步升级,升级也时通过购买完成的。在游戏过程中,以升级的部分也可能被摧毁或重建。这五个部件如下:

Repair Bay 会让任务中使用的载具都放在该处而不是弃牌堆。在回合结束(或被摧毁),Repair Bay 中的载具都会回到机库。这样,载具的利用率会大大提升。

Stockade 建成后,击败的 boss 卡会进入这里而不会重复进入游戏。

Battlestation 可以在团队任务中重掷一个骰子。

Laser Cannon 可以在支线任务中增加一个骰子。

Command Room 把手牌增加到 6 张。

卡牌构筑类桌游核心规则之五

2025-03-12 09:30:12

最近的兴趣重心转移,没怎么研究桌游。不过前几个月的笔记还有一点,今天继续整理一下。继续谈谈 PvE 向的卡牌构筑类桌游。

Aeon's End (2016) 末日决战是一款偏传统卡牌对战规则的游戏:一开始手牌中只有水晶 (gem) 和基础法术 (spell) 卡。游戏过程中,在一个固定市场(9 种)用水晶购买更强力的卡片升级自己的卡组。游戏的目标是合作(或 solo)击败 boss 或 boss 的仆从。如果被敌人攻击太多次,自己的 HP 减到 0 就失败了。

法术卡其实更像是炉石/万智牌中的玩家仆从,只不过是一次性消耗品。需要先部署在桌面,然后才可以攻击。而玩家卡组内的第三种遗物卡(relic)则更像是一般意义的法术:可以即时产生效果。

打出法术卡或遗物卡是没有直接费用的,水晶主要用来支付从市场购买新卡的费用。另外,法术卡要生效,需要桌面有空闲的位置(叫做 breach 裂隙)。所以即使手牌中的很多强力法术卡,也不能无限制的一次摆出来。

裂隙需要花费水晶费用打开才能使用(摆放法术)。打开 (open) 裂隙在一局游戏中是一次性的,一旦裂隙开启,就可以一直使用(放置法术)。但开启费用较高,也可以 focus 一个关着的裂隙一次性使用。focus 费用较低,但下次使用还需要再次支付水晶 focus 。focus 同一个裂隙 4 次后会自动开启。在开启的裂隙上摆放的法术,玩家可以选择再后续的任意回合生效;但通过 focus 预备的法术则必须在下一回合用掉。

每个玩家角色有一个独特的能力,这个能力需要 charge 后才可以使用。每次 charge 的费用是 2 水晶,不同角色使用能力需要的 charge 数量各不相同。

这个游戏规则中比较独特的是:玩家回合结束后,未使用的卡牌不会自动丢弃,但摸牌是将手牌补齐五张。即,打的牌少,卡组循环也会变慢。而弃牌是有次序的,正面朝上依次置入弃牌堆。抽牌堆用完后,弃牌堆不洗牌,直接反过来形成新的抽牌堆。

游戏的随机性不来源于抽牌堆的乱序,而是每个轮次的次序。每个轮次,玩家和敌人的指代物会放在一起洗混,其随机次序决定了这个轮次中,哪个玩家(如果有多个)以及敌人谁先行动谁后行动。

末日决战这几年口碑不错,一直在发新的扩展。尤其是新的版本加入了传承机制,也就是游戏可以一局局玩下去,玩家可以升级和继承过去的能力。玩家主要的升级是角色本身的技能,敌人也会逐步加强。


另一款同期发行,口碑不错的合作类 DBG 是 Clank!: A Deck-Building Adventure (2016) 。

这款游戏的玩法不是纯粹的打牌,而是加入了版图移动机制。玩家需要在版图上移动获得神器和宝藏(计分)。它并不算是一个合作游戏,更像是玩家有一个共同的敌人(系统),而玩家之间则需要比拼谁拿的分更多。

系统会攻击玩家,当至少有一个玩家 HP 减到零时,游戏会进入结束倒计时阶段。获得一个神器并逃离游戏的玩家会获得额外计分奖励。玩家需要在版图上获得一个神器的基础上尽可能的得分(获取宝藏)并在游戏结束前脱离。玩家如果没能获得神器,得再多分也不计算。游戏的结束条件是由第一个被击倒的玩家开启,但第一个被击倒的得分损失很大,所以玩家应尽可能的活得更久。

游戏规则中比较有特色的是 Clank 系统(也就是游戏的名称)。有些卡牌效果会给玩家增加 Clank 方块的数量。而系统发起攻击时,会把所有玩家获得的 Clank 方块以及系统方块一起放入抽取袋再抽出来。抽到 Clank 数量决定了对应玩家会掉多少 HP ,所以 Clank 越多(尤其是比队友多)意味着会更快挂掉。系统方块更多意味着玩家受伤害的概率越少。不过,每次系统发动攻击抽取出的黑色方块(系统方块)都不会再次放入抽取袋中,这就意味着系统攻击造成的伤害会越来越大。

市场(地城)上有三类卡片:一次性效果卡、小怪、新的可以加入卡组的卡片。这和 Ascension 非常的类似:玩家可以选择不同路线增强自己的能力,杀怪或购买更强力的卡片。

市场上除了随机卡堆翻出的六张卡外,还有固定卡堆:一个技能点及两点攻击(剑符号)的雇佣兵,两个技能点及一点移动力(腿符号)的探索卡、价值 7 分的宝藏卡、需要 2 点攻击击败的小怪哥布林。

每个玩家行动结束后,都会把市场补全六张。当市场上得卡上出现龙符号时,系统发起攻击。

玩家的牌主要有三种能力:

  • 技能点,用来在市场上购买卡片。
  • 攻击力(剑符号),用来杀掉市场上出现的怪物卡。
  • 移动力(腿符号),用来在版图上移动。

击败怪物可以获得金币,这是和技能点不同的第二种游戏货币。金币在游戏结束时可以用于计分,也可以在游戏过程中花掉购买物品。物品有固定的三种,价格均为 7 金币:

  • 钥匙,用来在版图上快速移动(使用通道)。
  • 背包,可以携带额外的神器,每个额外的神器价值 5 分。
  • 皇冠,价值 8,9,10 分。(先买的玩家收益更高)。

打牌过程和传统的领土一样:每个回合的手牌必须全部打出,不可保留到下一回合。每个回合抽取新的 5 张手牌。抽牌堆用完后,洗混弃牌堆形成新的抽牌堆。

Clank! 在最近几年也一直在出各种扩展,同样发行了传承版本。

传承机制加入后,游戏更有故事性。每个章节会加入一些事件卡以及每局游戏的合约(单局游戏目标)。玩家需要扮演不同角色(有一点不同的能力),并可以随着游戏进程增强能力,游戏版图也会慢慢开放。

Windows 10 如何把文件关联到 Store App 上

2025-03-11 11:13:58

我在 Windows 10 下倾向于通过 store 安装 app ,而不是使用独立的软件安装包。这样安装的 app 会比较安全。但正因为这些额外的安全措施,有些过去很容易做的事情却变得麻烦了。比如,把一个特定类型的文件关联到 app 上。

我习惯用 IrfanView64 查看图片。在游戏开发中常见的 .dds 文件它也可以打开(需要额外安装官方插件)。但 IrfanView64 在提交 store 发布时没有提交 .dds 这个后缀名,这导致 windows 10 无法选择用它打开 .dds 文件。在自选打开特定文件类型文件的设置菜单里(设置-默认应用),你会发现怎么都找不到这个应用。

通常,你很难找到 store app 的执行文件路径。它在 %ProgramFiles%\WindowsApps 目录下,这是一个特殊的目录,受 Windows 系统保护。但可以在启动应用后,开启任务管理器,找到对应进程打开文件所在位置找到它。

我通过 google 找到了一篇吐槽文,也是谈这个问题。这篇 blog 也是通过这个方法找到了 store app 执行文件的位置。有了这个地址似乎就可以在打开文件的设置菜单设置本地程序路径了。

但估计是最近的 windows 更新又进一步增强了安全机制,即使你这样设置打开 .dds 为 Irfanview64 app 本地安装的程序路径,双击 .dds 文件既然会出错。(缺少访问该程序的权限)

我想还是得自己动手改注册表。打开 regedit 注册表编辑器,找到 \HKEY_CLASSES_ROOT\.dds\OpenWithProgids 这里是 .dds 的备选程序列表。ps. 如果从来没有设置过 .dds 的关联程序,可能没有 OpenWithProgids 这个子项。创建出来即可,或随便设置一次 .dds 的打开方式也会被系统创建出来。

下一步在这里添加 Irfanview64 这个 app 项。每个 store app 都有一个 id 。Irfanview64 的为 AppXhg16hybbkbv7j3fk4s35ykmfvp63yx63 。其实我也不知道怎么获得这个 id ,但我的系统上 .jpg 或 .png 等都可以通过 Irfanview64 打开,找到 \HKEY_CLASSES_ROOT\.jpg\OpenWithProgids 把它抄过来就好了。

一旦添加了对应的 key ,然后再去 .dds 下选择打开应用,就会发现在右键菜单里多出来 Irfanview64 app 的选项。

2D 渲染管线的一点优化

2025-02-20 10:46:18

考虑到我想做的独立游戏并不需要以画面效果吸引人,游戏是策略向的,所以 2D 表现就足够了。之前几年做的 3d 引擎对这个需求来说太复杂了,而且这次我也不打算主打移动平台,之前为移动平台做的考虑也没太大意义。所以,最近想花个把月重新搭一个 2d 游戏用的框架。当然,最重要的是:我太久没写代码了,而做这个非常有趣。

前天在 github 上开了一个新坑,具体想法写在项目的讨论区了

虽说 2d 游戏在如今的硬件上,性能那是相当富裕。但在具体写代码时,还是忍不住想想怎么设计,性能会比较好。不然总是重复大家都有的东西也是无趣。

在现代 GPU 上实现一个最简单的 2d 管线,就是把它当成 3d 网格,一堆顶点数据填进去,绑定贴图,提交渲染即可。所谓 2d 图片,就是两个三角形,看成是 3d 世界里的一个面片即可。

所以,每个顶点的数据就是 vec2 pos ,要画一个矩形需要四个顶点,用 vertex buffer 传进去。

但是和 3d 游戏不同,2d 图片形状大多不规整,不是边长为 2 的幂的正方形,尺寸也不大。如果每张小图片(2d 游戏中通常称为 sprite)都构造一张贴图的话,会非常低效。通常我们会把很多 sprite 打包在同一张大的正方形的贴图上。这样,顶点数据中还需要定义绘制矩形对应在贴图上的区域,通常称之为 uv 坐标。至此,常规的实现方法中,每个顶点就是 4 个数据量:vec2 pos 和 vec2 uv 。因为 sprite 都在同一张贴图上,一次图形指令提交只画一个矩形就太浪费了,我们会把多个矩形的顶点放在一起,一次把整个顶点数组提交到 vertex buffer 中。

虽然 2d 游戏的大多数 sprite 只需要指定屏幕(画布)坐标渲染即可,画布可以整体缩放。sprite 单独缩放旋转的机会比较少,但也并非没有。用上面的方法怎么处理旋转和缩放呢?过去常见的方法是在 CPU 中计算好四个顶点,把结果填在顶点数据流中。btw, 很早以前,我在实现 ejoy2d 的初版时就是这么做的。这样最为灵活,CPU 计算一个 2x3 的矩阵也不慢(ejoy2d 使用了定点数,在早期的手机上性能更好)。而且,大多数 sprite 并不需要旋转和缩放,只需要做一次 vec2 的加法即可。

计算该怎么做?我们需要找到 sprite 的基准点。大多数情况下,这个基准点并不是图片的左上角。然后以这个点为坐标原点,对 sprite 的四个顶点依次做旋转和缩放变换再加上 sprite 的绘制位置。这一系列运算相当于乘一个 2x3 的矩阵。如果我们想把这个运算放在 GPU 该怎么做?顶点数据流中就不能直接放顶点计算结果的坐标了,而应该放针对 sprite 的基准点的相对坐标,以及一个 2x3 的变换矩阵。这样,顶点数据就变成了:vec2 offset ; vec2 uv ; mat2 sr; vec2 t; 一共是 10 个数据。

很明显,后面这个 mat2 sr; vec2 t; 在数据流中重复了 4 次(一个 sprite 的 4 个顶点有相同的 2x3 矩阵)。另一方面,绝大多数的 sprite 不需要旋转和缩放变换,这种情况下,mat2 sr 都是单位矩阵;即使有旋转变换,旋转角度也是有限的。整个数据流中必然存在大量重复的 mat2 sr 。怎么优化掉这些重复数据呢?我们可以用一个 storage buffer 保存唯一的 mat2 sr ,在顶点流中保存一个索引 index 即可。这样,顶点数据就剩下 vec2 offset; vec2 uv; index; vec2 t; 7 个数据。最后这个 vec2 t 不放在索引中是因为大多数 sprite 会有不同的位移坐标,而 2x2 的 SR 矩阵更容易合并。

接下来的问题是,index 和 vec2 t 还是重复了 4 次。为了去掉这个重复,我们可以采用 instance draw 或 indirect draw 。理论上用 indirect draw 更合适,但它对图形 api 版本要求高一些(如果想运行在 web 上,还是需要考虑这点),所以我选择用 instance draw 实现。

使用 instance draw 的一个额外好处是可以省掉 index buffer ,使用三角条带描述矩形即可。

但 instance draw 有个问题:它最初是为了把一组顶点数据重复渲染设计的。而这里,我们有很多不同的矩形需要同一批次渲染。即,vb 中每组数据 vec2 offset; vec2 uv; 有很多组。所以,我选择不使用顶点数据流,把这组数据放在另一格 storage buffer 中,然后在顶点着色器(vs)中通过 gl_InstanceIndexgl_VertexIndex 索引它。

做到这里,我注意到:2d 游戏中的 sprite 矩形都是轴对齐的。所以,描述四个顶点并不需要 8 个量,而只需要 4 个,保存两个对角顶点即可。另外,offset 矩形和贴图上的 uv 矩形形状也是一致的,我们只是把贴图上的一个区域完整映射到画布上,这样还可以少两个重复信息。最终,我们只需要 3 对 vec2 就可以表达一个矩形以及 uv 。

而图片是以像素为单位的,贴图尺寸不会有几万像素大。这个坐标量使用 int16 足够了。所以在保存 sprite 元信息的这个 storage buffer 中,每个图元其实只需要 6 个 int16 ,也就是 12 字节足够了。最终,绘制每个 sprite 的数据为 6 short + 3 float ( x,y,index ) = 26 字节。

最终的 vs 是这样的

layout(binding=0) uniform vs_params {
    vec2 texsize;
    vec2 framesize;
};

struct sr_mat {
    mat2 m;
};

layout(binding=0) readonly buffer sr_lut {
    sr_mat sr[];
};

struct sprite {
    uint offset;
    uint u;
    uint v;
};

layout(binding=1) readonly buffer sprite_buffer {
    sprite spr[];
};

in vec3 position;

out vec2 uv;

void main() {
    sprite s = spr[gl_InstanceIndex]; 
    ivec2 u2 = ivec2(s.u >> 16 , s.u & 0xffff);
    ivec2 v2 = ivec2(s.v >> 16 , s.v & 0xffff);
    ivec2 off = ivec2(s.offset >> 16 , s.offset & 0xffff) - 0x8000;
    uv = vec2(u2[gl_VertexIndex % 2] , v2[gl_VertexIndex >> 1]);
    vec2 pos = uv - ( off + ivec2(u2[0], v2[0]));
    pos = (pos * sr[int(position.z)].m + position.xy) * framesize;
    gl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1);
    uv = uv * texsize;
}

再来看 CPU 侧的设计:

我这次使用了 sokol 做底层图形 api 。sokol api 不支持多线程,所有图形指令必须在同一个线程提交。所以我做了一个简单的中间层:绘图时不直接调用图形 api ,而是填充一个内存结构。这个结构被称为 batch ,不同的线程可以持有多个不同的 batch 。所有 batch 汇总到渲染线程后,渲染线程再将 batch 中的数据转换为图形指令以及所需的数据结构。

因为 2d 游戏据大多数情况都在处理图片,使用默认的渲染方式。我对这种默认材质做了特别优化。batch 是由这样的结构数组构成:

struct draw_primitive {
    int32_t x;  // sign bit + 23 bits + 8 bits   fix number
    int32_t y;
    uint32_t sr;    // 20 bits scale + 12 bits rot
    int32_t sprite; // negative : material id
};

其中,用两个顶点 32bit 整数表示 sprite 的画布坐标;一个 32bit 整数表示旋转和缩放量;一个 sprite id 。

渲染层会查表把 sprite id 翻译成对应的元信息(上面提到的 offset 和 uv ),当 sprite id 为负数时,表示这是一个非默认材质,batch 中的下一组数据是该材质的参数。例如,文本渲染就会用到额外材质,文本的 unicode 和颜色信息就放在接下来的数据中。

卡牌构筑类桌游核心规则之四

2025-02-16 14:44:04

这篇谈谈 PvE (玩家对抗环境)向的卡牌构筑类游戏。

在桌游中,PvP 向(玩家对抗玩家)的游戏数量明显超过 PvE。我认为这是因为桌游需要靠玩家自己驱动游戏规则,扮演环境的同样是玩家,其规则不能设计的太复杂,通常只能靠简单机械的逻辑驱动。或者,起源于桌游的 RPG ,比如 D&D ,则由一个玩家扮演环境(城主),这样就可以增加游戏的深度。但毕竟这种不对称规则下,玩家方和环境方很难调配平衡。RPG 这样的游戏,城主也并非玩家的对立方,大家只是在一起享受游戏过程。而在电脑上,则可以通过程序实现更复杂的环境。所以在电脑游戏中,大量的游戏转向 PvE 。毕竟,找到可以一起玩的游戏搭档并不容易。

所以,对于卡牌构筑这个具体类别,电脑上的游戏几乎一开始就是 PvE 性质的:比如杀戮尖塔,玩家一直在挑战系统而获得乐趣;而桌游中,从领土(Dominion)开始,就是基于玩家对抗设计的规则。

传奇 Legendary 系列是一个比较早的 PvE 向卡牌构筑类桌游系列。最早可以追述到 Legendary: A Marvel Deck Building Game (2012) ,后续几年发布了大量系列作品,并衍生出 Legendary Encounters 系列。关注 Legendary 系列是因为星际孤儿(Stellar Orphans)这个电脑游戏,我特别喜欢。在星际孤儿的玩家社区,有玩家指出这个游戏明显受到了 Legendary 系列桌游的启发。

传奇系列的每个作品都围绕一个题材展开,以最初的漫威系列为例,下面我介绍一下它的核心玩法规则。


游戏是由玩家对抗系统。每个玩家以开始会拿到一叠基础的英雄卡(12 张),系统则由一叠系统卡设定。

每个回合,从系统卡堆中抽出一张卡片出来推动游戏发展。如果从系统卡堆中抽到坏人卡,则表示这张坏人卡会入侵城市(摆放在桌面区);如果抽到旁观者,则绑定在坏人卡上,变成坏人的俘虏;如果抽到事件卡,则引发特殊事件。

而在系统卡结算完毕后,玩家从手牌中打出卡片组合,产生本回合的攻击点、招募点以及特殊能力。攻击点可以用来消灭城市中的坏人卡(并解决俘虏);招募点则从桌面的 HQ 列(从英雄卡堆中翻出)购买新的英雄卡增强自己的卡组。而传统的卡牌构筑类游戏规则一致,每个回合的卡片都必须全部用完,不可保留到下个回合,溢出的攻击点和招募点会作废。购买的新卡片也会先置入弃牌堆。每个回合,玩家从自己的卡组中抽取新的手牌(6 张);一旦卡组抽完,洗混弃牌堆,形成新的抽牌堆。

初始卡组由 8 张基础的招募点卡(每张 1 点)以及 4 张基础的攻击卡(每张一点)构成;英雄卡堆(形成市场)则由玩家选择的几个英雄对应的卡组混在一起。每个英雄卡组有 14 张,其中两组各 5 张相同的普通卡,三张强力卡,以及一张稀有卡。市场上永远展示其中的五张,当玩家购买后会补齐。一旦英雄卡堆用玩,游戏结束。另外,市场上永远有固定的高级招募卡(每张 2 点)可供选购。它类似于 Dominion 中的银币。玩家每个回合还可以花 2 点招募点买到一张 sidekick ,该卡只能使用一次(用完回到市场),效果是抽两张卡。

每局游戏,系统存在一个终极 boss ,在 setup 阶段需要随机选择 boss 对应的一张 scheme 卡。scheme 卡上描述了系统胜利的方法。当系统达成条件,玩家就会输掉。

攻击 boss 他需要大量攻击点,每次成功的攻击会结算一次随机的 Tactics 卡(例如,有些 Tactics 卡会增加下一次攻击 boss 的难度)。每个 boss 对应 4 张 Tactics 卡,四次成功攻击后,玩家就赢得游戏。

系统每个回合翻出的坏人卡会以队列形式在桌面推进。桌面一共有 5 个位置(地点),坏人卡从最右侧进场,在进场时需要结算坏人卡上的 Ambush 效果(若有)。如果玩家一直对翻出的坏人卡置之不理的话,坏人卡会一步步向左推进,直到离开桌面。每离开一张坏人卡,都会摧毁掉市场上的一张卡片(由玩家自己选择);如果坏人带着俘虏离开,则还要弃掉一张手牌。如果逃离的坏人卡上标注有 escape 效果,还需要额外结算。

系统卡堆中存在一些地点卡,翻出后在场上并列一排从右至左一张张排列直到排满。摆满后,新的地点卡会替换掉场上最弱的那张。地点卡会改变存在这个地方的坏人卡的能力。地点卡本身可以作为攻击目标。

系统卡堆还包含一些特殊卡(坏人卡和旁观卡之外),它们不推进坏人卡。其中:

Trap 卡给玩家一个需立刻结算的挑战。

Twist 卡会推进当前 Scheme 卡上系统胜利的某种进度。

Strick 卡会触发 boss 的攻击。

玩家的手牌没有打出费用(类似杀戮尖塔的行动点限制),但受招募费用的限制,强力卡在一开始无法从市场购得。玩家倾向于在每个回合打出所有手牌(不打出的卡也无法带到下个回合)。基本卡片只是一个招募点和攻击点,把这些点数累加起来就是当前回合可以用于购买新卡以及消灭坏人的费用。但强力卡片的打牌次序是有选择的。因为卡片会有专门的特殊能力,比如,有的卡片需要弃掉别的手牌才能使用;有的需要前置打出某些类型的卡,就有额外的能力加成(形成 combo)。

此外,击败的坏人卡、拯救的俘虏(旁观者)、Tactics 卡都附带有 VP 。当以多人协作形式进行游戏时,每个玩家单独计算 VP ,在游戏结束后,可以比较在游戏过程中获得的 VP 总数来觉得谁表现得更好(但玩家在游戏过程中依旧是合作关系,而不应该为 VP 竞争。


在 Legendary 系列之后,同一家公司推出了新的 Legndary Encounters 系列。这是换了新设计师后在 Legendary 规则上的进一步发展。系列的第一作是 Legendary Encounters: An Alien Deck Building Game (2014) 。给我的感受是,Legendary Encounters 更加注重叙事(而不仅仅是围绕一个主题),通过卡片和游戏规则的设计,玩家可以更好的融入游戏故事中。不同的题材会给玩家不同的感受,Legendary Encounters 在今年(2025 年)还会有新作推出,最新的故事看起来会在冰与火之歌的世界中展开。

我在桌游模拟器中尝试了一下最初的异形(Alien)三部曲。和异形电影体验非常接近,代入感很强。和 Legendary 漫威不同的地方是:

敌人(坏人卡)是背面朝上在桌面(场景)中推进的。玩家需要主动 scan 。这和星际孤儿的设定非常相像(应该是它启发了星际孤儿)。玩家对敌人的推进置之不理的话,敌人牌移到头会触发攻击,而受到太多攻击后,玩家会死亡而输掉游戏。

多人协作模式下,不同的玩家会扮演不同的角色。不同角色的能力是不同的。另外,玩家在游戏过程中有可能因为不敌敌人而被感染,当玩家以这种形式被杀死后,会重新以异形的立场加入游戏,变成对抗其它玩家。

每局游戏会有固定的几个阶段,每个阶段有固定的目标。这些阶段性目标被设计成和电影情节一致。玩家需要完成每个阶段的目标推进游戏,直到所有阶段达成获得游戏胜利。btw, 星际孤儿也采用了这种形式,并进一步加长了游戏故事。