Search Unity

Recommended Way to Keep 2D Player Character in Camera Boundaries?

Discussion in 'Scripting' started by tfishell, Nov 21, 2017.

  1. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Hi, a few months back I started back up learning Unity and C#. For practice, I'm trying to create a simple point-n-click adventure.

    Using a "While char.position < destination" loop, the character gameobject "MoveTowards" the mouse click fine (other than screen tearing unrelated to this), and right now his top won't go past the top of the camera.

    However, I wonder if there's a better way, since right now there's a slight "bump" because for a frame he goes past the camera, then is pushed back down right below the cam's top.

    Not a big deal but I wanted to get others' input here. Thanks in advance.

    GIF of movement: https://imgur.com/O4RFMCX

    Code (CSharp):
    1.  
    2.  
    3. public class CharacterMove : MonoBehaviour {
    4.  
    5.     public int speed = 5;
    6.  
    7.     public GameObject marker;
    8.     public Animator charAnim;
    9.     public bool canMove;
    10.  
    11.  
    12.     // Use this for initialization
    13.     void Start ()
    14.     {
    15.     }
    16.  
    17.     // Update is called once per frame
    18.     void Update ()
    19.     {
    20.      
    21.  
    22.         if (Input.GetMouseButtonUp(0))
    23.         {
    24.             StopCoroutine("MoveCharacter");
    25.             Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    26.             StartCoroutine("MoveCharacter", mousePos);
    27.         }
    28.     }
    29.  
    30.     public IEnumerator MoveCharacter(Vector2 mousePos)
    31.     {
    32.         //while char hasn't reached destination
    33.         while (Vector3.Distance(transform.position, mousePos) > 0.1f)
    34.         {
    35.             //store main camera
    36.             Camera cam = Camera.main;
    37.  
    38.             //store half the character size
    39.             Bounds charBounds = transform.GetComponent<Renderer>().bounds;
    40.  
    41.             //store the top and bottom positions of the character
    42.             float charTop = charBounds.center.y + charBounds.extents.y;
    43.             float charBottom = charBounds.center.y - charBounds.extents.y;
    44.  
    45.             charAnim.SetBool("charIsMoving", true);
    46.  
    47.             gameObject.GetComponent<SpriteRenderer>().flipX = ShouldCharFlip(mousePos);
    48.  
    49.             //if char's top, bottom, left, and right are within camera
    50.             if (charTop < cam.orthographicSize
    51.                 && charBottom > cam.orthographicSize * -2)
    52.             {
    53.                 transform.position = Vector2.MoveTowards(transform.position, mousePos, speed * Time.deltaTime);
    54.             }
    55.             //else if char's top hits cam's top
    56.             else if (charTop > cam.orthographicSize)
    57.             {
    58.                 //push char back down slightly
    59.                 transform.position = new Vector3(transform.position.x, cam.orthographicSize - charBounds.extents.y - 0.05f);
    60.                 //disallow any higher mouse movement
    61.                 mousePos.y = cam.orthographicSize - charBounds.extents.y - 0.05f;
    62.             }
    63.             else if (charBottom < cam.orthographicSize * -1)
    64.             {
    65.  
    66.             }
    67.  
    68.             yield return null;
    69.         }
    70.         //stop animating when char has stopped moving
    71.         StopCharMoving();
    72.     }
    73.  
    74. [B]//BELOW isn't necessarily relevant to my question[/B]
    75.     bool ShouldCharFlip(Vector2 mousePos)
    76.     {
    77.         Vector3 charCenter = transform.GetComponent<Renderer>().bounds.center;
    78.  
    79.         if (mousePos.x < charCenter.x)
    80.             return true;
    81.         else
    82.             return false;
    83.     }
    84.  
    85.     public void StopCharMoving()
    86.     {
    87.         StopCoroutine("MoveCharacter");
    88.         charAnim.SetBool("charIsMoving", false);
    89.         gameObject.GetComponent<SpriteRenderer>().flipX = false;
    90.     }
    91. }
    92.  
     
    Last edited: Nov 21, 2017
  2. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    I couldn't really tell from the gif, but if you're saying it goes slightly higher and then is bumped back down, I suppose 1 "easy" solution would be to simply double check the position after you move it, thereby (re-)adjusting it within the same frame, if it had gone out :)
     
    tfishell likes this.
  3. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Thanks for responding. Do you sort of mean take the MoveTowards out of the initial if statement, and only use if statements below it? Or could you clarify?

    Code (CSharp):
    1. transform.position = Vector2.MoveTowards(transform.position, mousePos, speed * Time.deltaTime);
    2.  
    3.             if (charTop > cam.orthographicSize)
    4.             {
    5.                 Debug.Log("isAbove");
    6.                 //push char back down slightly
    7.                 transform.position = new Vector3(transform.position.x, cam.orthographicSize - charBounds.extents.y - 0.05f);
    8.                 //disallow any higher mouse movement
    9.                 mousePos.y = Camera.main.orthographicSize - charBounds.extents.y - 0.05f;
    10.             }
     
  4. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    That wasn't exactly what I had meant when I was writing, but I believe the result would be the same. :)
     
  5. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Okay, I think the result is close to the same, but maybe I'm expecting too much. Thanks. :)
     
  6. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Okay, 1 small detail.. Do you have :
    move
    calculate char top
    adjust
    in that order?
     
  7. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    No, currently it's more like:
    calc char top
    move
    adjust

    I'll try your order tomorrow though. (Getting to bed now, 11pm where I am.)
     
  8. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    no prob.. ya, I was just thinking if you had the calculation before, then after the move, it will be using the last calculation. :)
     
    tfishell likes this.
  9. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Trying it seems to produce the same or similar result, but thanks anyway. I think what I have will work well enough. :)
     
  10. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Okay, fair enough.. :) Good luck with your game.
     
    tfishell likes this.
  11. fire7side

    fire7side

    Joined:
    Oct 15, 2012
    Posts:
    1,819
    I was thinking you could use a variable for the transform position and move and test that and then make the transform equal to it if it passed, but I don't know how you got the boundaries to check it with the camera.
     
    Last edited: Nov 22, 2017
  12. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    The simplest sounds to me to just clamp the mouse click position to a rectangle that is the screen size less the extends of the Sprite. No need to check positions in the loop
     
  13. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    I'm using cam.orthographicSize (5) to check the top of the cam. "if (charTop >= cam.orthographicSize)"

    I think I tried what you said (after hours of trying to debug something that should have been an easy fix :p). The result seems the same, if I'm actually doing it your way. But again, I may just be too picky; there's a tiny bump then the char is sent back down.

    Click on the screen (which is sent to the IEnumerator below as mousePos) and the Coroutine starts.

    Code (CSharp):
    1.     public IEnumerator MoveCharacter(Vector2 mousePos)
    2.     {
    3.         //while char hasn't reached destination
    4.         while (Vector3.Distance(charTrans.position, mousePos) > 0.1f)
    5.         {
    6.             charBounds = transform.GetComponent<Renderer>().bounds;
    7.             charTop = charBounds.center.y + charBounds.extents.y;
    8.          
    9.             if (charTop >= cam.orthographicSize)
    10.             {
    11.                 charTrans.position = new Vector3(charTrans.position.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    12.                 mousePos = new Vector3(mousePos.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    13.             }
    14.          
    15.             transform.position = Vector2.MoveTowards(charTrans.position, mousePos, speed * Time.deltaTime);
    16.            
    17. yield return null;
    18.         }
    19.     }
    20.  
    21.  
    I'm willing to keep taking advice, but I also want to move on to other gameplay scripting pretty soon.

    I'll experiment with clamping eventually, though last time I tried that, the character just went back and forth between the two min and max positions every other frame (but I assume I did something wrong :p).
     
  14. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Since I saw you updating the thread, not to repeat myself, but did you try something like this?
    Code (csharp):
    1.  
    2. public IEnumerator MoveCharacter(Vector2 mousePos)
    3.     {
    4.         //while char hasn't reached destination
    5.         while (Vector3.Distance(charTrans.position, mousePos) > 0.1f)
    6.         {
    7.             transform.position = Vector2.MoveTowards(charTrans.position, mousePos, speed * Time.deltaTime);
    8.             charBounds = transform.GetComponent<Renderer>().bounds;
    9.             charTop = charBounds.center.y + charBounds.extents.y;
    10.          
    11.             if (charTop >= cam.orthographicSize)
    12.             {
    13.                 transform.position = new Vector3(charTrans.position.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    14.                 yield return break;
    15. /*
    16.                 charTrans.position = new Vector3(charTrans.position.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    17.                 mousePos = new Vector3(mousePos.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    18. */
    19.             }
    20.           yield return null;
    21.         }
    22.     }
    23.  
     
    tfishell likes this.
  15. fire7side

    fire7side

    Joined:
    Oct 15, 2012
    Posts:
    1,819
    First you use MoveTowards with a variable, that gets the character into the position he may be if it tests all right. Then you test that position to see if it is in range. If it is, you simply make the transform position equal to it.

    Sorry, I didn't notice the other post. But yeah, same deal.
     
    tfishell likes this.
  16. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Thanks, what you suggested seems to work a little better, though I couldn't do "yield return break" and used "yield return null" instead. I haven't used "yield" or "break" very much.

    I am concerned about the stuttering (https://imgur.com/a/sJpXm, though this was recorded at a 16fps framerate so it may be hard to tell) but I assume that's unrelated to this. (I tried FixedUpdate and that seemed to help the stuttering but I guess because it runs at a fixed interval, it doesn't recognize quick clicks, like if I click somewhere than quickly click elsewhere, that click won't be recognized.)

    Code (CSharp):
    1. public IEnumerator MoveCharacter(Vector2 mousePos)
    2. {
    3.     //while char hasn't reached destination
    4.     while (Vector3.Distance(transform.position, mousePos) > 0.1f)
    5.     {
    6.         //move towards
    7.         transform.position = Vector2.MoveTowards(transform.position, mousePos, speed * Time.deltaTime);
    8.  
    9.         charBounds = transform.GetComponent<Renderer>().bounds;
    10.         charTop = charBounds.center.y + charBounds.extents.y;
    11.  
    12.         if (charTop >= cam.orthographicSize)
    13.         {
    14.             transform.position = new Vector3(transform.position.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    15.             mousePos = new Vector3(mousePos.x, cam.orthographicSize - charBounds.extents.y - 0.02f);
    16.            
    17.             yield return null;
    18.  
    19.         }
    20.  
    21.         yield return null;
    22.     }
    23.     //stop animating when char has stopped moving
    24.     StopCharMoving();
    25. }
     
  17. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Just responding to 1 part -- it may be just 'yield break'.
     
  18. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    If I replace "yield return null" with "yield break", then (while the character is touching or near the top) the character stops moving entirely and doesn't reach the StopAnimating() function call outside the While loop. https://i.imgur.com/dEciYdN.gif

    So for now I'll probably stick with "yield return null" (though, honestly, I'm not sure including that is necessary anyway; commenting it out doesn't seem to make a difference.)
     
  19. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Ya, before I hadn't thought about that method there (maybe it wasn't there before).
    Part of it was just correcting the syntax of my post ;) (how you'd break out , if you had wanted to).

    Anyways, I just hope all in all it's pretty good..
     
    tfishell likes this.
  20. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    I appreciate the help. :) I'm sure I'll have more questions in the future, though of course I'll try to research first.
     
  21. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    No problem. Take it easy & have fun :)
     
  22. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    This is what I meant...one could also use a ray to find the intersect on the bounding box for a "more accurate" end point
    Code (CSharp):
    1. public class MoveInBox : MonoBehaviour
    2. {
    3.     public float speed = 10f;
    4.     private Rect boundingBox;
    5.  
    6.     void Start ()
    7.     {
    8.         Vector3 spriteSize = gameObject.GetComponent<SpriteRenderer>().bounds.size;
    9.         float viewHeight = Camera.main.orthographicSize * 2;
    10.         float viewWidth = Camera.main.aspect * viewHeight;
    11.         boundingBox = new Rect(0f, 0f, viewWidth - spriteSize.x, viewHeight - spriteSize.y);
    12.         boundingBox.center = Vector2.zero;
    13.         GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Quad);
    14.         temp.transform.localScale = new Vector3 (viewWidth, viewHeight, 1f + spriteSize.z) - spriteSize;
    15.     }
    16.  
    17.     void Update ()
    18.     {
    19.         if (Input.GetMouseButtonUp(0))
    20.         {
    21.             StopCoroutine("MoveCharacter");
    22.             Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    23.             StartCoroutine("MoveCharacter", mousePos);
    24.         }  
    25.     }
    26.  
    27.     public IEnumerator MoveCharacter(Vector2 mousePos)
    28.     {
    29.         Vector3 endPos = mousePos;
    30.      
    31.         endPos.x = Mathf.Clamp(endPos.x, boundingBox.xMin, boundingBox.xMax);
    32.         endPos.y = Mathf.Clamp(endPos.y, boundingBox.yMin, boundingBox.yMax);
    33.      
    34.         while (transform.position != endPos)
    35.         {
    36.             transform.position =  Vector3.Lerp(transform.position, endPos, speed * Time.deltaTime);
    37.             yield return null;
    38.         }
    39.     }
    40. }
     
    tfishell likes this.
  23. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Thanks. I'll have to play around with those variables to try to understand exactly what is happening here (as far as the initial variables go); I can't picture it in my head.

    Question: isn't leaving "private" blank the same as the variable being private? I'm thinking specifically of
    Code (CSharp):
    1. private Rect boundingBox;
     
    Last edited: Nov 23, 2017
  24. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Yes, blank/"private" is the same.
     
    tfishell likes this.
  25. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    You don’t have to picture it.
    I did for you :) - line 13/14 of the start function are not required, they visualize what is happening but can be deleted
     
    tfishell likes this.
  26. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Oh okay, thanks, I appreciate that. :) I'm just trying to get a better understanding so I'm going to mess around with it. My brain doesn't seem to like to retain this information easily. :p
     
  27. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Can you explain to me why "1f + spriteSize.z" is necessary, at least for me, to see any color on the quad?

    Code (CSharp):
    1. temp.transform.localScale = new Vector3 (viewWidth, viewHeight, 1f + spriteSize.z) - spriteSize;
    If I do
    Code (CSharp):
    1. temp.transform.localScale = new Vector3 (viewWidth - spriteSize.x, viewHeight - spriteSize.y);
    then I can see the quad as all black but the color never changes.

    If I do
    Code (CSharp):
    1. temp.transform.localScale = new Vector3 (viewWidth, viewHeight) - spriteSize;
    then I can't see the quad unless I go into 3D mode and fly around to the side facing away from the camera.

    It must have something to do with z-depth and camera interaction.
     
  28. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    I might have confused you with this temporary Object. It was meant to visualize the "valid" positions for the sprite. I did this in the simplest way possible but it brought in a 3D object to your 2D setup and might be more confusing to you than helpful, since it adds many things you do not need really. I.e. it is affected by light, it's color is set through its material, etc.

    Your questions:
    1. The color of the quad is defined by it's material (default: white) and by the light (default: assuming no light in the scene, the environment lighting -> ambient of the scene, see window->lighting) - I think in the scene view, it needs to have a z scale different from 0 to show in it's default color, in the game view it will show as the default color * light = gray, even if z=0). I just gave it z = 1 out of habit and I am I am sizing the quad not through its vertices (3D points) but through it's scale attribute. Width = ScreenWidth - SpriteWidth, Height = ScreenHeight - SpriteHeight, Depth = 1f + SpriteDepth - SpriteDepth (effectively 1f, could be any other number > 0)
    2. Because z will be 0. You are creating a 3D vector with only x, y and z will be set to 0 --- I guess it goes black in scene view and stays gray in game view?
    3. Because z will be negative. You have effectively flipped around (turned by 180) the quad. Since a quad uses a material that only renders its top side (not the bottom) you have to "walk around it" to see it

    Are you happy with the results of the script? If so, maybe better to ignore the temp object and draw it on a piece of paper. All I am doing is in 2D space, I just used a 3D object seen from birds eye perspective to visualize it.
     
    tfishell likes this.
  29. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    Thanks, I do appreciate the temporary object, it did help me to understand what the dimensions were. I was just confused about why the color was having issues. (I made a thread about it.) I was setting the color to red in the script (Color.red) but nothing was happening initially.

    Image showing color differences: https://i.imgur.com/pqHfoD6.png

    I haven't finished applying the script yet (for various reasons). It seems like it'll work well, my only concern is that mouse clicks won't register outside of the clamped area (but within camera view of course). If so, I'm wondering if I could apply clamping to the character's movement rather than the mouse click area.
     
    Last edited: Nov 26, 2017
  30. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    You got this wrong - mouse clicks will register outside the clamped area. The movement will be restricted to the area only.
     
    tfishell likes this.
  31. tfishell

    tfishell

    Joined:
    Nov 20, 2017
    Posts:
    97
    I just now realized that after re-reading the code. :p Thanks for confirming though.
     
  32. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    I just responded in your other thread, too. I hope this helps you to get a better understanding on why you had issues with the color for 3D objects. It is mainly a material/shader thing. The standard shader for 3D objects uses light. If you change this to unlit/color you will get the results you expect.

    PS:
    Add this after line 14 in my script above. This will actually create a new material instance in memory, that behaves more like you would expect it from a sprite
    Code (CSharp):
    1.         temp.GetComponent<Renderer>().material.shader = Shader.Find("Unlit/Color");
    2.         temp.GetComponent<Renderer>().material.color = Color.red;
     
    Last edited: Nov 26, 2017
    tfishell likes this.