Tuesday 21 June 2016

Using "ghosts" to create a "wrapped" world

Ok the title of this post could be a little misleading.

What I want to show you with this small project is how we can create a wrapping effect for our player in a 2D environment. And what I mean by that is I'm trying to achieve the "Pac-Man" effect or the "Asteroid" effect: the player exits the screen from one end, reappears from the other one.

These are the assets used: tiles and the racoon.

Here is a screenshot of the effect I'm talking about:


As you can see by my beautiful arrows, the character is leaving the screen to the left and coming back in the screen from the right side.

To achieve this effect we use ghosts.

What are ghosts you say? Well they are nothing more than copies of the player object which move in relation to it outside of the visible screen.

If we look at the scene, this is what is actually happening:



The "real" player character is the one in the red circle, the other ones are just copies of it.

For this particular project I used only 3 ghosts, you can have up to 8 if you want the effect to work for all directions. I didn't want, for example, the character to pop out from the bottom whenever I jumped from the very top platform. That was a choice I made, which is not necessarily what you might want for your game.

We can achieve this with one simple script:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class Ghosts : MonoBehaviour {

 float screenWidth;
 float screenHeight;

 Vector3 topLeft;
 Vector3 bottomRight;

 GameObject[] ghosts;
 Vector3[] ghostPosition;

 // Use this for initialization
 void Start () {

  topLeft = Camera.main.ViewportToWorldPoint(new Vector3(0,1,transform.position.z));
  bottomRight = Camera.main.ViewportToWorldPoint(new Vector3(1,0,transform.position.z));

  screenWidth = bottomRight.x - topLeft.x;
  screenHeight = topLeft.y - bottomRight.y;

  ghosts = new GameObject[3];
  ghostPosition = new Vector3[3];

  ghostPosition [0] = new Vector3 (screenWidth, 0, 0);
  ghosts[0] = Instantiate (this.gameObject, transform.position + ghostPosition[0], this.gameObject.transform.rotation)as GameObject;

  ghostPosition [1] =  new Vector3 (-screenWidth, 0, 0);
  ghosts[1] = Instantiate (this.gameObject, transform.position + ghostPosition[1], this.gameObject.transform.rotation)as GameObject;

  ghostPosition [2] =  new Vector3 (0, screenHeight, 0);
  ghosts[2] = Instantiate (this.gameObject, transform.position + ghostPosition[2], this.gameObject.transform.rotation)as GameObject;

 foreach (GameObject g in ghosts) {
   DestroyImmediate (g.GetComponent<Ghosts> ());
   DestroyImmediate (g.GetComponent<Rigidbody2D> ());
   DestroyImmediate (g.GetComponent<BoxCollider2D> ());
   DestroyImmediate (g.GetComponent<Movement> ());
  }
 
 
 }

 // Update is called once per frame
 void Update () {

  CheckGhostsPosition ();
  PlaceGhosts ();
 
 }

 void CheckGhostsPosition()
 {
  foreach (GameObject g in ghosts) {
   if (g.transform.position.x < bottomRight.x &&  g.transform.position.x > topLeft.x &&
    g.transform.position.y > bottomRight.y && g.transform.position.y < topLeft.y)
    this.gameObject.transform.position = g.transform.position;
  }
 }
 void PlaceGhosts()
 {
  int i = 0;
  foreach (GameObject g in ghosts) {
   g.transform.position = transform.position + ghostPosition [i];
   g.transform.rotation = transform.rotation;
   g.transform.localScale = transform.localScale;
   i++;
  }

 }
}

The character gameobject also has 2 other scripts attached, one for the movement and one for the animation. I have separated movement and animation for a reason which I will explain shortly.

To begin with, we need to get the screen width and height in world space: this is what happens from line 15 to 19. I get the bottom right and top left corner position of the screen in world space coordinates and I calculate the width and the height of the viewport.

This is done because the ghosts, for example, the 2 side ones will be placed a screen width distance away from the main character.

From line 24 to 31 I instantiate the 3 ghosts, store them in an array and place them accordingly. I also store their position in a Vector3 array so it makes it easier to iterate their position later in the script.

The foreach loop on line 33 is used to eliminate all unnecessary components from the ghosts I just instantiated. First of all, the Ghosts script that we are writing right now: we don't want the new instances to create new ones themselves or this will result in a infinite amount of ghosts!

I eliminate the Rigidbody2D  and the CircleCollider2D as I don't want the ghosts to bump into things while off screen. Also, they don't need the Movement script, as they will be placed around the scene from this script. The only component they need is the CharacterAnimation script, which simply handles their animation just like the main character.

The CheckGhostsPosition() method is called every frame and it checks for the position of every ghost. If one of them happens to be within the screen, the main character will be immediately "teleported" to that position so that the ghosts will be again placed outside the visible screen.

Finally the PlaceGhosts() simply iterate through all the ghosts and place them according to the main character position.

I will post the other 2 script, CharacterAnimation and Movement, however, I will not go through them
because, well....they are so simple.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CharacterAnimation : MonoBehaviour {

 Animator anim;

 // Use this for initialization
 void Start () {

  anim = GetComponent<Animator> ();
 
 }
 
 // Update is called once per frame
 void Update () {

  float x = Input.GetAxis ("Horizontal");
  anim.SetFloat ("Speed", Mathf.Abs(x));


 }
}



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Movement : MonoBehaviour {

 public float runSpeed = 5;
 public float jumpForce = 50;


 Rigidbody2D rb2d;
 CircleCollider2D cc2d;



 bool isFacingRight = true;
 public bool grounded;

 // Use this for initialization
 void Start () {

  rb2d = GetComponent<Rigidbody2D> ();
  cc2d = GetComponent<CircleCollider2D> ();
 
 }
 
 // Update is called once per frame
 void Update () {

  float x = Input.GetAxis ("Horizontal");

  MoveChar (x);    

  if (Input.GetKeyDown (KeyCode.Space) && grounded) {
   rb2d.AddForce (new Vector2 (0,jumpForce),ForceMode2D.Impulse);
   grounded = false;
  }
 }

 void MoveChar(float x)
 {
  Vector3 nextPos = new Vector3 ( x * runSpeed * Time.deltaTime, 0,0);

  transform.position = transform.position + nextPos;

  if (x > 0 && !isFacingRight || x < 0 && isFacingRight)
   Flip ();

 }

 void Flip()
 {
  transform.localScale = new Vector3 (transform.localScale.x * -1, transform.localScale.y, transform.localScale.z);
  isFacingRight = !isFacingRight;
 }

 void OnCollisionEnter2D(Collision2D c)
 {
  if (c.collider.tag.Equals ("Ground"))
   grounded = true;

 }




}


An example scene is playable here.