jueves, 10 de diciembre de 2015

Parallax Scrolling in Love2d. Part 2: Testing Collisions in different Layers

Hi all!

This is the second installment of the series on parallax scrolling for Love2d. In the previous post, we discussed how we could implement parallax scrolling in Love2d, by defining a camera component that was in charge of managing the different layers. In this post, we are going to provide further insight on this implementation and we are going to discuss how we can detect collisions happening between objects that belong to different layers. The gist here is noticing that we have two different, interrelated coordinate systems, and that in order to detect collisions, we must compare the positions of the objects in the same reference frame. 

Let's start by defining the two coordinate systems that we have, namely the camera coordinate system, and the world coordinate system. An object will have different values of its position in each system, although it is possible to move from one system to the other by simple mathematical operations. Let's assume that we follow up the example of the last post, in which we wanted the camera to track the player in the center of the screen. Figure 1 shows a diagram that relates the world coordinates with the camera coordinates of the player. We will focus only on the X dimension, but the same reasoning would apply to the Y dimension. Note that we define a world of 10000 x 10000 pixels, where the origin of such world is at the top left corner of the screen. The camera has a dimension of 1920 x 1080 pixels, which corresponds with the standard resolution of most screens today. Every object contained in the camera (the red rectangle) will be visible. As you can see, assuming that the position of the camera in the world is (x, y), the player will have different coordinates in camera space (960) and in world space (x + 960).

Figure 1. The player position in camera and world spaces 

In our example, we assume that the camera is updated as a consequence of the movement of the player (which is logical if we want the camera to track the player). Therefore, this is what happens in each frame:
  1. The player presses the right key, which increases the world position of the player by a rate depending on the speed we want the character to have. 
  2. As a consequence of the update on the character position, we have to update the world position of the camera, which is advanced the same units as the player has moved. That is, if the player has moved 50 units, the camera will move 50 units. The fact that the camera moves at the same rate as the player is due to the fact that the player belongs to a layer with scale = 1, as we will analyse further in the following.
Given that we update the camera when we update the player, we can consider that the world position* of the camera is a function of the world position of the player, and this function is as follows:

posCamera = posPlayer - 960 (1)

* From now on, we will simply say position when we refer to world position.

In general, if we have an object that belongs to a layer with a scale = n, the relationship between the position of the object in world and camera space is given by:

posObject_cam = posObject - posCamera * n (2)

In particular, think of the player, who belongs to a layer of scale = 1. From (1), we have that:
  
posPlayer = posCamera + 960

Substituting this in (2), we have:

posPlayer_cam = posCamera + 960 - posCamera * 1 = 960

As we can see, the position of the player in camera space does not depend on the position of the camera, because it is always the same: the center of the screen. Any other object that belongs to a layer with scale = 1 will meet (from (2)):

posObject_cam = posObject - posCamera (2a)

This means that the position in camera space of the object will change at the same rate that the camera moves. Consider for example a stationary object at position 50. If the camera initially is at position 0, then the object position in camera space will be 50. Now if the camera moves 10 units to the right, the position of the object in camera space would be 50 - 10 = 40. Moving the camera forward u units is equivalent to moving the object backward u units. In the case of the player, it is not a stationary object, but it moves at the same rate as the camera moves, and in particular, the position of the player in camera space is 960 because the difference between the position of the player and the position of the camera is always 960.

Consider now the case of stationary objects that belong to a layer with scale = 0. Then, we would have:

posObject_cam = posObject - posCamera * 0 = posObject (3)

This means that positions of objects that belong to these layers are not affected by changes in the position of the camera: their positions in camera space correspond always to their positions in world space. Or said another way: if a stationary object has the position 10 in camera space, it will keep that same position forever, being the net effect that the object moves at the same rate and same direction as the camera. 

Think now of stationary objects that belong to a layer with scale = 2. Then, we have:

posObject_cam = posObject - posCamera * 2 (4)

Let us suppose that the camera moves 10 units to the right. This means that the object in camera space moves -20 units (or 20 units to the left). In general, moving u units the camera to the right is equivalent to moving the object 2u units to the left.

Once we know how the position of any object in camera space is calculated, let's see something different. Now, we are going to calculate where the objects are actually drawn on the screen. The diagram in Figure 2 helps explaining this:


Figure 2. Object positions in different spaces

Let posObject_draw be the position where an object is drawn on the screen. Clearly, from the diagram above, we have the following:

posObject_draw = posCamera + posObject_cam 

, where posCamera in the X dimension is x.

And if we substitute posObject_cam from (2), we get:

posObject_draw = posCamera + posObject - posCamera * n (5)

, where n is the scale to which the object belongs.

Let us instantiate (5) for the player:

posPlayer_draw = posCamera + posPlayer - posCamera * 1 = posPlayer

This means that the player is drawn exactly in its world coordinates. If the player advances u units to the right, the player will be drawn these same u units to the right. In the case of stationary objects, this means that they will be drawn always in the same position. Therefore, if the camera advances u units to the right, in order for the object to be in the same position, the object will be drawn u units to the left. Note that this is consistent with the analysis performed on (2a).

If we instantiate (5) for a stationary object that belongs to a layer with scale = 0, then we have:

posObject_draw = posCamera + posObject - posCamera * 0 = posCamera + posObject

This means that the position where this object is drawn moves at the same rate as the camera and in the same direction. The net effect is that the position of the object is not affected by the position of the camera and the object seems to be still. This is consistent with what (3) expressed.

If we now instantiate an object that belongs to a layer with scale = 2, we would have:

posObject_draw = posCamera + posObject - posCamera * 2 = posObject - posCamera

This is expressing that moving u units the camera to the right causes the object to be moved u units to the left, being the net effect that the object is moved 2u units to the left, as the analysis in (4) concluded.

Once we understand how everything works, it is time to detect collisions among objects in different layers. The key idea here is that we must compare positions of the same type. Let us suppose that we want to check whether two objects A and B, belonging to layers with scales n and m, respectively, are colliding. We have two options:
  1. Compare positions in camera space.
  2. Compare positions where the objects are actually drawn.
In option 1, the steps would be as follows:

posA_cam = posA - posCamera * n
posB_cam = posB - posCamera * m
if (colliding(posA_cam, posB_cam)) then ...

In option 2, we would perform the following steps:

posA_draw = posA + posCamera * (1 - n)
posB_draw = posB + posCamera * (1 - m)
if (colliding(posA_draw, posB_draw)) then ...

Note that given that world coordinates of the objects do not account for the displacement that occurs each frame because of the parallax effect, we cannot use these world coordinates directly for the collision detection. For example, consider two stationary objects A and B that are initially placed in the same (world) position: 500, but A belongs to a layer with scale = 1 whereas B belongs to a layer with scale = 2. If we used the world positions to detect collisions between these objects and any other object C that moves toward them, we would detect that C collides with them at the same time, but this is a mistake. Actually, C will reach B in half the time that it will reach A, because B moves twice as faster than A in relation to the camera. In summary: we are using the world position only as an intermediate value to compute the actual position where the objects are drawn (or their position in camera space).

And this is all for now. In the last installment of the series, we will discuss how we can represent the same object in different camera spaces. This is useful for multiplayer games, where each player has a different view of the world.

Hope you found this interesting and see you soon!

No hay comentarios:

Publicar un comentario en la entrada