{"author_link":"\/users\/fre","author_name":"fre","author_uid":"fre","comments":[],"epoch":1544661304,"event":"LD43","format":"md","ldjam_node_id":137295,"likes":23,"metadata":{"p_key":"127088","p_author":"fre","p_authorkey":"1132742","p_urlkey":"343208","p_title":"How we made Welcome to the North Pole (Part 1 - Platformer mechanics)","p_cat":"LDJam ","p_event":"LD43","p_time":"1544661304","p_likes":"23","p_comments":"0","p_status":"WAYBACK","us_key":"1132742","us_name":"fre","us_username":"fre","event_start":"1543536000","event_key":"71","event_name":"LD 43"},"node":{"_collation":{"body_sanitizer":"TextUtils::SanitizeHTML via existing importer","event":"LD43","removed_author":false},"_superparent":120415,"_trust":3,"author":132742,"body":"In this post I'll be describing the implementation of some of the platformer elements in [Welcome to the North Pole](https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole).\n\nPerhaps this can serve as inspiration for others or as a way to start exchanging tips. We made the game using Unity but these concepts apply elsewhere.\n\nSee also:\n* [Part 2 - Building snowmen](https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-2-building-snowmen)\n\n## Detectors\n![WTTNP_Detectors.gif](\/\/\/raw\/686\/02\/z\/1fa0d.gif)\n\nEach snowball has 4 detectors that report contact with the ground or other characters. We use them to detect whether a snowball can jump and what shape a snowman has.\n\nDetectors are configured as trigger and only react to contact with other characters or the environment:\n\n![WTTNP_GroundDetectorStats.PNG](\/\/\/raw\/686\/02\/z\/1fa0e.png)\n\n![WTTNP_Layers.PNG](\/\/\/raw\/686\/02\/z\/1fa26.png)\n*Our layer setup, note that it avoids inter-collisions between static layers or between triggers*\n\nThis is the simplified code for the collision detection:\n\n```csharp\n  private void OnTriggerStay2D(Collider2D other) {\n    \/\/ Layers is defined by hand.\n    if (other.gameObject.layer == (int) Layers.GROUND) {\n      GroundContact = true;\n      lastGroundContactTime = Time.time;\n    }\n  }\n```\n\nContacts are reset in `FixedUpdate()`, which executes just before the physics loop (where `OnTriggerStay2D` is called). We moved `CharacterGroundDetector` last in the script execution order to make sure that the correct value can be read from other scripts' `Update()` or `FixedUpdate()` loops.\n\n```csharp\n  void FixedUpdate() {\n    \/\/ Do not clear contact if the rigidbody is sleeping,\n    \/\/ otherwise it won't be re-registered until the rigidbody wakes up.\n    if (controller.rb.IsSleeping()) return;\n\n    \/\/ Allow some tolerance after the last contact before clearing the flag.\n    \/\/ This allows e.g. jumping slightly after leaving a platform.\n    if (Time.time > lastGroundContactTime + ContactTimeTolerance) {\n      GroundContact = false;\n    }\n  }\n```\n\nDetectors are children of an \"Upright\" `GameObject` that preserves its orientation even though the snowball rotates:\n\n```csharp\n  void FixedUpdate() {\n    transform.rotation = Quaternion.identity;\n  }\n```\n\n\n## Visual vs physical size\n\n![WTTNP_Size.PNG](\/\/\/raw\/686\/02\/z\/1fa13.png)\n*Two size 1 snowballs: before accumulating snow (left) and about to grow (right)*\n\nMain ideas:\n* The physical size of a snowball is fixed for each stage (small\/medium\/large).\n* The visual sprite grows slowly as you accumulate snow.\n\nThis let us balance the game for a fixed collider size while giving a visual feedback that a snowball changes size (coherent with the world).\n\nWe kept the ball diameter below the size of one tile to make sure a snowball can easily fit through a gap of the corresponding size (collider diameters are 0.8, 1.8 and 2.6, a tile is 1x1).\n\nBecause snowballs are made of snow, the default visual size is already slightly bigger than the collider because a slight overlap looks more natural than having snowballs touch the ground right along their edge.\n\n## Momentum\n\n![WTTNP_Momentum.gif](\/\/\/raw\/686\/02\/z\/1fa27.gif)\n\nWe took the movement principles from this [Sonic physics guide](http:\/\/info.sonicretro.org\/SPG:Running). Another helpful resource was [this blog post by Yoann Pignole](https:\/\/www.gamasutra.com\/blogs\/YoannPignole\/20140103\/207987\/Platformer_controls_how_to_avoid_limpness_and_rigidity_feelings.php).\n\nMain ideas:\n* Each snowball has a maximum speed and an acceleration factor (the bigger the snowball, the slower it is).\n* While a direction is held, we increase the snowball's velocity in that direction up to the maximum speed.\n* Velocity is not reduced if it's above the maximum speed. This lets you gather a lot of momentum on the ground and keep it while jumping, or as a small snowball and preserve it as you grow bigger (adds a nice flow and is actually helpful for one of the challenges).\n* The ball does have some amount of friction that we let the physics engine resolve. This is helpful to make the snowball rotate naturally as it rolls along the ground.\n\nTranslated into code, it looks something like this:\n\n```csharp\n    private void MoveSideways() {\n        var currentSpeed = Mathf.Abs(rb.velocity.x);\n        var currentDirection = Mathf.Sign(rb.velocity.x);\n        \/\/ Air speed is lower than ground speed to allow less control in the air than on ground,\n        \/\/ but momentum is preserved if you hold the direction.\n        var maxSpeed = Grounded ? stats.GroundSpeed : stats.AirSpeed;\n        var targetSpeed = currentSpeed;\n        if (currentDirection == Direction) {\n            \/\/ Accelerate up to maxSpeed, but do not decelerate while holding the key.\n            targetSpeed += Mathf.Min(stats.Acceleration * Time.deltaTime,\n                Mathf.Max(maxSpeed - targetSpeed, 0));\n        } else if (Direction == 0) {\n            \/\/ Decelerate up to 0 if no key is held.\n            targetSpeed -= stats.IdleDeceleration * Time.deltaTime;\n            targetSpeed = Mathf.Max(targetSpeed, 0);\n        } else {\n            \/\/ Decelerate faster until we flip direction if the opposite key is held.\n            targetSpeed -= stats.OppositeDeceleration * Time.deltaTime;\n        }\n        \/\/ Apply the velocity change through a force on the rigidbody.\n        rb.AddForce(Vector2.right * currentDirection * (targetSpeed - currentSpeed) * rb.mass,\n            ForceMode2D.Impulse);\n    }\n```\n\n\n## Long jump\n\nHolding the jump key makes you jump higher:\n\n![WTTNP_Jump.gif](\/\/\/raw\/686\/02\/z\/1fa21.gif)\n\nThis is implemented as a combination of forces:\n* `JumpImpulse` is applied right as you press the jump key.\n* `JumpHoldForce` is applied over time, while the jump key is held, up to a maximum duration (0.35s).\n\nIn code, it looks something like this:\n\n```csharp\n    private void HandleJump() {\n        \/\/ Trigger jump\n        if (Grounded && input.Jump && !Jumping) {\n            Jumping = true;\n            jumpEndTime = Time.time + stats.MaxJumpDuration;\n            \/\/ Cancel vertical velocity and jump up.\n            \/\/ Vertical velocity must be canceled because the snowball may still be falling\n            \/\/ when the ground detector triggers.\n            rb.AddForce(Vector2.up * (stats.JumpImpulse - rb.velocity.y) * rb.mass,\n                ForceMode2D.Impulse);\n        }\n\n        if (Time.time > jumpEndTime) {\n            Jumping = false;\n        }\n\n        if (Jumping && input.JumpHold) {\n            rb.AddForce(Vector2.up * stats.JumpHoldForce * rb.mass, ForceMode2D.Force);\n        } else {\n            \/\/ Stop jump hold force when input is released.\n            Jumping = false;\n        }\n    }\n```\n\n\nWe later added a small tweak: moving against a wall while holding jump gives you a small additional boost (`ClimbHoldForce`) to counteract the added friction and because it feels satisfying:\n\n![WTTNP_Climb.gif](\/\/\/raw\/686\/02\/z\/1fa23.gif)\n\nThis allows you to clear 4-high walls without building a snowman :)\n\n\n## End of Part 1\n\nThanks for reading!\n\nSee also:\n* [Part 2 - Building snowmen](https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-2-building-snowmen)\n\nIn the next parts we plan to describe:\n* Art style and implementation\n* Music and sound effect design\n\nFeel free to post other suggestions in this thread.\n\n@fre and @catie-jo\n","comments":4,"comments-timestamp":"2018-12-13T21:54:05Z","created":"2018-12-12T22:34:35Z","files":[],"files-timestamp":0,"id":137295,"love":23,"love-timestamp":"2018-12-16T15:35:46Z","meta":[],"modified":"2018-12-16T15:35:46Z","name":"How we made Welcome to the North Pole (Part 1 - Platformer mechanics)","node-timestamp":"2018-12-16T01:30:17Z","parent":132745,"parents":[1,5,9,120415,132745],"path":"\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-1-platformer-mechanics","published":"2018-12-13T00:35:04Z","scope":"public","slug":"how-we-made-welcome-to-the-north-pole-part-1-platformer-mechanics","subsubtype":"","subtype":"","type":"post","version":413985},"node_metadata":{"n_key":"137295","n_urlkey":"343208","n_parent":"132745","n_path":"\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-1-platformer-mechanics","n_slug":"how-we-made-welcome-to-the-north","n_type":"post","n_subtype":"","n_subsubtype":"","n_author":"132742","n_created":"1544654075","n_modified":"1544974546","n_version":"413985","n_status":"WAYBACK"},"source_url":"https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-1-platformer-mechanics","text":"In this post I'll be describing the implementation of some of the platformer elements in [Welcome to the North Pole](https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole).\n\nPerhaps this can serve as inspiration for others or as a way to start exchanging tips. We made the game using Unity but these concepts apply elsewhere.\n\nSee also:\n* [Part 2 - Building snowmen](https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-2-building-snowmen)\n\n## Detectors\n![WTTNP_Detectors.gif](\/\/\/raw\/686\/02\/z\/1fa0d.gif)\n\nEach snowball has 4 detectors that report contact with the ground or other characters. We use them to detect whether a snowball can jump and what shape a snowman has.\n\nDetectors are configured as trigger and only react to contact with other characters or the environment:\n\n![WTTNP_GroundDetectorStats.PNG](\/\/\/raw\/686\/02\/z\/1fa0e.png)\n\n![WTTNP_Layers.PNG](\/\/\/raw\/686\/02\/z\/1fa26.png)\n*Our layer setup, note that it avoids inter-collisions between static layers or between triggers*\n\nThis is the simplified code for the collision detection:\n\n```csharp\n  private void OnTriggerStay2D(Collider2D other) {\n    \/\/ Layers is defined by hand.\n    if (other.gameObject.layer == (int) Layers.GROUND) {\n      GroundContact = true;\n      lastGroundContactTime = Time.time;\n    }\n  }\n```\n\nContacts are reset in `FixedUpdate()`, which executes just before the physics loop (where `OnTriggerStay2D` is called). We moved `CharacterGroundDetector` last in the script execution order to make sure that the correct value can be read from other scripts' `Update()` or `FixedUpdate()` loops.\n\n```csharp\n  void FixedUpdate() {\n    \/\/ Do not clear contact if the rigidbody is sleeping,\n    \/\/ otherwise it won't be re-registered until the rigidbody wakes up.\n    if (controller.rb.IsSleeping()) return;\n\n    \/\/ Allow some tolerance after the last contact before clearing the flag.\n    \/\/ This allows e.g. jumping slightly after leaving a platform.\n    if (Time.time > lastGroundContactTime + ContactTimeTolerance) {\n      GroundContact = false;\n    }\n  }\n```\n\nDetectors are children of an \"Upright\" `GameObject` that preserves its orientation even though the snowball rotates:\n\n```csharp\n  void FixedUpdate() {\n    transform.rotation = Quaternion.identity;\n  }\n```\n\n\n## Visual vs physical size\n\n![WTTNP_Size.PNG](\/\/\/raw\/686\/02\/z\/1fa13.png)\n*Two size 1 snowballs: before accumulating snow (left) and about to grow (right)*\n\nMain ideas:\n* The physical size of a snowball is fixed for each stage (small\/medium\/large).\n* The visual sprite grows slowly as you accumulate snow.\n\nThis let us balance the game for a fixed collider size while giving a visual feedback that a snowball changes size (coherent with the world).\n\nWe kept the ball diameter below the size of one tile to make sure a snowball can easily fit through a gap of the corresponding size (collider diameters are 0.8, 1.8 and 2.6, a tile is 1x1).\n\nBecause snowballs are made of snow, the default visual size is already slightly bigger than the collider because a slight overlap looks more natural than having snowballs touch the ground right along their edge.\n\n## Momentum\n\n![WTTNP_Momentum.gif](\/\/\/raw\/686\/02\/z\/1fa27.gif)\n\nWe took the movement principles from this [Sonic physics guide](http:\/\/info.sonicretro.org\/SPG:Running). Another helpful resource was [this blog post by Yoann Pignole](https:\/\/www.gamasutra.com\/blogs\/YoannPignole\/20140103\/207987\/Platformer_controls_how_to_avoid_limpness_and_rigidity_feelings.php).\n\nMain ideas:\n* Each snowball has a maximum speed and an acceleration factor (the bigger the snowball, the slower it is).\n* While a direction is held, we increase the snowball's velocity in that direction up to the maximum speed.\n* Velocity is not reduced if it's above the maximum speed. This lets you gather a lot of momentum on the ground and keep it while jumping, or as a small snowball and preserve it as you grow bigger (adds a nice flow and is actually helpful for one of the challenges).\n* The ball does have some amount of friction that we let the physics engine resolve. This is helpful to make the snowball rotate naturally as it rolls along the ground.\n\nTranslated into code, it looks something like this:\n\n```csharp\n    private void MoveSideways() {\n        var currentSpeed = Mathf.Abs(rb.velocity.x);\n        var currentDirection = Mathf.Sign(rb.velocity.x);\n        \/\/ Air speed is lower than ground speed to allow less control in the air than on ground,\n        \/\/ but momentum is preserved if you hold the direction.\n        var maxSpeed = Grounded ? stats.GroundSpeed : stats.AirSpeed;\n        var targetSpeed = currentSpeed;\n        if (currentDirection == Direction) {\n            \/\/ Accelerate up to maxSpeed, but do not decelerate while holding the key.\n            targetSpeed += Mathf.Min(stats.Acceleration * Time.deltaTime,\n                Mathf.Max(maxSpeed - targetSpeed, 0));\n        } else if (Direction == 0) {\n            \/\/ Decelerate up to 0 if no key is held.\n            targetSpeed -= stats.IdleDeceleration * Time.deltaTime;\n            targetSpeed = Mathf.Max(targetSpeed, 0);\n        } else {\n            \/\/ Decelerate faster until we flip direction if the opposite key is held.\n            targetSpeed -= stats.OppositeDeceleration * Time.deltaTime;\n        }\n        \/\/ Apply the velocity change through a force on the rigidbody.\n        rb.AddForce(Vector2.right * currentDirection * (targetSpeed - currentSpeed) * rb.mass,\n            ForceMode2D.Impulse);\n    }\n```\n\n\n## Long jump\n\nHolding the jump key makes you jump higher:\n\n![WTTNP_Jump.gif](\/\/\/raw\/686\/02\/z\/1fa21.gif)\n\nThis is implemented as a combination of forces:\n* `JumpImpulse` is applied right as you press the jump key.\n* `JumpHoldForce` is applied over time, while the jump key is held, up to a maximum duration (0.35s).\n\nIn code, it looks something like this:\n\n```csharp\n    private void HandleJump() {\n        \/\/ Trigger jump\n        if (Grounded && input.Jump && !Jumping) {\n            Jumping = true;\n            jumpEndTime = Time.time + stats.MaxJumpDuration;\n            \/\/ Cancel vertical velocity and jump up.\n            \/\/ Vertical velocity must be canceled because the snowball may still be falling\n            \/\/ when the ground detector triggers.\n            rb.AddForce(Vector2.up * (stats.JumpImpulse - rb.velocity.y) * rb.mass,\n                ForceMode2D.Impulse);\n        }\n\n        if (Time.time > jumpEndTime) {\n            Jumping = false;\n        }\n\n        if (Jumping && input.JumpHold) {\n            rb.AddForce(Vector2.up * stats.JumpHoldForce * rb.mass, ForceMode2D.Force);\n        } else {\n            \/\/ Stop jump hold force when input is released.\n            Jumping = false;\n        }\n    }\n```\n\n\nWe later added a small tweak: moving against a wall while holding jump gives you a small additional boost (`ClimbHoldForce`) to counteract the added friction and because it feels satisfying:\n\n![WTTNP_Climb.gif](\/\/\/raw\/686\/02\/z\/1fa23.gif)\n\nThis allows you to clear 4-high walls without building a snowman :)\n\n\n## End of Part 1\n\nThanks for reading!\n\nSee also:\n* [Part 2 - Building snowmen](https:\/\/ldjam.com\/events\/ludum-dare\/43\/welcome-to-the-north-pole\/how-we-made-welcome-to-the-north-pole-part-2-building-snowmen)\n\nIn the next parts we plan to describe:\n* Art style and implementation\n* Music and sound effect design\n\nFeel free to post other suggestions in this thread.\n\n@fre and @catie-jo\n","title":"How we made Welcome to the North Pole (Part 1 - Platformer mechanics)","wayback_source":[]}