喵的Unity游戏开发之路 - 物理:表面接触,保持联系
贴在地面,而不要从斜坡上飞走。
执行射线检测。
配置多层及其交互。
导航楼梯。
利用陡峭的接触(steep contacts)。
这是有关控制角色的运动的系列教程的第三部分。这是关于改善球体与曲面的相互作用。
本教程使用Unity 2019.2.14f1创建。它还使用ProBuilder软件包。
效果之一
贴在地面
当我们的球体到达坡道的顶部时,由于其向上的动量,它开始飞行。这是现实的,但可能并不理想。
当球体突然突然出现小的高差时,也会发生类似的情况。我制作了一个测试场景,以0.1增量的步长演示了这一步。
如果步距不太高,则以足够的速度接近时,球体会反弹。在测试场景中,这对于平坦车道甚至很少发生,因为我是通过将步高降低到零而不合并顶点来实现的。这会产生所谓的幻影碰撞。应该设计场景几何图形以避免这种情况,但我坚持指出来。
在现实生活中,有多种技术可以将某些东西固定在地面上。例如,一级方程式赛车旨在将气流转换为下压力。因此,为我们的领域做类似的事情有现实的基础。
碰撞时间
让我们考虑一下球体将从坡道发射的瞬间。为了使它粘在表面上,我们必须对其速度进行调整,使其与表面重新对齐。让我们检查一下何时可以收到所需信息。通过在基于 OnGround
的 Update
中调整其颜色,将球体不在地面上时将其变为白色,这与上一教程末尾展示的颜色类似
void Update () {
…
GetComponent<Renderer>().material.SetColor(
"_Color", OnGround ? Color.black : Color.white
);
}
要观察准确的时间,请暂时减少物理时间步长和时间范围。
球体发射的物理步骤仍然存在碰撞。我们会在下一步中根据这些数据采取行动,因此我们认为我们已经扎根,而不再需要。这是我们不再获取碰撞数据之后的步骤。因此,我们总是会为时已晚,但是只要我们意识到这一点就不会有问题。
自上次接地以来的步骤
让我们跟踪一下自从我们接地以来已经采取了多少物理步骤。为其添加一个整数字段,并在 UpdateState
的开头将其递增。然后,如果事实证明我们在地面上,则将其设置回零。我们将使用它来确定何时应该抓紧地面。它对于调试也很有用。
int stepsSinceLastGrounded;
…
void UpdateState () {
stepsSinceLastGrounded += 1;
velocity = body.velocity;
if (OnGround) {
stepsSinceLastGrounded = 0;
jumpPhase = 0;
if (groundContactCount > 1) {
contactNormal.Normalize();
}
}
else {
contactNormal = Vector3.up;
}
}
我们不必为此担心。整数不溢出将需要花费几个月的实时时间。
抓拍
添加 SnapToGround
方法,该方法可在需要时将我们固定在地面上。如果成功,那么我们将被扎根。通过返回一个布尔值(最初只是返回 false
)来指示是否发生了这种情况。
bool SnapToGround () {
return false;
}
这样,我们可以方便地将其与使用布尔OR的 UpdateState
中的 OnGround
结合起来。之所以可行,是因为只有在 OnGround
为 false
时,才会调用 SnapToGround
。
void UpdateState () {
stepsSinceLastGrounded += 1;
velocity = body.velocity;
if (OnGround|| SnapToGround()) {
…
}
…
}
SnapToGround
仅在我们不接地时被调用,因此自上次接地以来的步骤数大于零。但是,我们仅应在失去联系后立即尝试捕捉一次。因此,当步数大于1时,我们应该中止。
bool SnapToGround () {
if (stepsSinceLastGrounded > 1) {
return false;
}
return false;
}
射线检测
我们只想在球体下面要坚持的地面时捕捉。我们可以通过以 body.position
和向下向量作为参数调用 Physics.Raycast
从球体的位置垂直向下投射射线来进行检查。物理引擎将执行此射线广播,并返回是否击中某些东西。如果没有,那就没有根据,我们就会中止。
if (stepsSinceLastGrounded > 1) {
return false;
}
if (!Physics.Raycast(body.position, Vector3.down)) {
return false;
}
return false;
如果射线确实击中了某物,那么我们必须检查它是否算作地面。可以通过第三个 RaycastHit
结构输出参数来检索有关命中信息的信息。
if (!Physics.Raycast(body.position, Vector3.down,out RaycastHit hit)) {
return false;
}
RaycastHit 是一个结构,因此是一个值类型。我们可以通过 RaycastHit hit 定义一个变量,然后将其作为第三个参数传递给 Physics.Raycast 。但这是一个输出参数,这意味着它像对象引用一样通过引用传递。必须通过向其中添加 out 修饰符来明确指出这一点。该方法负责为其分配值。
除此之外,还可以在参数列表内声明用于输出参数的变量,而不必在单独的行上声明。那就是我们在这里所做的。
命中数据包括法线向量,我们可以使用该向量来检查我们命中的表面是否算作地面。如果没有,请中止。请注意,在这种情况下,我们处理的是真实的表面法线,而不是碰撞法线。
if (!Physics.Raycast(body.position, Vector3.down, out RaycastHit hit)) {
return false;
}
if (hit.normal.y < minGroundDotProduct) {
return false;
}
重新与地面对齐
如果我们此时还没有中止,那么我们只是失去了与地面的接触,但仍然在地面之上,因此我们要抓住它。将地面接触计数设置为1,使用找到的法线作为接触法线,然后返回 true
。
if (hit.normal.y < minGroundDotProduct) {
return false;
}
groundContactCount = 1;
contactNormal = hit.normal;
return true;
现在我们认为自己已经扎根,尽管我们仍处于空中。下一步是调整速度,使其与地面对齐。就像对齐所需速度一样,它的工作原理是必须保持当前速度,并且我们将显式计算该速度,而不是依靠 ProjectOnContactPlane
。
groundContactCount = 1;
contactNormal = hit.normal;
float speed = velocity.magnitude;
float dot = Vector3.Dot(velocity, hit.normal);
velocity = (velocity - hit.normal * dot).normalized * speed;
return true;
在这一点上,我们仍然漂浮在地面上,但是重力将有助于将我们拉到地面。实际上,速度可能已经有些下降,在这种情况下,重新调整速度会减慢向地面的收敛速度。因此,仅当其点乘积与表面法线为正时,才应调整速度。
if (dot > 0f) {
velocity = (velocity - hit.normal * dot).normalized * speed;
}
这足以使我们的球体在越过顶部时保持在坡道上。它们会漂浮一点,但是在实践中几乎看不到。即使球体会在一帧中变成白色,但在 FixedUpdate
中,我们将球体始终视为接地。只是在我们处于中间状态时调用 Update
。
它还可以防止球在跳下台阶时启动球体。
请注意,我们仅考虑位于我们下方的单个点来确定我们是否位于地面之上。只要关卡的几何图形不太嘈杂或不太详细,此方法就可以正常工作。例如,如果射线正好投射到其中,那么微小的深裂纹可能会导致破裂失败。
最大捕捉速度
无论如何,高速都是我们的球体被发射的原因,所以让我们添加一个可配置的最大捕捉速度。默认情况下,将其设置为最大速度,以便在可能的情况下始终进行捕捉。
[SerializeField, Range(0f, 100f)]
float maxSnapSpeed = 100f;
然后,当当前速度超过最大捕捉速度时,也终止 SnapToGround
。我们可以在射线检测之前通过更早地计算速度来做到这一点。
bool SnapToGround () {
if (stepsSinceLastGrounded > 1) {
return false;
}
float speed = velocity.magnitude;
if (speed > maxSnapSpeed) {
return false;
}
if (!Physics.Raycast(body.position, Vector3.down, out RaycastHit hit)) {
return false;
}
if (hit.normal.y < minGroundDotProduct) {
return false;
}
groundContactCount = 1;
contactNormal = hit.normal;
//float speed = velocity.magnitude;
float dot = Vector3.Dot(velocity, hit.normal);
if (dot > 0f) {
velocity = (velocity - hit.normal * dot).normalized * speed;
}
return true;
}
请注意,由于精度限制,将两个最大速度设置为相同的值可能会产生不一致的结果。最好使最大捕捉速度高于或低于最大速度。
探头距离
当球体下方有地面时,无论距离多远,我们都在捕捉。最好只检查附近的地面。我们通过限制探头的范围来做到这一点。没有最佳的最大距离,但是如果过低的捕捉可能会在陡峭的角度或较高的速度下失败,而过高的捕捉则会导致不合理的捕捉到远低于地面的捕捉。使其可配置,最小值为零,默认值为1。由于我们的球体的半径为0.5,这意味着我们要在球体底部以下最多检查半个单位。
[ ]
float probeDistance = 1f;
将距离作为第四个参数添加到 Physics.Raycast
。
if (!Physics.Raycast(
body.position, Vector3.down, out RaycastHit hit, probeDistance
)) {
return false;
}
忽略代理(Agents)
在检查地面是否贴合时,我们仅考虑可以表示地面的几何图形是有意义的。默认情况下,raycast会检查除放置在忽略Raycast 层上的对象外的所有内容。不应数的内容可以变化,但我们正在移动的领域很可能不会。我们不会偶然碰到要投射的球体,因为我们是从其位置向外投射,但是我们可能会碰到另一个移动的球体。为了避免这种情况,我们可以将其 Layer 设置为忽略Raycast ,但让我们为所有活动的,应忽略的内容创建一个新层为此目的。
通过游戏对象的 Layer 下拉菜单中的添加图层... 选项进入图层设置。项目设置的标签和图层(Tags and Layers)部分。然后定义一个新的自定义用户层。对于不属于关卡几何体的通用活动实体,我们将其命名为 Agent 。
将所有球体移动到该层。更改预制层即可。
接下来,向 MovingSphere
添加一个可配置的 LayerMask
探针掩码,该掩码最初设置为-1,与所有图层匹配。
[ ]
LayerMask probeMask = -1;
然后,我们可以配置球体,以便探测除忽略Raycast 和 Agent 之外的所有层。
要应用遮罩,请将其作为第五个参数添加到 Physics.Raycast
。
if (!Physics.Raycast(
body.position, Vector3.down, out RaycastHit hit,
probeDistance, probeMask
)) {
return false;
}
跳跃和抓拍
抓拍现在可以工作并且可以配置,但是当我们跳跃时它也会激活,从而抵消了向上的动力。为了使跳跃再次起作用,我们必须避免在跳跃后立即弹跳。我们可以通过计算自上次跳跃以来的物理步数来跟踪此情况,就像我们计算自上次停飞以来的步数一样。在 UpdateState
的开头将其递增,并在激活跳转时将其设置回零。
int stepsSinceLastGrounded, stepsSinceLastJump;
…
void UpdateState () {
stepsSinceLastGrounded += 1;
stepsSinceLastJump += 1;
…
}
…
void Jump () {
if (OnGround || jumpPhase < maxAirJumps) {
stepsSinceLastJump = 0;
jumpPhase += 1;
…
}
}
现在,即使跳转后过早,我们也可以中止 SnapToGround
。由于碰撞数据的延迟,我们仍然认为启动跳跃后的步骤已接地。因此,如果我们在跳跃后走了两个或更少的步骤,就必须中止。
if (stepsSinceLastGrounded > 1|| stepsSinceLastJump <= 2) {
return false;
}
楼梯
接下来让我们考虑一种更困难的表面:楼梯。实际上,球体根本无法很好地爬上楼梯,但是无论如何,我们可能希望它们这样做,也许是因为它们代表了应该能够在楼梯上导航的东西。我制作了一个测试场景,其中包含五个45°阶梯,步长分别为0.1、0.2、0.3、0.4和0.5。
使用默认设置时,球体根本无法处理楼梯。在最大加速度下,大多数设法上升,但是结果不可靠且有弹性,根本没有平稳的运动。试图以一定角度移动而不是直上楼梯几乎是不可能的。
简化碰撞体(Collider)
较大的楼梯台阶使移动无法进行。而且,虽然可以以较小的台阶弹起楼梯,但是碰撞变得任意,运动变得不稳定而不是平稳。
与其尝试与物理引擎战斗,不如我们更加务实并使它的工作更轻松。我们要在楼梯上进行平稳,一致,可控制的运动。当我们使用平坦的斜坡代替时,我们可以得到。因此,让我们用坡道代替楼梯的碰撞体。
坡道是楼梯的近似值。最好的折衷方案是设计碰撞体坡道,使其切穿台阶中间。然后,碰撞将发生在可见几何体的上方和下方。
我创建了这样的形状以匹配五个阶梯,首先是常规的 ProBuilder 对象。然后,通过 ProBuilder 窗口中的设置碰撞体选项将它们转换为碰撞体。
禁用楼梯的网格碰撞体组件,但此时不要删除它们。然后暂时将最大地面角度增加到46°,以使球体可以向上移动45°楼梯。
尽管需要一些额外的关卡设计工作,但使用简化的碰撞体上楼梯是使它们在物理上可导航的最佳方法。通常,使碰撞形状尽可能简单是一个好主意,出于性能和运动稳定性的原因,避免不必要的细节。因此,我们将坚持使用这种方法。但这是一个近似值,因此在仔细检查时,您会发现球体切穿并悬停在楼梯台阶上方。但是,从远处和运动中通常并不太明显。
将来,我们将在关注重力的情况下进行处理。
Detailed 和 Stairs 层(Layer)
我们使用简化的碰撞体进行球体与楼梯之间的交互并不意味着我们不能将原始的楼梯碰撞体用于其他碰撞。例如,我们可能希望小碎片正确地降落在各个楼梯台阶上,而不是从坡道上滑下来。让我们通过增加两层来实现这一点:一层用于详细信息,一层用于楼梯对象。
探针遮罩应包括楼梯层,但不包括 Detailed 层。
接下来,转到项目设置的物理部分,然后调整图层碰撞矩阵。楼梯仅应与代理商交互,而代理不应与 Detailed 交互。
现在,再次启用楼梯网格碰撞器组件。然后添加一些小的刚体对象,使其落在它们之上,以同时查看两种相互作用。如果您给这些物体提供足够低的质量(例如0.05),那么球体将能够将它们推到一边。
楼梯的最大角度
如果我们能够爬楼梯,那么我们使用的楼梯最大角度与正常地面的最大角度是有道理的。因此,为它们添加一个单独的最大角度,默认情况下设置为50°。
[ ]
float maxGroundAngle = 25f, maxStairsAngle = 50f;
…
float minGroundDotProduct, minStairsDotProduct;
…
void OnValidate () {
minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
}
![]()
最大地面和楼梯角度。
我们现在必须与哪种最小点产品进行比较取决于我们所使用的表面类型。我们将为此添加一个可配置的楼梯遮罩选项,类似于探针遮罩。
[ ]
LayerMask probeMask = -1, stairsMask = -1;
这是可能的,但是通过使用遮罩,我们不再依赖于硬编码的图层名称,并且更加灵活,这也使实验更加容易。
创建一个新的 GetMinDot
方法,该方法返回给定图层的适当最小值,该整数是整数。假设我们可以直接比较楼梯的蒙版和图层,如果它们不相等,则返回最小地面点积,否则返回最小楼梯点积。
float GetMinDot (int layer) {
return stairsMask != layer ?
minGroundDotProduct : minStairsDotProduct;
}
但是,该掩码是位掩码,每层只有一位。具体来说,如果楼梯是第11层,则它与第11位匹配。我们可以通过使用 1 << layer
来设置单个位的值,该值将左移运算符应用于数字1的次数等于层索引(十)。结果将是二进制数10000000000。
return stairsMask !=(1 << layer)?
如果蒙版仅选择了一个图层,这将起作用,但是让我们为任何图层组合支持一个蒙版。我们通过采用掩码和图层位的布尔AND来实现。如果结果为零,则该层不属于蒙版。
return(stairsMask & (1 << layer)) == 0?
在 EvaluateCollision
的开头检索正确的最小点值,并使用它来检查接触是否算作地面。
void EvaluateCollision (Collision collision) {
float minDot = GetMinDot(collision.gameObject.layer);
for (int i = 0; i < collision.contactCount; i++) {
Vector3 normal = collision.GetContact(i).normal;
if (normal.y >=minDot) {
groundContactCount += 1;
contactNormal += normal;
}
}
}
检查我们是否在地面上时,还可以在 SnapToGround
中使用 GetMinDot
。
if (hit.normal.y <GetMinDot(hit.collider.gameObject.layer)) {
return false;
}
陡峭的接触
除了接地触点,还有其他触点。移动需要接地,但有时我们仅与墙壁接触。否则我们可能陷入困境。如果我们有空气加速功能,在这种情况下我们仍然可以控制,但是通过一些额外的工作,我们可以做更多的事情。
检测陡峭的接触
陡峭的触点太陡而不能算作地面,但不是天花板或悬垂。因此,一切都达到了完美的垂直墙。就像在常规地面触点上一样,让我们在字段和属性中跟踪此类触点的正常和计数。
Vector3 contactNormal, steepNormal;
int groundContactCount, steepContactCount;
bool OnGround => groundContactCount > 0;
bool OnSteep => steepContactCount > 0;
还要在 ClearState
中重置新数据。
void ClearState () {
groundContactCount =steepContactCount =0;
contactNormal =steepNormal =Vector3.zero;
}
在 EvaluateCollision
中,如果我们没有地面接触,请检查它是否为陡峭接触。完美垂直的墙的点积应为零,但让我们稍微宽一点些,接受高于-0.01的所有值。
if (normal.y >= minDot) {
groundContactCount += 1;
contactNormal += normal;
}
else if (normal.y > -0.01f) {
steepContactCount += 1;
steepNormal += normal;
}
裂缝
缝隙是有问题的,因为一旦卡在缝隙中而没有跳气,除非空气加速度很大,否则就不可能逃脱。我创建了一个带有小裂缝的测试场景来演示这一点。
这个想法是,如果我们最终接地,则不需要陡峭的接触。但是当连抢断都无法发现地面时,我们的下一个最佳选择就是检查裂缝或类似情况。如果我们发现自己被楔入一个狭窄的空间中,并且有多个陡峭的接触点,那么我们可能可以通过推动这些接触点来移动。
创建一个 CheckSteepContacts
,返回是否成功将陡峭的接触点转换为虚拟地面。如果有多个陡峭的接触点,则将它们归一化,并检查结果是否算作接地。如果是这样,则返回成功,否则返回失败。在这种情况下,我们不必检查楼梯。
bool CheckSteepContacts () {
if (steepContactCount > 1) {
steepNormal.Normalize();
if (steepNormal.y >= minGroundDotProduct) {
groundContactCount = 1;
contactNormal = steepNormal;
return true;
}
}
return false;
}
在 UpdateState
中添加 CheckSteepContacts
作为对接地状态的第三次检查。
if (OnGround || SnapToGround()|| CheckSteepContacts()) {
stepsSinceLastGrounded = 0;
jumpPhase = 0;
if (groundContactCount > 1) {
contactNormal.Normalize();
}
}
现在,我们可以在裂隙中以及之前卡住的类似地方移动并跳跃。
跳墙
让我们还回顾一下跳墙。之前,我们仅将跳跃限制为仅在地面或空中跳跃时进行。但是,如果我们将跳跃方向设为陡峭法线而不是接触法线,那么我们也可以支持从墙跳。
开始制作跳转方向变量,并删除 Jump
中的当前有效性检查。
void Jump () {
Vector3 jumpDirection;
(OnGround || jumpPhase < maxAirJumps) {
stepsSinceLastJump = 0;
jumpPhase += 1;
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);
float alignedSpeed = Vector3.Dot(velocity,jumpDirection);
if (alignedSpeed > 0f) {
jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
}
velocity +=jumpDirection* jumpSpeed;
//}
}
相反,请检查我们是否在地面上。如果是这样,请使用接触法线作为跳转方向。如果不是,那么下一个检查是我们是否处在陡峭的状态。如果是这样,请改用陡峭的法线。之后,检查空气跳动,为此我们再次使用接触法线,该法线已设置为向上向量。如果所有这些都不适用,则不可能进行跳转,并且我们中止。
Vector3 jumpDirection;
if (OnGround) {
jumpDirection = contactNormal;
}
else if (OnSteep) {
jumpDirection = steepNormal;
}
else if (jumpPhase < maxAirJumps) {
jumpDirection = contactNormal;
}
else {
return;
}
空中跳跃
此时,我们应该重新考虑空中跳跃。检查跳跃阶段是否小于最大空中跳跃的唯一作用是,在跳跃之后,该阶段会立即重置为零,因为在下一步中,我们仍将其视为接地。因此,我们仅应在启动跳转后多一步才能在 UpdateState
中重置跳转阶段,以免错误着陆。
stepsSinceLastGrounded = 0;
if (stepsSinceLastJump > 1) {
jumpPhase = 0;
}
为了保持空中跳跃的正常进行,我们现在必须检查跳跃阶段是否小于或等于 Jump
中的最大值。
else if (jumpPhase<=maxAirJumps) {
jumpDirection = contactNormal;
}
但是,这样可以使空气从表面掉下来后再跳一次而不跳动。为了防止这种情况,我们在跳气时会跳过第一跳阶段。
else if (jumpPhase <= maxAirJumps) {
if (jumpPhase == 0) {
jumpPhase = 1;
}
jumpDirection = contactNormal;
}
但这仅在完全允许空气跳跃的情况下才有效,因此首先检查一下。
else if (maxAirJumps > 0 &&jumpPhase <= maxAirJumps) {
if (jumpPhase == 0) {
jumpPhase = 1;
}
jumpDirection = contactNormal;
}
最后,让壁跳重设跳跃阶段,以便有可能将壁跳变成新的空中跳跃序列。
else if (OnSteep) {
jumpDirection = steepNormal;
jumpPhase = 0;
}
向上跳跃偏见
从垂直墙上跳下来不会增加垂直速度。因此,尽管有可能在附近的相对壁之间反弹,但重力始终会将球体拉下。我用两个方块制作了一个测试场景来演示这一点。
但是,有些游戏将跳墙作为达到极高的一种手段。我们可以通过在跳转方向上增加一个向上的偏差来支持这一点。最简单的方法是将上矢量添加到跳转方向并将结果标准化。最终方向是两个方向的平均值,因此从平坦地面的跳跃不会受到影响,而从完全垂直的墙壁上跳跃的影响最大,成为45°跳跃。
jumpDirection = (jumpDirection + Vector3.up).normalized;
float alignedSpeed = Vector3.Dot(velocity, jumpDirection);
这会影响所有不在完美平坦的地面或空中的跳跃轨迹,这在上坡时跳跃时最明显。
最后,从 Update
删除调试球颜色。
//GetComponent<Renderer>().material.SetColor(
// "_Color", OnGround ? Color.black : Color.white
//);
下一个教程是轨道摄像机。
https://bitbucket.org/catlikecodingunitytutorials/movement-03-surface-contact/
往期精选
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/surface-contact/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes
本文分享自微信公众号 - Unity3D游戏开发精华教程干货(u3dnotes)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
2020 年微服务项目活跃度报告
导读:2020 年 8 月 18 日,首届云原生微服务大会于线上召开,会议首日,阿里云资深技术专家、CNCF TOC 李响 Keynote 演讲中正式发布了《 2020 年微服务领域开源数字化报告》。 微服务体系就像是一剂催化剂,可以加速数据和业务结合的过程,更好地提升生产力,从而实现业务的提升。本项目旨在通过建立一份建立在微服务领域的相对完整、可以反复进行推演的数据报告(报告、数据、算法均开源),分析微服务框架项目以及 Spring Cloud 项目的 GitHub 开发者行为日志,通过多维度数据分析的视角,来观察微服务领域的开源现状、进展趋势、演化特征等问题。 本报告根据 2020 年 1 月到 6 月的 GitHub 日志进行统计。值得一提的是,报告显示 Apache Dubbo 作为中国本土开源的项目,在微服务框架中排名第 5,全球排名跻身 693;Spring 社区第一个国产 Spring Cloud 项目 Spring Cloud Alibaba 作为开源的微服务全家桶,在 Spring Cloud 榜单中居于榜首。 关键词:微服务、开源、行为数据、GitHub 背景 随着...
- 下一篇
木兰编程语言重现:引用本地木兰模块;模拟凑十法加法
之前一直没重现本地包内的木兰模块引用,导致提取出的模块只能放在项目根目录下。 上周终于搞定。于是将上次的摆放规划应用的源码拆分成几个模块,比如测试/实用/规划/点.ul,在主模块中可以如此引用using 点 in 测试.实用.规划.点。不需要在包中放置类似__init__.py。 其他新添功能 字符串拼接时的报错信息 (..•˘_˘•..) 字符串只能拼接字符串,请将"int"先用 str() 转换 见第1行:print("1" + 2) 对匿名函数指定返回类型 type 形状 { {} } print(func (边长) : 形状 { return 形状() }(1) != nil) 查询字典中是否包含某个键 之前是用__contains__,一直觉得不爽,现在用get代替,比如:字集.get(字) != nil 新实例 最近看到幼儿数学启蒙时的一种“凑十法”,比如求 9+7,先把 7 拆为 1 和 6,而 9+1 = 10,最终得出 16。感觉小孩似乎不需要真正用“加法”运算。模拟解题过程如下: 拆分 = { 2 : [[1, 1]], 3 : [[1...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS6,CentOS7官方镜像安装Oracle11G