Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

For enemy to follow players exact movement (Doppelganger)

Discussion in 'Scripting' started by MarkHest, Jan 27, 2016.

  1. MarkHest

    MarkHest

    Joined:
    Jan 27, 2016
    Posts:
    6
    Hi. I'm making a 2D platforming game and in some levels I want an enemy to spawn and then follow the player around and if it touches you you're dead. What I need help with is for the doppelganger enemy to follow the players exact movement but with a delay of maybe 1 or 2 seconds. So the enemy will eventually catch up with you if you stand still. (The doppelganger should not have gravity or collision).

    How can I make the enemy(object) follow the players exact movement but with a delay? Any theories or script help? (I'm using C# to script)
     
  2. Mich_9

    Mich_9

    Joined:
    Oct 22, 2014
    Posts:
    118
    Create a queue to store the player position
    Code (CSharp):
    1. Queue<Vector2> playerpos = new Queue<Vector2>();
    Then add the player current position to the queue every Update (fixedUpdate if you want more precision). When your enemy is ready to chase, is just matter of follow the previously saved player position and apply then to your enemy.
     
  3. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    If you want it to follow their exact position (X) frames behind, then you can simply keep a queue of positions - pop one off the top and set the baddie's position to that, insert one on the bottom that gets its position from the player. (This is probably how most SNES-era RPGs had characters follow each other) Very simple, very fast.

    However, a modern PC game implementing that runs into one problem: inconsistent framerates. On a slow computer, the baddie would be much further behind than someone on a fast computer. And because framerates can vary even from frame to frame, the movement is likely to look jittery. We need framerate-independence.

    We'll have the same concept as before - a queue of positions - only we need to store the time of the capture along with the position. Let's make our own data structure:
    Code (csharp):
    1. public class TimePositionData {
    2. public float time;
    3. public Vector2 position;
    4. public TimePositionData(float t, Vector2 pos) {
    5. time=t;
    6. position=pos;
    7. }
    Use this in your queue instead of a plain Vector2. Now, when you go to pop them off, use a while loop to pop off as many (or as few, or no) bits of position data until positionData.time >= (current time - delay).
     
  4. Mich_9

    Mich_9

    Joined:
    Oct 22, 2014
    Posts:
    118
    Partially true, in this case is not as you say, the bad guy cannot go further or less far because the rate of storing/loading position will be the same for the player and enemy independent of the framerate.
     
  5. MarkHest

    MarkHest

    Joined:
    Jan 27, 2016
    Posts:
    6
    Thanks for replying! I'm a little confused.
    I've never used Queue before so I don't know how to use it. How do I save the players vector in the queue, it won't let me use "GameObject.FindGameObjectWithTag("Player)".transform.position".

    @StarManta: Should I make my queue like this? public Queue<TimePositionData> playerpos = new Queue<TimePositionData>();

    So I should make a new Queue every update and save it in a list of arrays somewhere? Then in every fixed update make the enemys transform be the vectors stored in the arrays?

    I'm really confused.
     
  6. Mich_9

    Mich_9

    Joined:
    Oct 22, 2014
    Posts:
    118
    Anyway I have another solution, I used it to create a ghost car that replays the player movement in his previous lap in a car game.
    Create a class containing that will hold the position data structure.
    Code (CSharp):
    1. public class Positions
    2. {
    3.     public AnimationCurve x = new AnimationCurve();
    4.     public AnimationCurve y = new AnimationCurve();
    5.     public AnimationCurve z = new AnimationCurve();//Remove if not needed
    6. }
    Create a variables that will store the player and enemy positions
    Code (CSharp):
    1. public Positions PlayerPositions;
    Then for the player save his current position and load them for the enemy.
    Code (CSharp):
    1. void FixedUpdate()
    2.     {
    3.             //Save player positions
    4.             Vector2 playerpos = player.transform.position;
    5.             float currenttime = Time.time;
    6.             PlayerPositions.x.AddKey(currenttime, playerpos.x);
    7.             PlayerPositions.y.AddKey(currenttime, playerpos.y);
    8.            
    9.             //Load positions for enemy
    10.             float x = PlayerPositions.x.Evaluate(currenttime - delay);
    11.             float y = PlayerPositions.y.Evaluate(currenttime - delay);
    12.             Vector3 newpos = new Vector2(x, y);
    13.             Enemy.transform.position = newpos;
    14.     }
     
  7. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    Depends on how the player is moved, but almost always the player will move each frame based on Time.deltaTime, which would absolutely cause the enemy to be farther behind him on lower framerates.

    Honestly, I think you're less confused than you think you are ;) If you are, you seem to be at least blindly guessing in all the right directions.

    You probably want it to be private, but that's a fairly minor detail. And you will probably have to 'new' it in Start/Awake, I don't think the compiler will let you initialize in the main body.
    Code (csharp):
    1. private Queue<TimePositionData> playerpos;
    2. void Awake() {
    3. playerpos = new Queue<TimePositionData>();
    4. }
    I would probably Enqueue the player's position in LateUpdate (so it gets the most current position data when it grabs it), and Dequeue in Update (so that anything else that looks at his position in LateUpdate will get his most current position, though I doubt this will matter).
     
  8. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    That's a nice elegant solution. I might worry about memory usage in that though if the level lasts a while - it will never remove old position data and will just keep building up over time.
     
  9. MarkHest

    MarkHest

    Joined:
    Jan 27, 2016
    Posts:
    6
    @Mich_9: That was an exelent solution. It works like a charm! Thanks so much for the help! And you too @StarManta! :)
     
  10. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    Have you considered how the old racing games used to do ghosts/replays? You can record the keystrokes the player uses per-frame and then relay those to the enemy. This way, you can delay the start as long as you like and he will always take the exact route the player went.

    What we used to do is store the keystrokes in a single byte and use bitwise operators to determine what button was pressed. (1 byte will obviously only allow for 8 different buttons since 1 byte = 8 bits). So instead of having a queue of Vector2s or objects like AnimationCurves, you can have a queue of bytes.
     
    ADNCG likes this.
  11. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    This is risky unless you do everything in your game in FixedUpdate, and if there is zero randomness in your game. Old games could get away with it because they didn't use an equivalent of Time.deltaTime; if the framerate slowed, the game slowed down, therefore whatever was going on was deterministic. Even a slight change can interfere chaotically and compound on itself. Even if you do use FixedUpdate, an off-by-one-frame error would be really easy to do and would screw the replay just as much. (Again, old games had this issue less; they wrote their engines themselves, and had a lot more direct control over the execution order of things.)

    Storing positions and time values (or storing them as animations, which is the same thing) are much more durable to small changes.
     
    Mich_9 likes this.
  12. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    Actually, you'll find most modern racing games etc. use the technique I described. It's just a matter of sending the commands to a controller class which interprets them then sends them to the enemy/ghost/whatever. The physics engine will take care of the rest!
     
  13. Mich_9

    Mich_9

    Joined:
    Oct 22, 2014
    Posts:
    118
    I would like to see an implementation of this from your part.
    This:
    The littlest fps spike can easily break the entire replay.
     
  14. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    To take a step back from the hypothetical discussion and back to the original issue: Recording keystrokes might be massaged to work with replays (even though IMO that'd be more trouble than it's worth), but it will absolutely not work for one character following another character with a delay. In a replay, it's possible to ensure that the world is in the same state when a given button is pressed, but not in the same world 2 seconds later. Imagine, for example, your character steps past a moving block. Another character, following your keystrokes exactly, would get stopped by this block.

    This is a problem you must solve with a stored-position approach too (unless you don't mind the follower passing through the block I guess), but that gives you the tools to solve it - if something blocks the follower that didn't block the player, the follower can attempt to return to the original path, and resume its pursuit from there. If you're storing only keystrokes, returning to the original path is fundamentally impossible, because that information no longer exists.
     
  15. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    What do you mean? If the movement is frame-rate independent, it won't matter.
     
  16. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483

    Well the OPs initial question involved not needing collision or gravity, which is why I suggested this option.
     
  17. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    OP said:
    (The doppelganger should not have gravity or collision).

    That doesn't mean that the same applies to the player, and to me seems to imply the opposite (as OP specified the doppelganger shouldn't have them). If the player walks against a wall for 5 seconds, the doppelganger would walk through it. That makes the case against using input recording.
     
  18. Mich_9

    Mich_9

    Joined:
    Oct 22, 2014
    Posts:
    118
    I took the time to show you exactly what really happens if we only rely on inputs in this case.
    File.
    Move the player around with the left/right arrow keys and jump with the Space key, you will notice how slowly the path of the enemy diverges from the player.
     
  19. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    I didn't say it did! All I have done is give the OP a simple solution to what HE described as a simple problem. The enemy only needs to collide with the player, nothing else. The player walking against the wall for 5 seconds would make the doppelganger bump into him, killing the player.
     
  20. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    That project works perfectly for me, I'm not sure what problem you were trying to express! Sorry if I've misunderstood