Jury Schon's Blog2024-02-21T07:17:00Z马伯庸写做爱2024-02-21T07:17:00Zbooks/2024-02-21Jury Schon's Blog<p class="md_block">
<span class="md_line md_line_start md_line_end">第一次读到马伯庸正面描写性爱是在《三国机密》里,此前读过他的《古董局中局》、《四海鲸骑》,虽有一些男欢女爱,但都没有 R18 的描写。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">当然,虽说是「正面描写」,只是指没有迫于内容审查的压力而避讳小说中的必要情节,从修辞上说,依然是侧面描写。</span>
</p>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">可以看一下马伯庸是如何在《三国机密》里正面描写做爱的:</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">伏寿朝刘协的方向挪了挪,把头贴在男人的宽阔的肩膀上,一条颀长的腿,有意无意地搭在她的双腿之间,绵软滚烫的身子自然而然的也靠了过来。这一次,两人之间再无间隙,刘协可以充分感受到女性肌肤的滑嫩与柔腻,白日里那位端庄贤淑的皇后,此时却如同一匹伏在暗处的母兽,蓄势待发。刘协感觉嗓子有些发干,正欲开口讨些水来,却不妨一对红唇迎了上来。他下意识的要抬起手来挡住指尖,却不小心陷入到一大团丰腴之中,然后被微微弹起。刘协自从来到许都之后,震惊、忧虑、恐惧、迷茫和沮丧接踵而来,整个人一直被极度压抑着,此时这大胆的撩拨在他紧绷的精神防线上弹开了小小的一个缺口,几乎就在一瞬间如泰山般的巨大压力,令堤坝崩塌,转化成了狂暴的洪流,肆意宣泄,把他与她怀中的女子裹挟在一起,开始的时候如羽化登仙般快乐。刘协感觉自己正握着一支如椽巨笔,在一张白洁绵软的左伯纸上挥毫作画,笔端蘸饱浓墨,挥洒间汁液四溅。如光滑的纸面上,留下斑斑印记。纸边微微卷起,似要抗拒,却被强势的压制铺平,任凭长而坚硬的笔杆运转自如。横,撇,竖,捺,勾,每一画的笔势都那么苍劲有力,力透纸背。</span>
</p>
</blockquote>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">近期在电车上打发时间读了他的一篇短篇小说《长安的荔枝》,久违地又读到了一次马伯庸对性爱的正面描写:</span>
</p>
<blockquote class="blockquote_lines_3 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start">李善德回到家里,心情大畅,压在心头几个月的石头总算可以放下了。他陪着女儿玩了好一阵双陆,又读了几首骆宾王的诗哄她睡着,然后拉着夫人进入帷帐,开始盘点子孙仓中快要溢出来的公粮。<br /></span>
<span class="md_line"> <br /></span>
<span class="md_line md_line_end">这个积年老吏查起账来,手段实在细腻,但凡勾检到要害之处,总要反复磨算。账上收进支出,每一笔皆落到实处方肯罢休。几番腾挪互抵之后,公粮才一次全数上缴,库存为之一清。</span>
</p>
</blockquote>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">我觉得太有意思了。于是搜了一下,发现马伯庸本人在知乎上还谈过这个问题:</span>
</p>
<blockquote class="blockquote_lines_5 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start">既然是一部小说里的情色部分,那么默认题主所说的情色描写应该是文学性的,是为整个小说服务的,而不是纯粹的感官刺激——或者我们不妨说的更直白一点,“不会被警察抓走” 的情色描写。办法有很多,其中有一种操作上比较容易,可以为很多人应用的技巧,叫做场景置换。具体技术上,作者要擅于联想,把性爱比拟成其他行为,通过比喻、象征、文字暗示等手法,让读者从这些场景置换中联想到性爱。至于比拟成什么行为,置换成什么场景,就各有巧妙不同,完全取决于你自己的想象力了。<br /></span>
<span class="md_line"> <br /></span>
<span class="md_line">.......<br /></span>
<span class="md_line"> <br /></span>
<span class="md_line md_line_end">简单来说,完成一次成功的情色场景置换,需要大胆想象,把情色比拟成匪夷所思却言之成理的另外一种行为;同时还需要小心描绘,避免喧宾夺主。</span>
</p>
</blockquote>
<p class="md_block">
<span class="md_line md_line_start md_line_end">他在这个回答里,还举了许多例子。</span>
</p>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">比如古代的,《剪灯新话·联芳楼记》:</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">误入蓬山顶上来,芙蓉芍药两边开。此身得似偷香蝶,游戏花丛日几回。</span>
</p>
</blockquote>
<p class="md_block">
<span class="md_line md_line_start md_line_end">现代文学的,老舍《骆驼祥子》</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">屋内灭了灯。天上很黑。不时有一两个星刺入了银河,或划进黑暗中,带着发红或发白的光尾,轻飘的或硬挺的,直坠或横扫着,有时也点动着,颤抖着,给天上一些光热的动荡,给黑暗一些闪烁的爆裂。有时一两个星,有时好几个星,同时飞落,使静寂的秋空微颤,使万星一时迷乱起来。有时一个单独的巨星横刺入天角,光尾极长,放射着星花;红,渐黄;在最后的挺进,忽然狂悦似的把天角照白了一条,好象刺开万重的黑暗,透进并逗留一些乳白的光。余光散尽,黑暗似晃动了几下,又包合起来,静静懒懒的群星又复了原位,在秋风上微笑。地上飞着些寻求情侣的秋萤,也作着星样的游戏。</span>
</p>
</blockquote>
<p class="md_block">
<span class="md_line md_line_start md_line_end">老舍《月牙儿》</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">他的笑唇在我的脸上,从他的头发上我看着那也在微笑的月牙。春风象醉了,吹破了春云,露出月牙与两对儿春星。河岸上的柳枝轻摆,青蛙唱着恋歌,嫩蒲的香味散在春晚的暖气里。我听着水流,象给嫩蒲一些生力,我想象着蒲梗轻快的往高里长。小蒲公英在潮暖的地上似乎正往叶尖花瓣上灌着白浆。什么都在溶化着春的力量,把春收在那微妙的地方,然后放出一些香味,象花蕊顶破了花瓣。我忘了自己,象四外的花草似的,承受着春的透入;我没了自己,象化在了那点春风与月的微光中。月儿忽然被云掩住,我想起来自己,我觉得他的热力压迫我。</span>
</p>
</blockquote>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">邓一光的《我是太阳》:</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">那天夜里关山林将滚烫的土炕变成了他另外的一个战场,一个他陌生的新鲜的战场。他像一个初上战场的新兵,不懂得地势,不掌握战情,不明白战况,不会使唤武器,跌跌撞撞地在一片白皑皑的雪地上摸爬滚打。他头脑发热,兴奋无比,一点儿也不知道这仗该怎么打,只是凭着矫健、英勇、强悍、无所畏惧、使不完的热情和力气没头没脑地发起冲锋。在最初的战役结束之后,他有些上路了,有些老兵的经验和套路了。他为战场的诱人之处所迷恋。他为自己势不可当的精力所鼓舞。他开始学着做一个初级指挥员,开始学着分析战情,了解战况,侦察地形,然后组织部队发起一次又一次的冲锋。他气喘吁吁,大汗淋漓,精神高度兴奋。他看到他的进攻越来越有效果了,它们差不多全都直接击中了对手的要害之处。这是一种全新的战争体验,这和他所经历过的那些战争不同,有着完全迥异但却其乐无穷的魅力。他越来越感到自信。他觉得他天生就是个军人,是个英勇无敌的战士。他再也不必在战争面前手足无措了,再也不必拘泥了,再也不会无所建树了。对于一名职业军人来说,这似乎是天生的,仅仅一夜之间,他就由一名新兵成长为一位能主宰整个战争局面的优秀指挥官。</span>
</p>
</blockquote>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">圣劳伦斯《查太莱夫人的情人》:</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">她仿佛像个大海,满是些幽暗的波涛,上升着,膨胀着,膨胀成一个巨浪,于是慢慢地,整个的幽暗的她,都在动作起来,她成了一个默默地、蒙昧地、兴风作浪的海洋。在她的里面,海底分开,左右荡漾,悠悠地,一波一波地荡到远处去。不住地荡漾。在她感觉最敏锐的部位,深渊分开,左右荡漾,中央便是探海者在温柔地往深处探索,越探越深,愈来愈触到她的深处,她就愈深愈远地暴露着,她的波涛越汹涌地荡开某处岸边。那个能被明显感受到的探海者愈探愈深入。她自身的波涛越荡越远去,离开她,抛弃她,直至突然地,在一阵温柔颤抖地的痉挛中,她自己知道被触到了,一切都完成了,她已经没有了,她再不存在了,她出世了:一个女人。</span>
</p>
</blockquote>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">他还举了一些失败的搞笑例子:</span>
</p>
<blockquote class="blockquote_lines_5 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start">学长忽然把校花扑倒在床,说我喜欢你很久了!校花却轻轻地把手按在他胸膛:学长,人家是向你请教作业的。先帮我求一下∫xcos2xdx的不定积分好吗?”<br /></span>
<span class="md_line"> <br /></span>
<span class="md_line">“嗯,这里要用分部积分法,具体的求解步骤是这样的。”学长从校花身上爬下来,戴上眼镜提起笔,在笔记本上写了1/2 [xsin2x -∫sin2xdx]。校花很是开心:“学长你好厉害!”双臂搂了过去,一股幽香扑鼻。学长却把她推开,不耐烦地说:“还没做完呢。”又写下一行字:1/2 xsin2x + 1/4 cos2x + C。<br /></span>
<span class="md_line"> <br /></span>
<span class="md_line md_line_end">那dy/dx - x/e^y =0的通解呢?校花双目含情脉脉,媚得快要滴出水来。学长如痴如醉,用手中的笔把e^y dy与x dx积来积去,尽情玩弄,直到校花娇哼一声,两边都酣畅淋漓地分离出变量……</span>
</p>
</blockquote>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">以下这个片段入选一个美国网站评选的 2005 年最糟糕的英文小说大奖:</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start md_line_end">“他死盯着她丰硕的胸部,开始幻想起他那台“凯旋-喷火”老爷车里的斯托姆伯格气化器。那是一台性能卓越、外形优美的机器,就挺立在进气歧管儿上,渴求着一双经验丰富的手去摆弄。润油管上的多边形小螺丝帽儿乞求着象销售手册第七章那样被检查和调校。”</span>
</p>
</blockquote>
<h1 id="toc_0" class="h16 md_first_h"><span class="span_for_h">Reference</span></h1>
<ul class="md_list md_ul">
<li class="md_li"><span class="md_li_span">《三国机密》
</span></li>
<li class="md_li"><span class="md_li_span">《长安的荔枝》
</span></li>
<li class="md_li"><span class="md_li_span">https://www.zhihu.com/question/23820465/answer/25779348
</span></li>
</ul>Dynamic 3D Physical Rope in Godot - Part22023-11-25T06:47:00Ztechnology/2023-11-25Jury Schon's Blog<p class="md_block">
<span class="md_line md_line_start md_line_end">In <a class="md_compiled" href="/post/technology/2023-09-05">previous article</a>, we solve the problem of generating <code>PinJoint3D</code> at the right places on the fly, given two <code>PhysicsBody3D</code> and how long the rope is.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">In this article, we try to draw a 3D rope mesh alongside those <code>PinJoint3D</code> in global space.</span>
</p>
<p class="md_compiled md_paragraph_html"><video style="width: 100%; max-width: 600px" controls preload="auto">
<source src="https://juryschon.cn-sh2.ufileos.com/rope_mesh.mp4" type="video/mp4"></source>
</video></p><h1 id="toc_0" class="h16 md_first_h"><span class="span_for_h">ImmediateMesh and Curve3D</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end"><code>ImmediateMesh</code> is a great fit because our rope is constantly updated in every physical frame. <code>ImmediateMesh</code> provides OpenGL 1.x style immediate mode API to draw mesh.</span>
</p>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">Godot also provides a class called <code>Curve3D</code>, representing what its name indicates. Combining these two classes we can draw our rope through following steps:</span>
</p>
<ul class="md_list md_ul">
<li class="md_li"><span class="md_li_span">After all <code>PinJoint3D</code>s are arranged at the right place during the rope generation, we save their positions along with two anchor positions into a <code>Curve3D</code>
</span></li>
<li class="md_li"><span class="md_li_span">In every physical frame, we update the points of curve according to the place of pinjoints and anchors, whose positions are already solved by game engine's physics.
</span></li>
<li class="md_li"><span class="md_li_span">In every rendering frame, we draw a 3d rope using <code>ImmediateMesh</code> with the points in the curve.
</span></li>
</ul>
<h1 id="toc_1" class="h16"><span class="span_for_h">Draw 3D rope from a Curve3D</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">For every point of the curve, we extrude the point to a circle. It's not a precise circle in parametric equation, but a approximate one constitued of discrete vertices as we are doing real time rendering instead of offline ray tracing.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">The function below returns the points on the cross section circle from a point of curve. With the <code>section_circle_resolution</code> increases, the circle is more continous at the cost of performance.</span>
</p>
<pre class="lang_gdscript"><code>func _generate_cross_section_circle_points(point: Vector3, next_point: Vector3) -> PackedVector3Array:
var points := PackedVector3Array()
var dir := next_point - point
var right := dir.cross(Vector3.UP).normalized()
var up := right.cross(dir).normalized()
for i in section_circle_resolution:
var phi = 2.0 * PI * float(i) / float(section_circle_resolution)
var local_point := Vector3(sin(phi) * radius, cos(phi) * radius, 0.0)
var global_point := point + right * local_point.x + up * local_point.y
points.push_back(global_point)
return points</code></pre>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">After we have the points on the circle, we draw quads between sibling circles. A quad is just two triangles. Note that we deliver our vertices to <code>ImmediateMesh</code> clockwise. <code>( a[i], b[i], b[j] )</code> then <code>( a[i], b[j], a[j] )</code>.</span>
</p>
<p class="md_block">
<span class="md_line md_line_dom_embed md_line_with_image md_line_start md_line_end"><img class="md_compiled " src="/Technology/_image/dynamic_rope_6.png" alt="" title="" ></span>
</p>
<pre class="lang_gdscript"><code>func _draw_quad_between_circle_points(circle_points: PackedVector3Array, next_circle_points: PackedVector3Array):
for i in section_circle_resolution:
var j := (i + 1) % section_circle_resolution
# First triangle
var i_normal := -1.0 * (circle_points[i] - circle_points[j]).cross(next_circle_points[i] - circle_points[i]).normalized()
mesh.surface_set_normal(i_normal)
mesh.surface_add_vertex(circle_points[i])
var next_i_normal := -1.0 * (circle_points[i] - next_circle_points[i]).cross(next_circle_points[i] - next_circle_points[j]).normalized()
mesh.surface_set_normal(next_i_normal)
mesh.surface_add_vertex(next_circle_points[i])
var next_j_normal := -1.0 * (circle_points[j] - next_circle_points[j]).cross(next_circle_points[i] - next_circle_points[j]).normalized()
mesh.surface_set_normal(next_j_normal)
mesh.surface_add_vertex(next_circle_points[j])
# Second triangle
mesh.surface_set_normal(i_normal)
mesh.surface_add_vertex(circle_points[i])
mesh.surface_set_normal(next_j_normal)
mesh.surface_add_vertex(next_circle_points[j])
var j_normal := -1.0 * (circle_points[i] - circle_points[j]).cross(next_circle_points[j] - circle_points[j]).normalized()
mesh.surface_set_normal(j_normal)
mesh.surface_add_vertex(circle_points[j])</code></pre>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">For more detail, you can checkout the <a class="md_compiled" href="https://github.com/skyquakers/godot-rope3d">github repo</a>, especially the source file <code>rope_mesh.gd</code></span>
</p>
<h1 id="toc_2" class="h16"><span class="span_for_h">Further Improvements</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">The repo still has a lot to do, like UVs and rewriting in native languages like Rust to support more pinjoints at the same time. More pinjoints we have, more real it feels and behaves.</span>
</p>
<h1 id="toc_3" class="h16"><span class="span_for_h">Reference</span></h1>
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start">https://docs.godotengine.org/en/stable/tutorials/3d/procedural_geometry/immediatemesh.html<br /></span>
<span class="md_line">https://docs.godotengine.org/en/stable/classes/class_curve3d.html<br /></span>
<span class="md_line md_line_end">https://chat.openai.com</span>
</p>Abandoned Space Werewolf Indiegame Showcase2023-09-10T16:49:00Zstartup/2023-9-11Jury Schon's Blog<p class="md_block">
<span class="md_line md_line_start">I had been developing this game in my spare time for quite a long time and decided not to coninue.<br /></span>
<span class="md_line md_line_end">So I make a self-conclusion and momento.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Youtube video below, some readers may need a VPN.</span>
</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/gW4NSra8oI0?si=CyVAexd6ovYZi9DF" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
<span class="md_repeated_n md_repeated_n_1"></span>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">I completed these features using Godot 3.5:</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span">A game launcher with room management and authentication
</span></li>
<li class="md_li"><span class="md_li_span">Multiplayer using legacy RPC solutions provided by Godot3.5 (from 4.0 on things get much simpler)
</span></li>
<li class="md_li"><span class="md_li_span">Rigidbody physics character movement
</span></li>
<li class="md_li"><span class="md_li_span">Dynamic physical rope, click <a class="md_compiled" href="https://juryquinn.com/post/technology/2023-09-05">here</a> for more info
</span></li>
<li class="md_li"><span class="md_li_span">Inventory system
</span></li>
<li class="md_li"><span class="md_li_span">Crafting system
</span></li>
<li class="md_li"><span class="md_li_span">Path navigation
</span></li>
<li class="md_li"><span class="md_li_span">Procedural performant background asteriods using MultiMesh
</span></li>
<li class="md_li"><span class="md_li_span">Container movement with movable characters inside, syncing across multiplayers
</span></li>
<li class="md_li"><span class="md_li_span">Procedural minerals generating on asteroids
</span></li>
</ol>
<p class="md_block">
<span class="md_line md_line_start md_line_end">I also completed server side alone using Nakama, with ability to create dedicated instances dynamically, click <a class="md_compiled" href="https://juryquinn.com/post/technology/2023-01-04">here</a> for more info</span>
</p>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">Despite the developing, I also did:</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span">Gameplay desgin
</span></li>
<li class="md_li"><span class="md_li_span">Modeling of spaceship, minerals, asteroids
</span></li>
<li class="md_li"><span class="md_li_span">Map editing
</span></li>
<li class="md_li"><span class="md_li_span">Jump, weapon holding, struggling, shivering and many other keyframe animations of human character
</span></li>
<li class="md_li"><span class="md_li_span">Walk, weapon holding, sprint in space, death, and many other keyframe animations of astronaut
</span></li>
<li class="md_li"><span class="md_li_span">A quick iteration workflow with Google Sheets for numerical design
</span></li>
<li class="md_li"><span class="md_li_span">Graphic design of many posters and banners
</span></li>
</ol>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">I've abandoned the development of this game because:</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span">I found it no fun to play at all and don't know how to improve gameplay by myself
</span></li>
<li class="md_li"><span class="md_li_span">Godot 4.0 has been released with so many incompatibilies and it's tedious to migrate a project quite large
</span></li>
<li class="md_li"><span class="md_li_span">Someone agreed to help me with gameplay design so I've started to make a new game
</span></li>
<li class="md_li"><span class="md_li_span">Realistic space theme is not a good choice because space is very empty in reality
</span></li>
</ol>
<h1 id="toc_0" class="h16 md_first_h"><span class="span_for_h">Lesson</span></h1>
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start md_line_end">Being capabable of doing all these things above doesn't automatically give you a fun game that sells. Making games is not all about technology. I should have thought it quite clear that what is the gameplay of my game and is it interesting to play. I also should have kept testing gameplay as possibly as I could during the development instead of finding it too late when the framework is about to complete.</span>
</p>Dynamic 3D Physical Rope in Godot - Part12023-09-06T15:51:00Ztechnology/2023-09-05Jury Schon's Blog<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">It's not hard to <a class="md_compiled" href="https://www.youtube.com/watch?v=0AR7RqQ_QzU">create a nice static physical rope using blender</a>. However in some scenarios we have to create physical ropes at runtime. This article tries to solve the problem below:</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">Given two <code>PhysicsBody3D</code> and how long the rope is(the length is always longer than the distance between two bodies), connect them at runtime with a rope that reacts to physics system.</span>
</p>
</blockquote>
<p class="md_block">
<span class="md_line md_line_start md_line_end">I solved this problem in an abandoned 3D space werewolf game project last year. Now I want to share with you how I do it.</span>
</p>
<p class="md_compiled md_paragraph_html"><video style="width: 100%; max-width: 600px" controls preload="auto">
<source src="https://juryschon.cn-sh2.ufileos.com/physical_rope.mp4" type="video/mp4"></source>
</video></p><h1 id="toc_0" class="h16 md_first_h"><span class="span_for_h">Physical Rope Basics</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Similar to fluid matter simulation, a continuous physical rope is often divided into discrete parts.</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span>
<p class="md_block">
<span class="md_line md_line_dom_embed md_line_with_image md_line_start md_line_end"><img class="md_compiled " src="/Technology/_image/dynamic_rope_1.png" alt="" title="" ></span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Each circle is a JointPin3D. Segment(rectangles in the image) will not be drawn as it is only actually abstract concept here(We will talk about the rendering in part 2). Each joint connects two neighbour joint or physical body. By using Joints, we get a physical rope of 6 degrees of freedom.</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">Tips: To make Joint work, we need at least one <code>RigidBody3D</code></span>
</p>
</blockquote>
<h1 id="toc_1" class="h16"><span class="span_for_h">Inputs</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">In order to create the rope at runtime, we accept 4 inputs.</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span"><code>resolution: int</code>, segments count. The higher this value, the more continuous this discrete simulation will feel like.
</span></li>
<li class="md_li"><span class="md_li_span"><code>rope_length: float</code>, how long the rope is. Of course this must be bigger than the distance between two anchors.
</span></li>
<li class="md_li"><span class="md_li_span"><code>start_point: Node3D</code>, a Node3D providing the information of global position of the starting anchor of the rope whose parent node is the physical body.
</span></li>
<li class="md_li"><span class="md_li_span"><code>end_point: Node3D</code>, same as above but provide the information of the ending anchor.
</span></li>
</ol>
<h1 id="toc_2" class="h16"><span class="span_for_h">Overview</span></h1>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">Basically we just need two steps:</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span">Place segments correctly along the rope, we can easily calculate segment length from <code>rope_length</code>, and <code>resolution</code> which is joints count.
</span></li>
<li class="md_li"><span class="md_li_span">In each gap between every two segments, we put a joint right at it, connecting two neighbour segments.
</span></li>
</ol>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Step 2 is fairly easy, we need to talk more on step 1.</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_3" class="h16"><span class="span_for_h">Arrange the Positions of Segments</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">How to know the positions of segments even before we create them?</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Well, we need to draw a line between two anchors, then arrange segments along this line evenly given <code>segment_length</code> and <code>joint_length</code>, where <code>segment_length</code> can be very small like <code>0.1</code> (Pinjoint3D will work the same whatever the initial distance is), <code>joint_length = (rope_length - resolution * segment_length) / joint_count</code>, <code>joint_count = resolution + 1</code>.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">We place first and last joint at the position of start point and end point to let module user to customize.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">As for situations where <code>resolution</code> is 1 or 2, the problem is much simpler.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">When <code>resolution</code> is 1, there is only one segment connecting two joints.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">When <code>resolution</code> is 2, joints count is 3, of which 2 are at anchors positions and segments count is 2(same as <code>resolution</code>). To make these 3 joints positioned evenly, just place the left one in the middle.</span>
</p>
<p class="md_block">
<span class="md_line md_line_dom_embed md_line_with_image md_line_start md_line_end"><img class="md_compiled " src="/Technology/_image/dynamic_rope_2.png" alt="" title="" ></span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">When <code>resolution</code> is greater than 2, things get interesting. As the rope length is longer than the distance, we must bend the line into two parts at a turning point. Let's say point T, and let's assume we need to connect body A and body B. The line is divided into two parts, one part is from body A to point T and the other one is from point T to body B. Then we just need to place pinjoints evenly along these two parts.</span>
</p>
<p class="md_block">
<span class="md_line md_line_dom_embed md_line_with_image md_line_start md_line_end"><img class="md_compiled " src="/Technology/_image/dynamic_rope_3.png" alt="" title="" ></span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Now we need to calculate the global position of point T.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Suppose the length of part before T is <code>r1</code>, the length of part after T is <code>r2</code>. It is not hard to find that the trajectory of T is a circle, intersecting by two spheres whose radiuses are <code>r1</code> and <code>r2</code>.</span>
</p>
<p class="md_block">
<span class="md_line md_line_dom_embed md_line_with_image md_line_start md_line_end"><img class="md_compiled " src="/Technology/_image/dynamic_rope_4.png" alt="" title="" ></span>
</p>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">We already know:</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span">two centers of the spheres, let's call them <code>c1</code> and <code>c2</code>.
</span></li>
<li class="md_li"><span class="md_li_span">the normal of the intersecting circle, which is a vector between <code>c1</code> and <code>c2</code>.
</span></li>
<li class="md_li"><span class="md_li_span"><code>r1</code> and <code>r2</code>, we can approximately consider they are nearly half of the <code>rope_length</code>. If <code>r1</code> does not equal <code>r2</code> because of the parity of <code>resolution</code>, that's totally ok.
</span></li>
</ol>
<p class="md_block">
<span class="md_line md_line_start md_line_end">With parametric equations, we can solve this spatial circle.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">$ x (\theta) = c_1 + r cos ( \theta ) a_1 + r sin ( \theta ) b_1 $</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">$ y (\theta) = c_2 + r cos ( \theta ) a_2 + r sin ( \theta ) b_2 $</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">$ z (\theta) = c_3 + r cos ( \theta ) a_3 + r sin ( \theta ) b_3 $</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">where $(c_1, c_2, c_3 $) is the center of the circle, $\vec a$ and $\vec b$ are perpendicular unit vectors inside the circle, they are perpendicular to each other and perpendicular to the normal $\vec n$.</span>
</p>
<p class="md_block">
<span class="md_line md_line_dom_embed md_line_with_image md_line_start md_line_end"><img class="md_compiled " src="/Technology/_image/dynamic_rope_5.jpg" alt="" title="" ></span>
</p>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">Here is the circle code:</span>
</p>
<pre class="lang_gdscript"><code>class_name SphereSphereCircle
extends Object
var _rng = RandomNumberGenerator.new()
# spheres
var _sphere_a_center: Vector3
var _sphere_a_radius: float
var _sphere_b_center: Vector3
var _sphere_b_radius: float
var _sphere_sphere_distance: float
# triangle
var cosA: float
var a: float
var b: float
var c: float
# circle
var center: Vector3
var radius: float
var vector_a: Vector3
var vector_b: Vector3
var normal: Vector3
func _init(sphere_a_position: Node3D, sphere_a_radius: float, \
sphere_b_position: Node3D, sphere_b_radius: float):
_sphere_a_center = sphere_a_position.global_transform.origin
_sphere_a_radius = sphere_a_radius
_sphere_b_center = sphere_b_position.global_transform.origin
_sphere_b_radius = sphere_b_radius
_sphere_sphere_distance = _sphere_a_center.distance_to(_sphere_b_center)
_calc_center()
_calc_radius()
_calc_vectors()
func _calc_center():
a = _sphere_b_radius
b = _sphere_a_radius
c = _sphere_sphere_distance
cosA = (pow(b, 2) + pow(c, 2) - pow(a, 2)) / (2 * b * c)
var k := b * cosA / c
normal = _sphere_b_center - _sphere_a_center
center = _sphere_a_center + k * normal
func _calc_radius():
var sinA = sqrt(1 - pow(cosA, 2))
radius = b * sinA
func _calc_vectors():
var unit = Vector3.UP
if not unit.cross(normal) == Vector3.ZERO:
vector_a = unit.cross(normal).normalized()
else:
unit = Vector3.RIGHT
vector_a = unit.cross(normal).normalized()
vector_b = normal.cross(vector_a).normalized()
func get_point(theta: float) -> Vector3:
var point := center + radius * cos(theta) * vector_a + radius * sin(theta) * vector_b
return point</code></pre>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">Initialize the circle, just randomize a <code>theta</code> between $ [0, 2 \pi] $, and call <code>get_point(theta)</code>, you get the global position of point T.</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span>
<p class="md_block">
<span class="md_line md_line_start md_line_end">As we have point T, the rest of the work is just placing segments evenly alongside the bended line and connecting segments or phyiscal bodies with <code>PinJoint3D</code>s.</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_4" class="h16"><span class="span_for_h">Insert PinJoints</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Now we have <code>segments</code> consisted of <code>RopeSegment</code>, each of them has correct global position. Now what we do is to connect them with <code>PinJoint3D</code>s.</span>
</p>
<pre class="lang_gdscript"><code>func _add_joints():
var first_segment: RopeSegment = segments[0]
var last_segment: RopeSegment = segments[resolution - 1]
var first_joint = _generate_joint_between(head_anchor, first_segment)
var last_joint = _generate_joint_between(last_segment, tail_anchor, 0)
first_segment.add_child(first_joint)
first_joint.global_transform.origin = start_point.global_transform.origin
last_segment.add_child(last_joint)
last_joint.global_transform.origin = end_point.global_transform.origin
for i in len(segments):
if segments.size() >= i + 2:
var segment: RopeSegment = segments[i]
var next_segment: RopeSegment = segments[i + 1]
var joint = _generate_joint_between(segment, next_segment)
segment.add_child(joint)
var sphere_middle_point = segment.global_transform.origin + (next_segment.global_transform.origin - segment.global_transform.origin) * 0.5
joint.global_transform.origin = sphere_middle_point</code></pre>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">And <code>_generate_joint_between()</code> just setup the constraints for game engine's physics to resolve.</span>
</p>
<pre class="lang_gdscript"><code>func _generate_joint_between(body_a: PhysicsBody3D, body_b: PhysicsBody3D, priority: int = 1) -> PinJoint3D:
var joint := PinJoint3D.new()
joint.set_node_a(body_a.get_path())
joint.set_node_b(body_b.get_path())
joint.set_solver_priority(priority)
return joint</code></pre>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">For more detail, you can checkout the <a class="md_compiled" href="https://github.com/skyquakers/godot-rope3d">github repo</a>.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">In <a class="md_compiled" href="/post/technology/2023-11-25">next article</a>, we will talk about the rendering part.</span>
</p>
<h1 id="toc_5" class="h16"><span class="span_for_h">Reference</span></h1>
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start">https://www.zhihu.com/question/39049685<br /></span>
<span class="md_line md_line_end">https://blog.sina.com.cn/s/blog_6496e38e0102vi7e.html</span>
</p>我挤上了女性专用车厢2023-04-17T11:11:00Zthoughts/2023-04-17Jury Schon's Blog<p class="md_block">
<span class="md_line md_line_dom_embed md_line_start md_line_end"><center><img class="md_compiled " src="/Thoughts/_image/2023-04-17-1.jpg" alt="" title="" ></center></span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">注意到车窗上标志的时候,我起初并没有慌张,因为平时做电车,女性专用车厢里也有男性,似乎规则并没有启用。我想知道此时此刻,早高峰时期,是不是这节车厢就真的变成了女性专用车厢。很不幸,目前车厢里除了我没有一个男性。我立刻紧张起来,车厢里弥漫的香水味也变成了对我的拷打。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">我开始回溯一些记忆,情况愈发不妙起来。上一站上车的人也全部是女性,不光如此,其中有一位女士,上车之前打量了我一眼,然后皱了皱眉头。回想到这里,我的脸开始发热。现在我作为唯一一名男性挤在早高峰的女性专用车厢里,犹如大海中的一叶舟。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">这样令人尴尬的事情是如何发生的呢。我从车站楼梯下到月台,发现有一辆电车正要发车,于是赶忙就近登上车厢,完全没有注意到这是一节女性专用车厢。不如说,就算注意到也晚了,因为就在登上车之后,电车门立刻就关上了。就在我迈入这香气弥漫的车厢的那一刻,我的罪恶已无法避免。我现在唯一能做的,就是在下一站下车,然后换一节车厢再上车。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">这两三分钟无比漫长。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">终于到站了,我又不得不面对另一个刑场。电车也好地铁也好新干线也好,大家都严格遵守先下后上的规则。而等待上车的人们,是的没错,都是女性。她们分两列站在车门两边,队形如此整齐,只把中间的道路让给我,我无处可逃。我只能迎着她们所有人的目光,让她们注视着我作为唯一一个男性从女性专用车厢里走出来。走出来的不光是一个普通社会人男性,还是贪恋与女性身体接触的死宅,是一个変態,至少是一个変な人,一个不遵守规矩的人。她们能想到我是外国人刚来日本不懂吗,想必不会吧,我长着一张昂扬的亚洲脸。</span>
</p>
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start md_line_end">没有什么比这段经历更能诠释什么是社会性死亡了。</span>
</p>日本见闻(其一)2023-03-23T15:45:00Zthoughts/2023-03-23Jury Schon's Blog<p class="md_block">
<span class="md_line md_line_start md_line_end">站在中国人的角度,只说觉得新鲜的地方,在来日之前就已经知道,比如有礼貌,街道干净整洁,自来水直接喝之类的,就不说了。</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_0" class="h16 md_first_h"><span class="span_for_h">服务业的人工</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">从下飞机开始,机场五步一岗十步一哨,每个人都向你问好,生怕你走错。有许多留学生在这里工作,中国游客会有中国留学生帮忙,西方游客的话,印度留学生接待居多。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">在日本第一餐饭吃的是成田机场候机大厅二楼的吉野家,我走到结账的地方问是不是在这儿点餐,答曰在座位上点餐。此时马上就来一个小姑娘带我坐到座位上点餐。日本很多店还是先吃饭再结账,专门有一个人带你入座点餐,即使是一些拥有点单机器的饭店,也是对着点单机器刷卡或者投入现金,没有看到扫码点单然后手机支付的情况。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">成田机场下去地铁的地方,专门有一个人站在电梯门口向你问好。入口的标志上,汉字写着“铁道”,英文标着“Train”,于是我问她 Google Map 上的“天空快线”是不是从这儿下去。她先是看了一眼我手机,然后戴上了老花镜继续看我的手机,最后摇了摇头说不知道,那边有案内,请去那边问。她的工作就是单纯的向人问好。后来我理解她为啥不知道了,因为“天空快线”是一个糟糕的翻译,如果直接写 Skyliner,我连问的必要都没有。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">进到了成田机场地铁站,有一位端庄的女士,穿着不知是机场还是地铁部门的制服,大概四五十来岁,妆化得很好看。她的工作就是帮人买票。如果你不知道怎么买票,她会帮你。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">办理在留卡住所登录的时候,区役所办事大厅专门有人教你怎么填表格,要叫什么号。区役所相当于国内的区政府,不过属于地方自治体,在区政府办事有个人专门教你填表格,对你礼貌周到,这在中国无法想象。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">这种对人工的大量应用,是一种减少贫富差距的手段。如果在中国,软件不好用,就更新软件,使之更好用;在日本,如果软件不好用,就专门请一个人,站在旁边教你怎么用。这里的“软件”指的是一种广义上的做事流程。这是一种效率和公平的不同取舍。</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_1" class="h16"><span class="span_for_h">租房制度</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">许多人在日本租了多年房也搞不清楚日本的租房制度是怎么回事。首先日本立法规定房东不能赶房客,这就催生了形形色色的保证公司。如果房客乱搞,保证公司会给房东赔钱。风险就落到了保证公司这里,为了平衡这种风险,必然也需要给保证公司一种代偿。市场上有各种各样的保证公司,保证公司和保证公司的策略也不一样。有的是每个月多收,有的是前期支付一次性费用。至于具体收多少,则取决于保证公司对你风险的评估情况,如果你是留学生,随时可能回国退租,也没有收入,支付能力存疑,就会多收一点保证金。如果已经能提供用人单位的内定书,证明你的收入,则可以少收一点。有的是前期多支付房租的 40%,有的甚至要多支付 100%。你作为一个外国人,连人都还没来日本,就可以直接租房给你,甚至这样的保证公司,市场上也有。日本最大的专门面向外国人的保证公司是 GTN。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">至于礼金和押金,在自由市场竞争之下,当然会有许多房子是根本不需要礼金和押金的。如果供需关系发生改变,可以预见礼金和押金的情况也会改变。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">管理费也就是物业费,是每个月都要付的,没有看到不需要管理费的房子。所以真实的房租至少要加上管理费。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">日本的收入房价比跟中国相比简直是太便宜了。蕨,到东京站四十分钟的乡下,全都是独栋,售价折合人民币在 250 万以内。400 万可以在川口买一栋三层楼的房子,包括装修,包括土地所有权,月供和你自己租这套房子差不多。日本有房产税,并且大部分房产在可预见的未来内,不会发生类似中国早些年的快速上涨,因此日本人也没什么买房的热情。正因如此,许多开发商盖房子就是专门拿来租的。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">听说在日华人之间也有另外一种租房方法,就是中国人之间私下交易,不走保证公司,会便宜一点。但是:1. 保证公司的出现是对房东不能单方面毁约的代偿,如果不走保证公司,与房东私下的房租契约在多大程度上受法律保护我目前不清楚;2. 制度安排只能界定权责,并不能消除风险,保证公司承担了信息不对称的风险,也享受了其带来的收益。私下租房想必也会催生其他的制度安排,比如押金可能会更常见。</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_2" class="h16"><span class="span_for_h">街道很窄</span></h1>
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start md_line_end">日本很少有四车道,两车道就算的上大马路,绝大多数路都是一车道。这给了行人很大便利,过马路不超过 10 秒,从 A 走到 B,有无数条道可以走。当然,对车来说,也有无数条道可以走,距离也差不多,这就缓解了拥堵。我认为这种窄街道的规划增加了城市的车容量和人口密度的上限,而不是减少。另外,这仅是我的主观看法,我认为这种规划,看上去也更加美观,温情。</span>
</p>Integrate Godot Headless Server with Nakama2023-01-16T13:18:00Ztechnology/2023-01-04Jury Schon's Blog<h1 id="toc_0" class="h16 md_first_h"><span class="span_for_h">Introduction</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">"Nakama", aka 仲間, means "friend" in Japanese, which implies its multiplayer capability. Nakama is an open source BaaS server software. You can deploy it on your own server and get a free BaaS framework immediately. Besides, Nakama provides various client SDKs including Unity, Unreal, Godot, Cocos2d, Defold, etc. Using Nakama can save your time when it comes to multiplayer gamedev.</span>
</p>
<p class="md_block">
<center>
<span class="md_line md_line_dom_embed md_line_with_image"><img class="md_compiled " src="/Technology/_image/nakama_logo.png" alt="" title="" ><br /></span>
</center></p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Different multiplayer games have different gameplays. Nakama allows developers to write server codes with its plugin system. At the moment it provides Lua runtime, Go runtime and JavaScript runtime. You can write plugins or modules and Nakama will load them at runtime. For example, you can write simple server logic inside <code>matchLoop</code> callback called by Nakama every server frame. The plugin system is enough for many light weight scenarios like turn-based card games, simple 2d games, chat rooms, etc. What about complex server logic like physics? What about combining Nakama with dedicated server exported by the game engine?</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Well, Nakama allows you to do that.</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">For complex gameplay where you want the physics running server-side (e.g. Unity headless instances). Nakama can manage these headless instances, via an orchestration layer, and can be used for matchmaking, moving players on match completion, and reporting the match results.</span>
</p>
</blockquote>
<p class="md_block">
<span class="md_line md_line_start md_line_end">However, the documentation doesn't cover this topic in detail. There's no example or guides on how to do it exactly.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">This article talks about integrating dedicated server exported by Godot Engine with Nakama. All server codes are written in TypeScript. This article won't talk about the details of exporting dedicated server (You can get such information on Godot Docs), details of multiplayer game developing, or details of how to dockerize your Godot server or how to setup a Nakama server. These topics are beyond the range of this article. If you use another game engine, this article should help as well.</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_1" class="h16"><span class="span_for_h">Communication Overview</span></h1>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">We have two types of Nakama clients, the client on player's device and the client on server. <strong class="md_compiled md_compiled_strong">The dedicated server is essentially also a client to Nakama</strong>. All of the communications are implemented by four ways:</span>
</p>
<ol class="md_list md_ol" start="1">
<li class="md_li"><span class="md_li_span">RPC calls between Nakama client and Nakama server;
</span></li>
<li class="md_li"><span class="md_li_span">Match state message broadcast between Nakama client and Nakama server;
</span></li>
<li class="md_li"><span class="md_li_span">RPC calls between Godot client and Godot server;
</span></li>
<li class="md_li"><span class="md_li_span">Nakama HTTP API calls between Nakama client and Nakama server.
</span></li>
</ol>
<p class="md_block">
<center>
<span class="md_line md_line_dom_embed md_line_with_image"><img class="md_compiled " src="/Technology/_image/godot_nakama_structure.png" alt="" title="" ><br /></span>
</center></p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_2" class="h16"><span class="span_for_h">Create Match</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">This is done by calling Nakama RPC instead of a direct client HTTP API call, because we need to insert initial match data into storage like room status(Has game server already started? Are we waiting for players to be ready?), container host and port, player defined room name, etc. We insert the data, then create the match by API provided in server runtime, then return the match id to the client so player can join the match.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">User ID is default system user ID 00000000-0000-0000-0000-000000000000 because these objects can never be modified by any player.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end"><code>'room'</code> is the module name, you may register a different module. Like I said above, details on how to setup Nakama server is not included in this article. You should read Nakama's doc if you are not familiar with this.</span>
</p>
<div class="codehilite code_lang_javascript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">const</span> <span class="nx">createMatch</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">RpcFunction</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">ctx</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">logger</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">Logger</span><span class="p">,</span> <span class="nx">nk</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">Nakama</span><span class="p">,</span> <span class="nx">payload</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">match_name</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">payload</span><span class="p">).</span><span class="nx">match_name</span>
<span class="kr">const</span> <span class="nx">match_id</span> <span class="o">=</span> <span class="nx">nk</span><span class="p">.</span><span class="nx">matchCreate</span><span class="p">(</span><span class="s1">'room'</span><span class="p">)</span>
<span class="kr">const</span> <span class="nx">newMatchInfo</span><span class="o">:</span> <span class="nx">TrodMatchInfo</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">host</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="nx">port</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="nx">matchId</span><span class="o">:</span> <span class="nx">match_id</span><span class="p">,</span>
<span class="nx">containerId</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="nx">status</span><span class="o">:</span> <span class="s1">'preparing'</span><span class="p">,</span>
<span class="nx">kickIds</span><span class="o">:</span> <span class="p">[],</span>
<span class="nx">roomOwnerId</span><span class="o">:</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">userId</span><span class="p">,</span>
<span class="nx">roomName</span><span class="o">:</span> <span class="nx">match_name</span>
<span class="p">}</span>
<span class="nx">nk</span><span class="p">.</span><span class="nx">storageWrite</span><span class="p">([</span>
<span class="p">{</span>
<span class="nx">collection</span><span class="o">:</span> <span class="nx">TrodMatchInfoCollectionKey</span><span class="p">,</span>
<span class="nx">key</span><span class="o">:</span> <span class="nx">match_id</span><span class="p">,</span>
<span class="nx">userId</span><span class="o">:</span> <span class="nx">SystemUserId</span><span class="p">,</span>
<span class="nx">value</span><span class="o">:</span> <span class="nx">newMatchInfo</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">])</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'new match info written'</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="nx">match_id</span><span class="p">,</span> <span class="nx">match_name</span> <span class="p">})</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="k">throw</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end--><h1 id="toc_3" class="h16"><span class="span_for_h">Use Match Loop as Room Handler</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">After joining the match, client and server can communicate through Match State Message.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Server code</span>
</p>
<div class="codehilite code_lang_javascript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">const</span> <span class="nx">matchLoop</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">ctx</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">logger</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">Logger</span><span class="p">,</span> <span class="nx">nk</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">Nakama</span><span class="p">,</span> <span class="nx">dispatcher</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">MatchDispatcher</span><span class="p">,</span> <span class="nx">tick</span><span class="o">:</span> <span class="nx">number</span><span class="p">,</span> <span class="nx">state</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">MatchState</span><span class="p">,</span> <span class="nx">messages</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">MatchMessage</span><span class="p">[])</span> <span class="o">:</span> <span class="p">{</span> <span class="nx">state</span><span class="o">:</span> <span class="nx">nkruntime</span><span class="p">.</span><span class="nx">MatchState</span> <span class="p">}</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="s1">'Room match loop executed'</span><span class="p">)</span>
<span class="nx">messages</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">message</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'Received %v from %v'</span><span class="p">,</span> <span class="nx">message</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="nx">message</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">userId</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">opCode</span> <span class="o">===</span> <span class="nx">MatchOpCode</span><span class="p">.</span><span class="nx">ReadyStateUpdatedYes</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">})</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">Client code</span>
</p>
<div class="codehilite code_lang_python highlight"><pre><span></span><span class="c1"># client code</span>
<span class="c1"># ...somewhere</span>
<span class="n">Network</span><span class="o">.</span><span class="n">socket</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">"received_match_state"</span><span class="p">,</span> <span class="bp">self</span><span class="p">,</span> <span class="s2">"_on_match_state"</span><span class="p">)</span>
<span class="c1"># ...</span>
<span class="n">func</span> <span class="n">_on_match_state</span><span class="p">(</span><span class="n">match_state</span><span class="p">:</span> <span class="n">NakamaRTAPI</span><span class="o">.</span><span class="n">MatchData</span><span class="p">):</span>
<span class="k">if</span> <span class="n">match_state</span><span class="o">.</span><span class="n">op_code</span> <span class="o">==</span> <span class="n">GameData</span><span class="o">.</span><span class="n">OpCode</span><span class="o">.</span><span class="n">ReadyStateUpdatedYes</span><span class="p">:</span>
<span class="k">pass</span>
<span class="c1"># ...handle match state message</span>
</pre></div>
<!--block_code_end-->
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">Op codes must be defined both on client and server, values must be same. Below is what I defined in my case:</span>
</p>
<div class="codehilite code_lang_javascript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">enum</span> <span class="nx">MatchOpCode</span> <span class="p">{</span>
<span class="nx">StartGameServer</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
<span class="nx">GameServerInited</span><span class="p">,</span>
<span class="nx">StartGameServerFailed</span><span class="p">,</span>
<span class="nx">ReadyStateUpdatedYes</span><span class="p">,</span>
<span class="nx">ReadyStateUpdatedNo</span><span class="p">,</span>
<span class="nx">ReadyStatesBroadcast</span><span class="p">,</span>
<span class="nx">Kicked</span><span class="p">,</span>
<span class="nx">UsernameUpdated</span><span class="p">,</span>
<span class="nx">RoomNameUpdated</span><span class="p">,</span>
<span class="nx">GameServerStarting</span><span class="p">,</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">For example, when player hits ready button on the client, we emit a Match State Message.</span>
</p>
<div class="codehilite code_lang_python highlight"><pre><span></span><span class="n">func</span> <span class="n">_on_ready_button_clicked</span><span class="p">():</span>
<span class="n">AudioManager</span><span class="o">.</span><span class="n">play_click_sound</span><span class="p">()</span>
<span class="n">imready</span> <span class="o">=</span> <span class="ow">not</span> <span class="n">imready</span>
<span class="n">var</span> <span class="n">op_code</span> <span class="o">=</span> <span class="n">GameData</span><span class="o">.</span><span class="n">OpCode</span><span class="o">.</span><span class="n">ReadyStateUpdatedYes</span> <span class="k">if</span> <span class="n">imready</span> <span class="k">else</span> <span class="n">GameData</span><span class="o">.</span><span class="n">OpCode</span><span class="o">.</span><span class="n">ReadyStateUpdatedNo</span>
<span class="k">yield</span><span class="p">(</span><span class="n">Network</span><span class="o">.</span><span class="n">socket</span><span class="o">.</span><span class="n">send_match_state_async</span><span class="p">(</span><span class="n">match_id</span><span class="p">,</span> <span class="n">op_code</span><span class="p">,</span> <span class="n">JSON</span><span class="o">.</span><span class="k">print</span><span class="p">({})),</span> <span class="s2">"completed"</span><span class="p">)</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">And server broadcast to other clients</span>
</p>
<div class="codehilite code_lang_javascript highlight"><pre><span></span><span class="c1">// server code inside matchLoop</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">opCode</span> <span class="o">===</span> <span class="nx">MatchOpCode</span><span class="p">.</span><span class="nx">ReadyStateUpdatedYes</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">state</span><span class="p">.</span><span class="nx">readyStates</span><span class="p">[</span><span class="nx">message</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">userId</span><span class="p">]</span> <span class="o">=</span> <span class="kc">true</span>
<span class="nx">readyStatesUpdated</span> <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">opCode</span> <span class="o">===</span> <span class="nx">MatchOpCode</span><span class="p">.</span><span class="nx">ReadyStateUpdatedNo</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">state</span><span class="p">.</span><span class="nx">readyStates</span><span class="p">[</span><span class="nx">message</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">userId</span><span class="p">]</span> <span class="o">=</span> <span class="kc">false</span>
<span class="nx">readyStatesUpdated</span> <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">readyStatesUpdated</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">dispatcher</span><span class="p">.</span><span class="nx">broadcastMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">opCode</span><span class="p">,</span> <span class="nx">message</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="nx">message</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">Then client can update the ready states at local or display a "I'm ready" on UI.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">When the room owner (In my case only room owner can start the game) hits Start button, we also broadcast message to other clients like the example above. And more importantly, we need to create and start the game server container.</span>
</p>
<span class="md_repeated_n md_repeated_n_1"></span><h1 id="toc_4" class="h16"><span class="span_for_h">Spawn Server Container</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">I assume you already dockerize your game server, I'm afraid the only way to run game server is through Docker HTTP API. Because Nakama JavaScript runtime doesn't support running bash command. It may never support as Nakama is designed to be scalable and is typically run inside a container.</span>
</p>
<h3 id="toc_5" class="h16"><span class="span_for_h">Find an Available Port</span></h3>
<p class="md_block">
<span class="md_line md_line_start md_line_end">We actually don't know if a port can be used because of the limits of the TypeScript runtime. So I store the using ports in the storage. Every time we create a container, we only use the port that has not been stored, and has not been occupied by other services on server. We assign these ports before we start Nakama (retainedPorts below).</span>
</p>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">const</span> <span class="nx">canUsePort</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">port</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">logger</span>: <span class="kt">nkruntime.Logger</span><span class="p">,</span> <span class="nx">nk</span>: <span class="kt">nkruntime.Nakama</span><span class="p">)</span><span class="o">:</span> <span class="kr">boolean</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">retainedPorts</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">port</span><span class="p">)</span> <span class="o">!==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">objects</span> <span class="o">=</span> <span class="nx">nk</span><span class="p">.</span><span class="nx">storageRead</span><span class="p">([</span>
<span class="p">{</span> <span class="nx">collection</span>: <span class="kt">TrodPortsKey</span><span class="p">,</span> <span class="nx">key</span>: <span class="kt">port</span><span class="p">,</span> <span class="nx">userId</span>: <span class="kt">SystemUserId</span> <span class="p">}</span>
<span class="p">])</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">objects</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kc">false</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">Get an available port with <code>canUsePort</code> above.</span>
</p>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code inside startGameServer function</span>
<span class="kd">let</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">getRandomPort</span><span class="p">()</span>
<span class="kd">let</span> <span class="nx">triedTimes</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kr">const</span> <span class="nx">tryingCount</span> <span class="o">=</span> <span class="mi">20</span>
<span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="nx">canUsePort</span><span class="p">(</span><span class="nx">port</span><span class="p">,</span> <span class="nx">logger</span><span class="p">,</span> <span class="nx">nk</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">port</span> <span class="o">=</span> <span class="nx">getRandomPort</span><span class="p">()</span>
<span class="nx">triedTimes</span><span class="o">++</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">triedTimes</span> <span class="o">></span> <span class="nx">tryingCount</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'cannot allocate usable ports'</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">This solution is not perfect, we may start other services after we start Nakama. Accidentally, we may fail to create container because the port is occupied and player will see error messages.</span>
</p>
<h3 id="toc_6" class="h16"><span class="span_for_h">Pass Parameters to Godot server</span></h3>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">Command lines of container creating API can be used to pass parameters from Nakama to Godot server. These parameters can be retrieved by Godot server through APIs provided by game engine, like <code>OS.get_cmdline_args()</code> in Godot. In my case, I pass values below:</span>
</p>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code inside startGameServer function</span>
<span class="kr">const</span> <span class="nx">cmd</span> <span class="o">=</span> <span class="p">[</span>
<span class="sb">`--server-port=</span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="sb">`--match-id=</span><span class="si">${</span><span class="nx">matchId</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="sb">`--max-players=</span><span class="si">${</span><span class="nx">maxPlayers</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="sb">`--imposters-count=</span><span class="si">${</span><span class="nx">maxImposters</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="p">]</span>
<span class="kr">const</span> <span class="nx">containerId</span> <span class="o">=</span> <span class="nx">createContainer</span><span class="p">(</span><span class="nx">ImageName</span><span class="p">,</span> <span class="nx">port</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">,</span> <span class="sb">`game-server-</span><span class="si">${</span><span class="nx">matchId</span><span class="si">}</span><span class="sb">-</span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span>
</pre></div>
<!--block_code_end--><h3 id="toc_7" class="h16"><span class="span_for_h">Make HTTP Call through <code>nk</code> Object</span></h3>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">const</span> <span class="nx">createContainer</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">image</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">port</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">cmd</span>: <span class="kt">string</span><span class="p">[],</span> <span class="nx">name</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">options</span>: <span class="kt">InternalCallOptions</span><span class="p">)</span> <span class="p">{</span>
<span class="kr">const</span> <span class="p">{</span> <span class="nx">nk</span><span class="p">,</span> <span class="nx">logger</span><span class="p">,</span> <span class="nx">ctx</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">options</span>
<span class="kr">const</span> <span class="nx">dockerAPIUrl</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">DockerAPI</span><span class="p">.</span><span class="nx">scheme</span><span class="si">}${</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="s1">'DOCKER_HOST'</span><span class="p">]</span><span class="si">}</span><span class="sb">:</span><span class="si">${</span><span class="nx">DockerAPI</span><span class="p">.</span><span class="nx">port</span><span class="si">}</span><span class="sb">`</span>
<span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">dockerAPIUrl</span><span class="si">}</span><span class="sb">/v1.41/containers/create?name=</span><span class="si">${</span><span class="nx">name</span><span class="si">}</span><span class="sb">`</span>
<span class="kr">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'Content-Type'</span><span class="o">:</span> <span class="s1">'application/json'</span> <span class="p">}</span>
<span class="kr">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
<span class="nx">Image</span>: <span class="kt">image</span><span class="p">,</span>
<span class="nx">Tty</span>: <span class="kt">true</span><span class="p">,</span>
<span class="nx">ExposedPorts</span><span class="o">:</span> <span class="p">{</span>
<span class="p">[</span><span class="sb">`</span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">/tcp`</span><span class="p">]</span><span class="o">:</span> <span class="p">{},</span>
<span class="p">[</span><span class="sb">`</span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">/udp`</span><span class="p">]</span><span class="o">:</span> <span class="p">{},</span>
<span class="p">},</span>
<span class="nx">Cmd</span>: <span class="kt">cmd</span><span class="p">,</span>
<span class="nx">HostConfig</span><span class="o">:</span> <span class="p">{</span>
<span class="nx">PortBindings</span><span class="o">:</span> <span class="p">{</span>
<span class="p">[</span><span class="sb">`</span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">/tcp`</span><span class="p">]</span><span class="o">:</span> <span class="p">[{</span>
<span class="s2">"HostIP"</span><span class="o">:</span> <span class="s2">"0.0.0.0"</span><span class="p">,</span>
<span class="s2">"HostPort"</span><span class="o">:</span> <span class="nx">port</span>
<span class="p">}],</span>
<span class="p">[</span><span class="sb">`</span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">/udp`</span><span class="p">]</span><span class="o">:</span> <span class="p">[{</span>
<span class="s2">"HostIP"</span><span class="o">:</span> <span class="s2">"0.0.0.0"</span><span class="p">,</span>
<span class="s2">"HostPort"</span><span class="o">:</span> <span class="nx">port</span>
<span class="p">}],</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">})</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="nx">nk</span><span class="p">.</span><span class="nx">httpRequest</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="s1">'post'</span><span class="p">,</span> <span class="nx">headers</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="mi">201</span><span class="p">)</span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">dockerCreateResult</span><span class="o">:</span> <span class="p">{</span> <span class="nx">Id</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">Warnings</span>: <span class="kt">Array</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="p">}</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'container %q created'</span><span class="p">,</span> <span class="nx">dockerCreateResult</span><span class="p">.</span><span class="nx">Id</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">dockerCreateResult</span><span class="p">.</span><span class="nx">Id</span>
<span class="p">}</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">))</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">'error occurred while creating container, %q'</span><span class="p">,</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="k">throw</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end--><h1 id="toc_8" class="h16"><span class="span_for_h">Wait for the Server Initialization</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Godot server may have time-consuming bootstrap to process like loading level resources. Godot server notifies Nakama when it is ready for players to join. After that can players connect to it.</span>
</p>
<pre class="lang_gdscript"><code># client code in godot server
func _on_server_init_completed():
print("_on_server_init_completed")
if GameData.server_match_id == "":
push_error("server inited but no match id")
return
var result: NakamaAPI.ApiRpc = yield(Network.socket.rpc_async(
"game_server_inited",
JSON.print({ "match_id": GameData.server_match_id })
), "completed")
print("game_server_inited rpc called")
if result.is_exception():
push_error(result.get_exception().message)</code></pre>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">We changes the status of a match info in the storage, then broadcasts messages to the clients inside <code>matchLoop</code>, which is the only place we can emit Match State Message. We are transfering information from RPC function to <code>matchLoop</code> through the storage. Many features have to be implemented this way, including kicking players.</span>
</p>
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">Notice: When you're developing on Nakama's JavaScript runtime, you must understand the limits brought by <a class="md_compiled" href="https://github.com/dop251/goja">Goja</a> and Nakama. Like, there's no event loop. JavaScript runtime is pooled by Nakama, so global variables can't be used.</span>
</p>
</blockquote>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">const</span> <span class="nx">gameServerInited</span>: <span class="kt">nkruntime.RpcFunction</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">ctx</span>: <span class="kt">nkruntime.Context</span><span class="p">,</span> <span class="nx">logger</span>: <span class="kt">nkruntime.Logger</span><span class="p">,</span> <span class="nx">nk</span>: <span class="kt">nkruntime.Nakama</span><span class="p">,</span> <span class="nx">payload</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isPrivateIP</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">clientIp</span><span class="p">))</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Headless server can only be run by Nakama'</span><span class="p">)</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
<span class="kr">const</span> <span class="nx">matchId</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">match_id</span>
<span class="kr">const</span> <span class="nx">objects</span>: <span class="kt">nkruntime.StorageObject</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">nk</span><span class="p">.</span><span class="nx">storageRead</span><span class="p">([</span>
<span class="p">{</span> <span class="nx">collection</span>: <span class="kt">TrodMatchInfoCollectionKey</span><span class="p">,</span> <span class="nx">key</span>: <span class="kt">matchId</span><span class="p">,</span> <span class="nx">userId</span>: <span class="kt">SystemUserId</span> <span class="p">}</span>
<span class="p">])</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">objects</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'matchinfos read results not correct'</span><span class="p">)</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">objects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">value</span> <span class="kr">as</span> <span class="nx">TrodMatchInfo</span>
<span class="nx">value</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="s1">'inited'</span>
<span class="nx">nk</span><span class="p">.</span><span class="nx">storageWrite</span><span class="p">([</span>
<span class="p">{</span> <span class="nx">collection</span>: <span class="kt">TrodMatchInfoCollectionKey</span><span class="p">,</span> <span class="nx">key</span>: <span class="kt">matchId</span><span class="p">,</span> <span class="nx">userId</span>: <span class="kt">SystemUserId</span><span class="p">,</span> <span class="nx">value</span> <span class="p">}</span>
<span class="p">])</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'game server %q is inited'</span><span class="p">,</span> <span class="nx">value</span><span class="p">.</span><span class="nx">containerId</span><span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="k">throw</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">Check status of match info in storage in matchLoop every server frame. Tell players' clients to connect to Godot server if needed.</span>
</p>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code inside matchLoop</span>
<span class="c1">// get match info as value from storage...</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">value</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'inited'</span> <span class="o">&&</span> <span class="nx">value</span><span class="p">.</span><span class="nx">host</span> <span class="o">&&</span> <span class="nx">value</span><span class="p">.</span><span class="nx">port</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">value</span><span class="p">))</span>
<span class="nx">dispatcher</span><span class="p">.</span><span class="nx">broadcastMessage</span><span class="p">(</span><span class="nx">MatchOpCode</span><span class="p">.</span><span class="nx">GameServerInited</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
<span class="nx">host</span>: <span class="kt">value.host</span><span class="p">,</span>
<span class="nx">port</span>: <span class="kt">Number</span><span class="p">(</span><span class="nx">value</span><span class="p">.</span><span class="nx">port</span><span class="p">),</span>
<span class="p">}))</span>
<span class="nx">value</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="s1">'broadcasted'</span>
<span class="nx">nk</span><span class="p">.</span><span class="nx">storageWrite</span><span class="p">([</span>
<span class="p">{</span> <span class="nx">collection</span>: <span class="kt">TrodMatchInfoCollectionKey</span><span class="p">,</span> <span class="nx">userId</span>: <span class="kt">SystemUserId</span><span class="p">,</span> <span class="nx">key</span>: <span class="kt">ctx.matchId</span><span class="p">,</span> <span class="nx">value</span> <span class="p">}</span>
<span class="p">])</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'gameserverinited broadcasted'</span><span class="p">)</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<p class="md_block">
<span class="md_line md_line_start md_line_end">Connect to Godot server after being notified the server is ready.</span>
</p>
<pre class="lang_gdscript"><code># client code
# ...
elif match_state.op_code == GameData.OpCode.GameServerInited:
var result := JSON.parse(match_state.data)
if result.error_string == "":
print(result.result)
var host: String = result.result["host"]
var port: int = result.result["port"]
Network.room.connect_to_game_server(host, port)
else:
GameData.show_toast(result.error_string)
# ...</code></pre>
<!--block_code_end--><h1 id="toc_9" class="h16"><span class="span_for_h">Validate Identity</span></h1>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">When user logins, we store its token into storage using client HTTP API. When player connects to the server, the server calls RPC to Nakama with player's token, Nakama checks the token in the storage and returns the result to the server. Specifically:</span>
</p>
<ol class="md_list md_ol" start="0">
<li class="md_li"><span class="md_li_span">Godot client connects to Godot server
</span></li>
<li class="md_li"><span class="md_li_span">Godot server requests user token through Godot RPC call
</span></li>
<li class="md_li"><span class="md_li_span">Client replies
</span></li>
<li class="md_li"><span class="md_li_span">Godot validate token through Nakama RPC call
</span></li>
</ol>
<pre class="lang_gdscript"><code># client code in godot server
func _verify_peer_id(id: int) -> bool:
print("veryfing ", id)
if not _cached_player_session.has(id):
print("no session cached, verify fails")
return false
var result: NakamaAPI.ApiRpc = yield(Network.socket.rpc_async("validate_token", JSON.print(({
"user_id": _cached_player_session[id]["user_id"],
"token": _cached_player_session[id]["token"],
}))), "completed")
if result.is_exception():
print("token not matched, verify fails")
return false
return true</code></pre>
<!--block_code_end--><ol class="md_list md_ol" start="4">
<li class="md_li"><span class="md_li_span">If succeeds, add character into game world
</span></li>
<li class="md_li"><span class="md_li_span">If fails or timeouts, disconnect with the client peer. Any other RPCs between this client and server are not allowed meantime
</span></li>
</ol>
<p class="md_block md_block_as_opening">
<span class="md_line md_line_start md_line_end">One thing to note, we also need to validate that if the caller is Godot server. We don't allow malicious Godot clients to call RPCs which are only for Godot servers. How to do that? We only allow this RPC be called from LAN by checking <code>ctx.clientIp</code>:</span>
</p>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">const</span> <span class="nx">validateToken</span>: <span class="kt">nkruntime.RpcFunction</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">ctx</span>: <span class="kt">nkruntime.Context</span><span class="p">,</span> <span class="nx">logger</span>: <span class="kt">nkruntime.Logger</span><span class="p">,</span> <span class="nx">nk</span>: <span class="kt">nkruntime.Nakama</span><span class="p">,</span> <span class="nx">payload</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isPrivateIP</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">clientIp</span><span class="p">))</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Headless server can only be run by Nakama'</span><span class="p">)</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
<span class="kr">const</span> <span class="nx">userId</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">user_id</span>
<span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">token</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">userId</span> <span class="o">||</span> <span class="o">!</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'validation fails'</span><span class="p">)</span>
<span class="p">}</span>
<span class="kr">const</span> <span class="nx">objects</span> <span class="o">=</span> <span class="nx">nk</span><span class="p">.</span><span class="nx">storageRead</span><span class="p">([</span>
<span class="p">{</span> <span class="nx">collection</span>: <span class="kt">TrodTokensKey</span><span class="p">,</span> <span class="nx">key</span>: <span class="kt">userId</span><span class="p">,</span> <span class="nx">userId</span> <span class="p">}</span>
<span class="p">])</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">objects</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'validation fails'</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">objects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">value</span><span class="p">[</span><span class="s1">'value'</span><span class="p">]</span> <span class="o">!==</span> <span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'validation fails'</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'validate user %q'</span><span class="p">,</span> <span class="nx">userId</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
<span class="nx">success</span>: <span class="kt">true</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="k">throw</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end--><h1 id="toc_10" class="h16"><span class="span_for_h">Game Over and Collect Game Result</span></h1>
<p class="md_block">
<span class="md_line md_line_start md_line_end">After players join the Nakama, all commnunications should be done between Godot client and Godot server. With or without Nakama, there should be no difference on this part.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">When the game produces a result, Godot server can notify Nakama through a Nakama RPC call, as I've showed you many times. Nakama can save the game record into storage, depending on your usecase.</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">Most importantly, when it is appropriate, remove the container through Docker HTTP API and remove the port from the storage.</span>
</p>
<div class="codehilite code_lang_typescript highlight"><pre><span></span><span class="c1">// server code</span>
<span class="kr">export</span> <span class="kr">const</span> <span class="nx">removeContainer</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">id</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">options</span>: <span class="kt">InternalCallOptions</span><span class="p">)</span> <span class="p">{</span>
<span class="kr">const</span> <span class="p">{</span> <span class="nx">nk</span><span class="p">,</span> <span class="nx">logger</span><span class="p">,</span> <span class="nx">ctx</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">options</span>
<span class="kr">const</span> <span class="nx">dockerAPIUrl</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">DockerAPI</span><span class="p">.</span><span class="nx">scheme</span><span class="si">}${</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="s1">'DOCKER_HOST'</span><span class="p">]</span><span class="si">}</span><span class="sb">:</span><span class="si">${</span><span class="nx">DockerAPI</span><span class="p">.</span><span class="nx">port</span><span class="si">}</span><span class="sb">`</span>
<span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">dockerAPIUrl</span><span class="si">}</span><span class="sb">/v1.41/containers/</span><span class="si">${</span><span class="nx">id</span><span class="si">}</span><span class="sb">?force=true`</span>
<span class="kr">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'Content-Type'</span><span class="o">:</span> <span class="s1">'application/json'</span> <span class="p">}</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="sb">`removeContainer </span><span class="si">${</span><span class="nx">id</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
<span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="nx">nk</span><span class="p">.</span><span class="nx">httpRequest</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="s1">'delete'</span><span class="p">,</span> <span class="nx">headers</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">code</span> <span class="o">!==</span> <span class="mi">204</span> <span class="o">&&</span> <span class="nx">response</span><span class="p">.</span><span class="nx">code</span> <span class="o">!==</span> <span class="mi">404</span><span class="p">)</span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">body</span><span class="o">:</span> <span class="p">{</span> <span class="nx">message</span>: <span class="kt">string</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'container %q removed'</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">'error occurred while removing container, %q'</span><span class="p">,</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="k">throw</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<!--block_code_end-->
<blockquote class="blockquote_lines_1 blockquote_without_image">
<p class="md_block">
<span class="md_line md_line_start md_line_end">Attention: Nakama supports HTTP delete method from 3.15.0 on</span>
</p>
</blockquote>
<h1 id="toc_11" class="h16"><span class="span_for_h">Related</span></h1>
<p class="md_block last_md_block_in_page">
<span class="md_line md_line_dom_embed md_line_start"><a class="md_compiled" href="https://heroiclabs.com">https://heroiclabs.com</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://heroiclabs.com/docs/nakama/concepts/multiplayer/">https://heroiclabs.com/docs/nakama/concepts/multiplayer/</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://heroiclabs.com/docs/nakama/client-libraries/godot/index.html">https://heroiclabs.com/docs/nakama/client-libraries/godot/index.html</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://heroiclabs.com/docs/nakama/server-framework/typescript-runtime/">https://heroiclabs.com/docs/nakama/server-framework/typescript-runtime/</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://forum.heroiclabs.com/t/documentation-for-headless-instance-orchestration-layer/2638">https://forum.heroiclabs.com/t/documentation-for-headless-instance-orchestration-layer/2638</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://forum.heroiclabs.com/t/multiplayer-game-with-physics/270/2">https://forum.heroiclabs.com/t/multiplayer-game-with-physics/270/2</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://github.com/heroiclabs/nakama-docs/issues/161">https://github.com/heroiclabs/nakama-docs/issues/161</a><br /></span>
<span class="md_line md_line_dom_embed"><a class="md_compiled" href="https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html">https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html</a><br /></span>
<span class="md_line md_line_dom_embed md_line_end"><a class="md_compiled" href="https://stackoverflow.com/questions/13969655/how-do-you-check-whether-the-given-ip-is-internal-or-not">https://stackoverflow.com/questions/13969655/how-do-you-check-whether-the-given-ip-is-internal-or-not</a></span>
</p>Lamp Animation Rigged2022-08-30T18:31:00Zart/2022-10-18Jury Schon's Blog<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start">Software: Blender<br /></span>
<span class="md_line">Model: CGFastTrack Starterkit<br /></span>
<span class="md_line md_line_end">Tutorial: CGFastTrack Rigging Fundementals</span>
</p>
<p class="md_compiled md_paragraph_html"><video style="width: 100%; max-width: 600px" controls preload="auto">
<source src="https://juryschon.cn-sh2.ufileos.com/lamp_animation_v0020080-0180.mp4" type="video/mp4"></source>
</video></p>Jet Animation Sequence2022-08-30T18:31:00Zart/2022-08-31Jury Schon's Blog<p class="md_block last_md_block_in_page">
<span class="md_line md_line_start">Software: Blender<br /></span>
<span class="md_line">Model: CGFastTrack Starterkit<br /></span>
<span class="md_line md_line_end">Tutorial: CGFastTrack Animation Fundementals</span>
</p>
<p class="md_compiled md_paragraph_html"><video width="100%" controls preload="auto">
<source src="https://juryschon.cn-sh2.ufileos.com/jet_animation_v0010001-0203.mp4" type="video/mp4"></source>
</video></p>第一次用数位板画画2022-05-16T18:31:00Zart/2022-05-17Jury Schon's Blog<p class="md_block">
<span class="md_line md_line_start md_line_end">最近想学习 3D Modeling,需要熟练使用数位板,于是想到用 Sai2 画一幅素描来练手。我虽然不是艺术相关专业毕业的,但有一定的美术基础,小时候作为兴趣爱好学了许多年画画,算是一个童子功。数位板和 Sai2 都是第一次使用,谈一下上手感受。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">第一次使用数位板有一点不适应,与在纸上画画所见即所得相比,使用数位板眼和手是分离的。但适应并不难,十来分钟即可适应。大概类似于手游 FPS 玩家适应键鼠 FPS 的过程。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">第一次使用 Sai2 立马体会到了便利。能用缩放、移动、旋转来画画真是太爽了。只不过橡皮擦不如在纸上画画好,擦得太干净了。我相信 Sai2 对此一定有解决方案,只是我还不知道。我看很多大佬用 Sai2 画立绘,入木三分,有机会也希望学习一下如何使用 Sai2。</span>
</p>
<p class="md_block">
<span class="md_line md_line_start md_line_end">最后给头发涂黑,为了图方便就选择了大笔号的铅笔,没想到涂起来是那么黑,只好用深黄色来模拟散射,不要看起来那么 Flat,然而脸和头发的画风还是不一样的。</span>
</p>
<p class="md_block last_md_block_in_page">
<center>
<span class="md_line md_line_dom_embed md_line_with_image"><img class="md_compiled " src="https://juryschon.cn-sh2.ufileos.com/lydia.png" alt="" title="" ><br /></span>
</center></p>