{"id":102,"date":"2022-06-08T19:31:12","date_gmt":"2022-06-08T23:31:12","guid":{"rendered":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/?p=102"},"modified":"2022-06-08T19:31:12","modified_gmt":"2022-06-08T23:31:12","slug":"day-17-camera-control-triggers","status":"publish","type":"post","link":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/?p=102","title":{"rendered":"Day 17: Camera Control &#038; Triggers"},"content":{"rendered":"\n<p>Today we continued to develop our platform game by making our camera smoothly follow the player throughout the environment, implementing our ruleset for how the player can move through the environment (forward only, please!) and finally we added an enemy controller and gave it a patrolling &#8220;behavior&#8221; through the use of triggers (a special variation of colliders).<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Part 1: Camera Control<\/h2>\n\n\n\n<p>Now we have created a player object and let him run through our level, but very quickly the player runs out of view.  There are a number of ways to address this.  The simplest way to let the camera follow our player is to make the camera a <em>child<\/em> of the player object.  This way, any movement of the player was automatically reflected in the camera.  The end result is that the player remains perfectly still as the rest of the world moves around it.<\/p>\n\n\n\n<p>This solution is OK, but does not really fit what we are going for.  This should be a side scroller, meaning we move our camera to the side, and so we need a better solution.<\/p>\n\n\n\n<p>Our first step is to move the camera horizontally with the player, but not vertically.  We accomplish this by placing a script on the Camera object that would mirror the X value of the player object, like so:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"false\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public GameObject player;\nvoid Update () {\n    Vector3 playerposition = player.transform.position;\n    Vector3 cameraposition = transform.position;\n    cameraposition.x = playerposition.x\n    transform.position = cameraposition;\n}<\/pre>\n\n\n\n<p class=\"has-luminous-vivid-amber-background-color has-background\"><strong>IMPORTANT NOTE:<\/strong> Although our game is 2D, our transforms are still very much 3D.  This means when working with position, we always need to use Vector3 rather than Vector2 structures.<\/p>\n\n\n\n<p>We set a public game object and assign the Player object to it in the Inspector.\u00a0 Then on each frame, we find the X position and match it.\u00a0 The result was just OK.  The side motion worked but things feel a little jittery.\u00a0 We want to give our player a little room to move away from the center and have the camera catch up, as though it were controlled by some camera person trying to keep up with the action.\u00a0 To facilitate that, we used <strong>Mathf.SmoothDamp( )<\/strong>, a dampening function to create a gentle curve to the updating X values, giving the camera an eased, elastic feel.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"false\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public GameObject player;\nprivate float xVelocity = 0.0f;\n\/\/ Update is called once per frame\nvoid Update () {\n    Vector3 playerposition = player.transform.position;\n    Vector3 cameraposition = transform.position;\n \n    cameraposition.x = Mathf.SmoothDamp (cameraposition.x, playerposition.x, ref xVelocity, 0.5f);\n    transform.position = cameraposition;\n}<\/pre>\n\n\n\n<p>SmoothDamp ( ) is part of the float math libraries, and takes 4 parameters \u2013 the start value, the end value, the current velocity (as a reference), and the time the smoothing should occur.&nbsp; Velocity here is tricky, as SmoothDamp ( ) will modify it each time it runs.&nbsp; In order to let that persist, we pass the velocity variable as a \u201cref\u201d, which is the closest we will get to pointers in this class.&nbsp; Normally when we call a method we say \u201chere is a value\u201d but in this case by declaring \u201cref\u201d&nbsp; we say \u201chere is access to the variable itself\u201d.&nbsp; SmoothDamp ( ) will update velocity each time it runs.<\/p>\n\n\n\n<p>Playing this again, this is getting better.&nbsp; My player runs away and the camera catches up again.&nbsp; Since I\u2019ve decided to use \u201cMario\u201d rules, I want to make sure that the player can only advance, not move backwards.&nbsp; I\u2019ll do that by defining two rules.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p><strong>Rule #1:&nbsp;<\/strong>Any time the player moves right of the midpoint on the screen, the camera will follow.<br><strong>Rule #2:&nbsp;<\/strong>The player can only move as far left as is currently visible on the screen.<\/p><\/blockquote>\n\n\n\n<p>Rule #1 is easy enough to implement.\u00a0 To do this, we set a condition around the SmoothDamp and transform position update that test to see if playerposition.x is greater than cameraposition.x and if so, it will let the camera follow.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"false\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public GameObject player;\n\/\/ ref value for smoothDamp\nprivate float xVelocity = 0.0f;\n\/\/ Update is called once per frame\nvoid Update()\n{\n    \/\/ match the player x position\n    Vector3 playerposition = player.transform.position;\n    Vector3 cameraposition = transform.position;\n    if (playerposition.x > cameraposition.x) { \n        \/\/ cameraposition.x = playerposition.x;\n        cameraposition.x = Mathf.SmoothDamp(cameraposition.x, playerposition.x, ref xVelocity, 0.5f);\n        transform.position = cameraposition;\n    }\n}<\/pre>\n\n\n\n<p>Rule #2 is a little trickier, as we don\u2019t really know where the left edge is.&nbsp; We could do all kinds of math to figure this out, casting rays and such, but we are going to do this the lazy way.<\/p>\n\n\n\n<p><strong>OPTION 1: (the complicated way)&nbsp;<\/strong>&nbsp;<em>In a previous semester, we included a value called \u201c<strong>leftSideOffset<\/strong>\u201d that held the distance in x units that corresponded with the left edge of the screen from the center of the camera.&nbsp; Since the camera is always the same z-distance from the player, this number can be a constant.&nbsp; In our update loop, we then check the offset as a bounding x-value for the player, with the following code:&nbsp;&nbsp;<\/em><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"> &nbsp;&nbsp;&nbsp;Vector3&nbsp;checkposition&nbsp;=&nbsp;transform.position;\n &nbsp;&nbsp;&nbsp;float&nbsp;leftx&nbsp;=&nbsp;gameCamera.transform.position.x&nbsp;-&nbsp;leftSideOffset;\n \n &nbsp;&nbsp;&nbsp;if&nbsp;(checkposition.x&nbsp;&lt;&nbsp;leftx)&nbsp;{\n &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;checkposition.x&nbsp;=&nbsp;leftx;\n &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;transform.position&nbsp;=&nbsp;checkposition;\n &nbsp;&nbsp;&nbsp;}\n<\/pre>\n\n\n\n<p><strong>OPTION 2: (the cheap way)&nbsp;&nbsp;<\/strong>For this class, I have implemented a much more rudimentary system.  I create an object and assign it a <strong>BoxCollider 2D<\/strong>.  I size this to span beyond the vertical length of my camera view, and move it to the very left of the camera.  Finally, I make this object a child of the Main Camera, meaning that it will follow along wherever our camera goes.   This prevents our player from being able to run beyond the edge of the screen.   (Once we place enemy objects, you will want to adjust your Collision Matrix in the <strong>Physics 2D<\/strong> panel in your <strong>Project Settings<\/strong>, just like we did with our 3D collisions in Astral Attackers, so that your non-player objects can pass through unimpeded.)<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"https:\/\/courses.ideate.cmu.edu\/53-353\/f2020\/wp-content\/uploads\/2020\/11\/image-22.png\" alt=\"\" class=\"wp-image-153\"\/><figcaption>That box on the left means there&#8217;s only one way you can go.<\/figcaption><\/figure><\/div>\n\n\n<h3 class=\"wp-block-heading\">Part 6 &#8211; Enemies and Triggers<\/h3>\n\n\n\n<p>Now that we\u2019re moving and jumping, let\u2019s create some enemy objects to interact with.&nbsp; We won\u2019t worry about colliding with them right now, but let\u2019s at least get them in the scene and behaving the way we expect them to behave (patrolling their platforms)!<\/p>\n\n\n\n<p>We created an object similar to our player but with our Enemy sprite.  We added CharacterController2D,  Circle Collider 2D, and Rigidbody 2D component to it, and gave it a simplified version of our script to automate its movement.    We use a simple <strong>boolean<\/strong> in our script called &#8220;FaceLeft&#8221; which will indicate if our enemy is facing left or right, and uses that value to pass that direction into the CC2D move command.<\/p>\n\n\n\n<p>To create behaviors, we will use <strong>Triggers<\/strong> to give our Enemy the illusion of intelligently patrolling its platform.  Triggers are a variation of colliders that don&#8217;t actually collide &#8211; they simply define a volume, and that volume will create a collison-like event when another object&#8217;s collider enters it.<\/p>\n\n\n\n<p>For our purposes, we are going to create a &#8220;turn around&#8221; box &#8211; a cube volume that our enemy will enter, register, and react by reversing direction.   First, create an empty object, add a Box Collider 2D, and check the <strong>Is Trigger<\/strong> box.  Create a tag called &#8220;TurnAround&#8221; and assign it to this box.   Make this object a prefab so that we can place them throughout the level.  Then we add this to the Enemy script:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"false\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">private void OnTriggerEnter2D(Collider2D collision)\n{\n    if (collision.gameObject.tag == \"TurnAround\")\n    {\n        Debug.Log(\"Hit the Collider\");\n        faceLeft = !faceLeft;\n    }\n    \n}<\/pre>\n\n\n\n<p>Once the Enemy object enters the trigger object, it checks for the \u201cTurnAround\u201d tag, and if it finds one, it will invert the \u201cfaceLeft\u201d value of the Enemy causing him to walk in the other direction.&nbsp; This will only fire when the enemy first enters the volume as&nbsp;<strong>OnTriggerEnter2D<\/strong>&nbsp;only occurs at the first point of overlap.&nbsp; You can use&nbsp;<strong>OnTriggerExit2D<\/strong>&nbsp;to register when the overlap has ended, or&nbsp;<strong>OnTriggerStay2D<\/strong>&nbsp;to confirm that an overlap is ongoing.<\/p>\n\n\n\n<p class=\"has-pale-cyan-blue-background-color has-background has-small-font-size\"><strong>NOTE: <\/strong>Trigger&#8217;s use the <strong>OnTrigger<\/strong> commands, as opposed to the <strong>OnCollision<\/strong> commands.  Unity treats these differently, so they will only apply to the corresponding setting for <strong>isTrigger<\/strong>.  Also, because this is 2D physics,  note the <strong>2D<\/strong> designation at the end of each of these.   <strong>OnCollisionEnter<\/strong> and <strong>OnCollisionEnter2D<\/strong> use different physics systems and are not interchangeable, so if your collisions are not registering, check that you are using the proper command.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Part 7: Pixel Perfect Camera<\/h2>\n\n\n\n<p>When working with pixel art, you may notice some tearing or flickering lines in your game.  This is often due to the slight artifacts created by the camera sampling the artwork at an incorrect distance.  Thankfully, Unity includes a package to help correct for this &#8211; the <strong>2D Pixel Perfect Camera<\/strong>.  Rather than run through all of the features here, check out the Unity Manual which does an excellent job breaking down exactly how this works.<\/p>\n\n\n\n<p><a href=\"https:\/\/docs.unity3d.com\/Packages\/com.unity.2d.pixel-perfect@1.0\/manual\/index.html\">Unity Manual: 2D Picture Perfect<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<pre class=\"wp-block-preformatted\">CameraFollow.cs<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System.Collections;\nusing System.Collections.Generic;\nusing UnityEngine;\n\npublic class CameraFollow : MonoBehaviour\n{\n\n    public GameObject player;\n\n    \/\/ smoothing velocity\n    private float xVelocity = 0.0f;\n\n\n    \/\/ Update is called once per frame\n    void Update()\n    {\n        Vector3 playerPosition = player.transform.position; \/\/ get the player position\n        Vector3 cameraPosition = transform.position; \/\/ get our (camera) position\n\n        \/\/ cameraPosition.x = playerPosition.x;\n\n        if (playerPosition.x > cameraPosition.x)\n        {\n            \/\/ smooth the camera transition\n            cameraPosition.x = Mathf.SmoothDamp(cameraPosition.x, playerPosition.x, ref xVelocity, 0.5f);\n        }\n\n        \/\/update our position\n        transform.position = cameraPosition;\n    }\n}\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<pre class=\"wp-block-preformatted\">EnemyScript.cs<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System.Collections;\nusing System.Collections.Generic;\nusing UnityEngine;\n\npublic class EnemyScript : MonoBehaviour\n{\n    public float speed;\n    public bool faceLeft = true;\n\n    private CharacterController2D controller;\n\n    \/\/ Start is called before the first frame update\n    void Start()\n    {\n        controller = GetComponent&lt;CharacterController2D>();\n    }\n\n    void FixedUpdate()\n    {\n        float horizontalMove = speed * Time.fixedDeltaTime;\n        if(faceLeft) { horizontalMove *= -1.0f; }\n        controller.Move(horizontalMove, false, false);\n    }\n\n    private void OnTriggerEnter2D(Collider2D collision)\n    {\n        if (collision.gameObject.tag == \"TurnAround\")\n        {\n            \/\/ hit the collider, turn around\n            Debug.Log(\"Enemy hit a collider\");\n            faceLeft = !faceLeft;\n        }\n    }\n\n}\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p><a class=\"more-link\"  href=\"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/?p=102\"><span class=\"more-text\"><\/span><\/a><\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"greenlet_layout":[]},"categories":[6],"tags":[],"_links":{"self":[{"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=\/wp\/v2\/posts\/102"}],"collection":[{"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=102"}],"version-history":[{"count":1,"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=\/wp\/v2\/posts\/102\/revisions"}],"predecessor-version":[{"id":103,"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=\/wp\/v2\/posts\/102\/revisions\/103"}],"wp:attachment":[{"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=102"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=102"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/courses.ideate.cmu.edu\/53-353\/m2022\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=102"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}