Search Unity

Methods of keeping player in view

Discussion in 'Game Design' started by DroidifyDevs, Sep 2, 2017.

  1. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Hi!

    Currently I'm messing around with a procedurally generated world with the character running around a forest. There's also a primitive inventory system, and a way to cut down trees. The trees are automatically populated, along with the monsters.

    Here's what it looks like for now:
    upload_2017-9-2_15-32-54.png

    However, I've run into an issue: Sometimes the camera gets blocked by a tree. Since it's always following the player, if the player runs behind a tree, you can't see the player. For example:
    upload_2017-9-2_15-34-21.png
    Looking at that, you'd never guess that the player is behind that tree, and if you're fighting an enemy, this is very annoying as you can't see what you're doing. I've made a script that will raise the camera over the tree to keep the player in view. However, this creates a strange, jarring experience:


    So, currently, I've thought of 3 paths to go down:
    1: Shorten the tall trees so none of them completely block the camera. However, this would mostly kill the "in the forest" effect I'm going for.
    2: Make a point-based camera system like in the Walking Dead Telltale games. I'm afraid it might be confusing for players though.
    3: Just do nothing and let the camera clip through trees.

    What are your ideas on this?
     

    Attached Files:

  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Always a thorny issues. Here are some solutions I've seen that aren't already on your list:
    1. When a tree is in the way (detectable via raycast), make it see-through (lower its alpha value).
    2. When something is in the way, move the camera closer to the player until it's not.
    3. When something's in the way, move the camera off to the side and rotate it. For this I imagine the camera is attached to the player via an invisible rod, and you basically do collision detection and response between this rod and the environment.
    3 is certainly the trickiest, and might work in only certain kinds of environments. I think in your case I'd like option 1 best, which happily is also the easiest to implement.
     
  3. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    This is what most games do in my experience.
     
  4. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Yeah I noticed that, but for me game as shown in the screenshots, that wouldn't really be an option as its a top/down angled view.

    I'm going to try a different camera movement system; if that fails then I'll probably go with #1 from JoeStrout. This should be a fun challenge :)
     
  5. RockoDyne

    RockoDyne

    Joined:
    Apr 10, 2014
    Posts:
    2,234
    Then you either scrap the camera for a better one or construct environments around the limitation.
     
    angrypenguin likes this.
  6. Fera_KM

    Fera_KM

    Joined:
    Nov 7, 2013
    Posts:
    307
    Isn't this a perfect problem for the new cinemachine to deal with?

    edit: example, have a second orbit cam at slightly higher angle.
     
    DroidifyDevs likes this.
  7. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Never used Cinemachine, but I should look into it.

    I also noticed outlines can be helpful:
    upload_2017-9-4_13-12-56.png

    That doesn't fix the problem, but it makes a hidden player 50% less annoying.

    I'm currently working on a lerping camera controller with raycasts; if it's close to a tree it moves to the side so you can see the player. In theory I have it working but I want to add accelerating lerps to make it smooth. I have free time today, so I'll try to finish it today and post the result! Fingers crossed.
     
  8. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I wasn't sure if you were making the camera fixed in some way. In that case I would definitely concur that #1 from JoeStrout is the best idea. I'll be honest, I might be annoyed if a camera suddenly swung around without my telling it to do so (but then, I also don't care for it in traditional third-person cameras).
     
    angrypenguin likes this.
  9. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Well, after spending most of the day on this, here's what I came up with:


    I like 80% of it; specifically how you can walk around a bit without the camera moving, and also how the camera can move between trees. So far I know of 2 bugs: If the view to the player is blocked by a tree farther than the TreeDodgeDistance away, the camera won't move. Also, the player can sometimes move out of view to the sides and can only be seen after letting go of the movement keys.

    It usually avoids trees, but leaves the small ones alone so the player can still be behind them without the camera moving. I really like that effect and apart from a few bugs, I'm pretty satisfied with the result. I think I need to increase the TreeDodgeDistance so it'll try to avoid trees farther away, and maybe scale the gigantic ones down slightly.

    Also, this was my 1st time using a Tween engine (LeanTween), and I have to say it's pretty awesome :). I might still look into Cinemachine just to learn it though.

    Here's the rough code, over the next few days, once I have a mostly finished sample I'll update the code to a polished and well-commented version:
    Code (CSharp):
    1. //NOTE: REQUIRES LEANTWEEN. Get it from the Asset Store!
    2. using cakeslice;
    3. using System.Collections;
    4. using UnityEngine;
    5.  
    6. public class CameraControllerTweenV2 : MonoBehaviour
    7. {
    8.     #region "Variables"
    9.     public PlayerController PlayerControllerScript;
    10.     LeanTween TweenScript;
    11.     LeanTweenType TweenType;
    12.     public Transform Player;
    13.     public Vector3 StartOffset;
    14.     public Vector3 PlayerToCameraDifference;
    15.     public float OffsetToKeepX;
    16.     public float OffsetToKeepZ;
    17.     public float MoveTime;
    18.     public Vector3 PreviousFrameVector;
    19.     public Vector3 DifferecePerFrame;
    20.     public Vector3 ToPlayer;
    21.     private Ray _Ray;
    22.     private RaycastHit _RayHit;
    23.     public bool CoolingDownX;
    24.     public bool CoolingDownZ;
    25.     public bool EnableDirectControlMaster;
    26.     public bool EnableDirectControlX;
    27.     public bool EnableDirectControlZ;
    28.     public bool DodgingTree;
    29.     public bool Override;
    30.     public bool Test;
    31.     public float RayHitDistance;
    32.     public bool PlayerIsVisible;
    33.     public float TreeDodgeDistance;
    34.     #endregion
    35.     void Start()
    36.     {
    37.         StartOffset = transform.position - Player.position;
    38.     }
    39.  
    40.     void FixedUpdate()
    41.     {
    42.         PlayerToCameraDifference = transform.position - Player.position;
    43.         if (Mathf.Abs(PlayerToCameraDifference.x) > 5f && !CoolingDownX && !EnableDirectControlX)
    44.         {
    45.             LeanTween.cancelAll();
    46.             EnableDirectControlMaster = true;
    47.             EnableDirectControlX = true;
    48.             OffsetToKeepX = transform.position.x - Player.position.x;
    49.         }
    50.  
    51.         if (Mathf.Abs(PlayerToCameraDifference.z) < 4f  && !CoolingDownZ && !EnableDirectControlZ)
    52.             PrepareZ();
    53.         if (Mathf.Abs(PlayerToCameraDifference.z) > 12f && !CoolingDownZ && !EnableDirectControlZ)
    54.             PrepareZ();
    55.  
    56.         if (EnableDirectControlMaster)
    57.         {
    58.             if (Mathf.Abs(PlayerToCameraDifference.x) > 5f && EnableDirectControlX)
    59.             {
    60.                 //if (TestMovePointForTree(new Vector3((Player.position.x + OffsetToKeepX), transform.position.y, transform.position.z)))
    61.                     transform.position = new Vector3((Player.position.x + OffsetToKeepX), transform.position.y, transform.position.z);
    62.             }
    63.             if (Mathf.Abs(PlayerToCameraDifference.z) < 4f || Mathf.Abs(PlayerToCameraDifference.z) > 12f && EnableDirectControlZ)
    64.             {
    65.                 //if (TestMovePointForTree(new Vector3(transform.position.x, transform.position.y, (Player.position.z + OffsetToKeepZ))))
    66.                     transform.position = new Vector3(transform.position.x, transform.position.y, (Player.position.z + OffsetToKeepZ));
    67.             }
    68.             //if stopped moving
    69.             if (!PlayerControllerScript.IsMoving && !Override)
    70.             {
    71.                 AttemptToCenter();
    72.             }
    73.         }
    74.         ToPlayer = (Player.position - transform.position).normalized;
    75.         #region "Raycasting"
    76.         if (Physics.Raycast(transform.position, ToPlayer, out _RayHit) && _RayHit.transform.name != "Player")
    77.         {
    78.             GetComponent<OutlineEffect>().enabled = true;
    79.             PlayerIsVisible = false;
    80.         }
    81.         else
    82.         {
    83.             GetComponent<OutlineEffect>().enabled = false;
    84.             PlayerIsVisible = true;
    85.         }
    86.         RayHitDistance = _RayHit.distance;
    87.         if (_RayHit.transform.tag == "Choppable" && _RayHit.distance < TreeDodgeDistance && !Override && !PlayerIsVisible)
    88.         {
    89.             AttemptToCenter();
    90.             Override = true;
    91.             LeanTween.cancelAll();
    92.             //DodgingTree = true;
    93.             if (TestMovePointForTree(new Vector3(_RayHit.transform.position.x + 5, transform.position.y, _RayHit.transform.position.z)) && _RayHit.transform.name != "Player")
    94.             {
    95.                 StartLerp(new Vector3(_RayHit.transform.position.x + 5, transform.position.y, _RayHit.transform.position.z));
    96.                 Debug.Log("Dodging right " + _RayHit.transform.name + " " + new Vector3(_RayHit.transform.position.x + 5, transform.position.y, _RayHit.transform.position.z));
    97.                 return;
    98.             }
    99.             if (TestMovePointForTree(new Vector3(_RayHit.transform.position.x - 5, transform.position.y, _RayHit.transform.position.z)) && _RayHit.transform.name != "Player")
    100.             {
    101.                 StartLerp(new Vector3(_RayHit.transform.position.x - 5, transform.position.y, _RayHit.transform.position.z));
    102.                 Debug.Log("Dodging right " + _RayHit.transform.name + " " + new Vector3(_RayHit.transform.position.x - 5, transform.position.y, _RayHit.transform.position.z));
    103.                 return;
    104.             }
    105.             else
    106.             {
    107.                 Debug.Log("Can't dodge this tree because " + new Vector3(_RayHit.transform.position.x + 5, transform.position.y, _RayHit.transform.position.z) + " && "
    108.                     + new Vector3(_RayHit.transform.position.x - 5, transform.position.y, _RayHit.transform.position.z) + " don't pass tree bool inspection");
    109.                 Override = false;
    110.             }
    111.         }
    112.         else
    113.             Override = false;
    114.         #endregion
    115.     }
    116.  
    117.     //when dodging a tree, try this FIRST. That way, you don't have to move to an OK position then immediatly center the camera
    118.     //because centered was also an OK position
    119.     void AttemptToCenter()
    120.     {
    121.         if (TestMovePointForTree(new Vector3(Player.position.x, transform.position.y, Player.position.z + StartOffset.z)))
    122.         {
    123.             Debug.Log("Attempted to center, Override no longer needed");
    124.             LeanTween.cancelAll();
    125.             Override = false;
    126.             StopLerp();
    127.         }
    128.         else
    129.             Debug.Log("Stopped centering due to tree near " + new Vector3(Player.position.x, transform.position.y, Player.position.z + StartOffset.z));
    130.     }
    131.  
    132.     bool TestMovePointForTree(Vector3 TargetPosition)
    133.     {
    134.         //Debug.Log("Testing: " + TargetPosition);
    135.         if (Physics.Raycast(TargetPosition, (Player.position - TargetPosition).normalized, out _RayHit) && _RayHit.transform.name != "Player")
    136.         {
    137.             if (_RayHit.transform.tag == "Choppable" && _RayHit.distance < TreeDodgeDistance)
    138.             {
    139.                 Debug.Log(_RayHit.transform.name + " was in the way!");
    140.                 return false;
    141.             }
    142.             else
    143.             {
    144.                 Debug.Log(TargetPosition + " was deemed a safe place for the camera");
    145.                 return true;
    146.             }
    147.         }
    148.         else
    149.         {
    150.             //Debug.Log("No ray hit from " + TargetPosition);
    151.             return true;
    152.         }
    153.     }
    154.  
    155.     void PrepareZ()
    156.     {
    157.         LeanTween.cancelAll();
    158.         EnableDirectControlMaster = true;
    159.         EnableDirectControlZ = true;
    160.         OffsetToKeepZ = transform.position.z - Player.position.z;
    161.         //OffsetToKeepZ = Player.position.z - 7;
    162.     }
    163.  
    164.     void StartLerp(Vector3 Destination)
    165.     {
    166.         float MoveTime = 3f;
    167.         if (Override)
    168.         {
    169.             MoveTime = 5f;
    170.         }
    171.         LeanTween.moveX(gameObject, Destination.x, MoveTime).setEase(TweenType);
    172.         //need start offset for Z so camera won't be directly above player
    173.         if(Override)
    174.             LeanTween.moveZ(gameObject, Destination.z, MoveTime).setEase(TweenType);
    175.         else
    176.             LeanTween.moveZ(gameObject, Destination.z + StartOffset.z, MoveTime).setEase(TweenType);
    177.     }
    178.  
    179.     void StopLerp()
    180.     {
    181.         TweenType = LeanTweenType.easeOutBack;
    182.         if(!Override)
    183.             StartLerp(Player.position);
    184.         EnableDirectControlMaster = false;
    185.         EnableDirectControlX = false;
    186.         EnableDirectControlZ = false;
    187.     }
    188.  
    189.     IEnumerator CoolDownXTimer()
    190.     {
    191.         CoolingDownX = true;
    192.         yield return new WaitForSeconds(0.2f);
    193.         CoolingDownX = false;
    194.     }
    195.  
    196.     IEnumerator CoolDownZTimer()
    197.     {
    198.         CoolingDownZ = true;
    199.         yield return new WaitForSeconds(0.2f);
    200.         CoolingDownZ = false;
    201.     }
    202. }
    203.  
    What do you think?
     
    theANMATOR2b likes this.
  10. Xoduz

    Xoduz

    Joined:
    Apr 6, 2013
    Posts:
    135
    Is this still possible with terrain trees?
     
  11. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Since it's a procedural system, I'm spawning tree prefabs that are tagged "Choppable". I don't know of a way to interact with individual Terrain trees, so with this script, no.

    Unless you try one of the rather complicated solutions here:
    https://forum.unity3d.com/threads/interact-with-terrain-trees.301470/

    And that is why I never use trees in the Terrain system :confused:
     
    EternalAmbiguity likes this.
  12. Fera_KM

    Fera_KM

    Joined:
    Nov 7, 2013
    Posts:
    307
    I haven't either, and I should also look into it. Just haven't had the use for it yet.
    But it would be interesting to see how it used, considering how they (unity) described it to me.


    • Clear Shot Real-time shot evaluation. Setup any number of cameras and give them a priority. If the camera becomes occluded or can't make a good shot, Cinemachine will cut (or blend) to the next highest priority shot.
     
    DroidifyDevs likes this.
  13. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    OH MY GOD. I don't think anyone developing in Unity is ever going to write a camera controller again.

    After 2 hours of watching Unite 2017 Austin Cinemachine demos to convince myself to dive in, I spent another 20 minutes watching tutorials and came up with this:

    For so little work and absolutely ZERO (null) (0) scripting, I was able to achieve such an amazing result. My script above, I probably spent 15 hours on 5 different revisions, even using a tweening engine, and even if it worked bug-free it still wouldn't achieve this result. I'm not the world's best programmer so I'm sure it would be possible to do this without Cinemachine, but this is just so easy and so fast.

    With the Curb Feelers option, you have to try hard to get the camera to clip something. I'm just amazed. Huge thanks @Kemonono
     
  14. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    You can do that occluding with a very simple shader. You don't want to do what you are doing. Sorry don't have the code in front of me right now I'll try and post it tomorrow.
     
  15. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Code (csharp):
    1.  
    2. Shader "Custom/StandardOccluded"
    3. {
    4.     Properties {
    5.         _Color ("Color", Color) = (1,1,1,1)
    6.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    7.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    8.         _Metallic ("Metallic", Range(0,1)) = 0.0
    9.         _OccludedColor("Occluded Color", Color) = (1,1,1,1)
    10.     }
    11.     SubShader {
    12.  
    13.         Pass
    14.         {
    15.             Tags { "Queue"="Geometry+1" }
    16.             Blend SrcAlpha OneMinusSrcAlpha
    17.             ZTest Greater
    18.             ZWrite Off
    19.  
    20.             CGPROGRAM
    21.             #pragma vertex vert          
    22.             #pragma fragment frag
    23.             #pragma fragmentoption ARB_precision_hint_fastest
    24.  
    25.             half4 _OccludedColor;
    26.  
    27.             float4 vert(float4 pos : POSITION) : SV_POSITION
    28.             {
    29.                 float4 viewPos = UnityObjectToClipPos(pos);
    30.                 return viewPos;
    31.             }
    32.  
    33.             half4 frag(float4 pos : SV_POSITION) : COLOR
    34.             {
    35.                 return _OccludedColor;
    36.             }
    37.  
    38.             ENDCG
    39.         }
    40.  
    41.         Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}
    42.         LOD 200
    43.         ZWrite On
    44.         ZTest LEqual
    45.      
    46.         CGPROGRAM
    47.         // Physically based Standard lighting model, and enable shadows on all light types
    48.         #pragma surface surf Standard fullforwardshadows
    49.  
    50.         // Use shader model 3.0 target, to get nicer looking lighting
    51.         #pragma target 3.0
    52.  
    53.         sampler2D _MainTex;
    54.  
    55.         struct Input {
    56.             float2 uv_MainTex;
    57.         };
    58.  
    59.         half _Glossiness;
    60.         half _Metallic;
    61.         fixed4 _Color;
    62.  
    63.         void surf (Input IN, inout SurfaceOutputStandard o) {
    64.             // Albedo comes from a texture tinted by color
    65.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    66.             o.Albedo = c.rgb;
    67.             // Metallic and smoothness come from slider variables
    68.             o.Metallic = _Metallic;
    69.             o.Smoothness = _Glossiness;
    70.             o.Alpha = c.a;
    71.         }
    72.         ENDCG
    73.     }
    74.     FallBack "Diffuse"
    75. }
    76.  
     
  16. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    @snacktime, I have to confess I don't understand how this shader works or how to use it. Can you explain?

    Thanks,
    - Joe
     
  17. Max_Bol

    Max_Bol

    Joined:
    May 12, 2014
    Posts:
    168
    Personally, here how I would have tackled this problem by design.

    I would have made the trees into 3 possible versions :
    1) The normal Tree,
    2) A partial version of the tree,
    3) The cut version of the tree.

    The way it can be handle depends on what you want to do.

    It could be via raycast between the camera and the player or simply by making a system that detect all trees between the player and the camera. Then depending on what method you use, you can switch the "normal" trees by their "partial" version.

    Both ways requires a bit of finesse as to how to handle them in an optimal way, but they are still relatively reliable and easy to do.

    The key part in this method I'm writing about is that you can control how the tree that are between the player and the camera looks. If you want, you could even make the upper part of them kinda shadowy and semi-transparent or something.

    Personally, I would avoid forcing the camera around when in collision with any trees. Not that it's should never be done, but simply that it should mainly only be done when the space around the trees or any collision is sufficient. When the camera turn or the player move, if the camera "hit" a tree too often and start moving, it can get really chaotic and the could even make your players sick. (You know those game that are critiqued for their bad camera? Same comment would come if your camera keeps moving around because it hit tons of stuff in the world.)

    This is why, usually, you find camera that "avoid" obstacles between the player and the said camera in games that have really short distance between both by default. Games in 3rd person like Skyrim, Legend of Zelda, Dark Souls and so on. Your game seem more closer to games like Diablo and Grim Dawn in how the view is presented.

    Just a tip here. :)
     
    theANMATOR2b and DroidifyDevs like this.
  18. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Correct me if I'm wrong, but don't those games tend to avoid the issue rather than solve it? I don't remember there being a lot of large, camera occluding objects in any of the Diablo games I've played, or any of their clones. The camera is at a fairly sharp downward angle to minimise the effect, it's possibly ortho rather than perspective, and there are few if any objects so tall as to occlude more than a character's worth of space in the play area.

    When there is stuff that would otherwise occlude the camera, it's stuff like roofs that can be turned off or made really transparent when you go under it.
     
    Kiwasi likes this.
  19. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    If I'm reading it right (and I admit my reading of shaders is still sketchy) this shader should be applied to the player.

    If the player is hidden, it will draw them in a flat _OcculdedColour. If the player is not hidden it will draw them normally.

    You could use the same principle to draw the player in any number of ways.
     
    JoeStrout likes this.