Commit 14ac8cab authored by Nathan H. Bean's avatar Nathan H. Bean

Added parallax-scrolling

parent 8901d05c
baseURL = "https://cs.ksu.edu/~nhbean/cis580"
languageCode = "en-us"
title = "K-State CIS 580 Textbook"
staticDir = ["static"]
# Change the default theme to be use when building the site with Hugo
theme = "ksucs-hugo-theme"
......
---
title: "Introduction"
pre: "1. "
weight: 1
date: 2020-03-24T10:00:00-05:00
---
Parallax scrolling is a technique that simulates depth by scrolling background layers at different speeds. This mimics the perceptions we have of distant objects moving more slowly than nearby ones.
Think of the last time you drove down a highway - how quickly did you pass fenceposts along the road? How about houses set some distance from the road? Mountains or other landmarks in the far distance? The closest objects appear to fly by, while the most distant objects hardly seem to move.
This is exactly what parallax scrolling simulates in a video game. But rather than being completely accurate (as we would be with perspective 3D modeling), the parallax scrolling technique simplifies the background into a few layers that can be transformed together. This is what we'll build in this lesson.
I've prepared a starter project in which we will build this parallax scrolling project. Feel free to clone it from GithHub, here: [https://github.com/ksu-cis/parallax-starter](https://github.com/ksu-cis/parallax-starter). In the starter project, I have added a player class providing the representation of the player in the game. The player controls a helicopter with either keyboard or gamepad input and can move it around the screen. Also in the starter project game's content folder you will find the player's spritesheet and three textures representing a parallax background, midground, and foreground.
\ No newline at end of file
---
title: "Defining Sprites"
pre: "2. "
weight: 2
date: 2020-03-24T10:00:00-05:00
---
At its most basic, a parallax layer consists of a transformation which shifts images in the layer to the correct position.
There are several ways we could structure this. We could have a single transformation and image per layer, or we could share the same transformation amongst multiple images or even game objects. The latter has the benefit of a lot of flexibility with very little overhead, so let's adopt that approach here.
Since we need to draw multiple sprites in each layer, we'll need a means of collecting these sprites together. Thus, we need a common type that each aspect can be treated as. If we explore the documentation, the [IDrawable](https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb197416(v%3Dxnagamestudio.42)) interface looks _almost_ like what we want. But our textures are all rendered using a `SpriteBatch`, so we need this to be passed as an argument in our `Draw()` method. Let's come up with our own interface to meet this need:
## ISprite Interface
We'll call our interface `ISprite` and define it in the _ISprite.cs_ file. Basically, we just want a `Draw()` method which takes a `SpriteBatch` as an argument, and we'll add a `GameTime` argument as well to be consistent with the `IDrawable`:
```csharp
/// <summary>
/// Interface representing a sprite to be drawn with a SpriteBatch
/// </summary>
public interface ISprite
{
/// <summary>
/// Draws the ISprite. This method should be invoked between calls to
/// SpriteBatch.Begin() and SpriteBatch.End() with the supplied SpriteBatch
/// </summary>
/// <param name="spriteBatch">The SpriteBatch to draw with</param>
/// <param name="gameTime">The GameTime object</param>
void Draw(SpriteBatch spriteBatch, GameTime gameTime);
}
```
## StaticSprite
We then need classes that implement the `ISprite` interface that we can use in our layers. Let's start with a basic layer that does not change in any way - it just draws a static texture. Let's call it `StaticSprite` and define it in _StaticSprite.cs_. It should implement our new `ISprite` interface.
```csharp
/// <summary>
/// A class representing a texture to render with a SpriteBatch
/// </summary>
public class StaticSprite : ISprite
{
// TODO: Implement class
}
```
We need to know the position the sprite should be drawn at:
```csharp
/// <summary>
/// The sprite's position in the game world
/// </summary>
public Vector2 position = Vector2.Zero;
```
And we'll need to keep track of the `Texture2D` that it is drawing:
```csharp
/// <summary>
/// The texture this sprite uses
/// </summary>
Texture2D texture;
```
We can supply that texture through the constructor:
```csharp
/// <summary>
/// Creates a new static sprite
/// </summary>
/// <param name="texture">The texture to use</param>
public StaticSprite(Texture2D texture)
{
this.texture = texture;
}
```
Or both the texture and position:
```csharp
/// <summary>
/// Creates a new static sprite
/// </summary>
/// <param name="texture">the texture to use</param>
/// <param name="position">the upper-left hand corner of the sprite</param>
public StaticSprite(Texture2D texture, Vector2 position)
{
this.texture = texture;
this.position = position;
}
```
And finally, we'll need to implement the `Draw()` method, using the format supplied by `ISprite`:
```csharp
/// <summary>
/// Draws the sprite using the provided SpriteBatch. This
/// method should be invoked between SpriteBatch.Begin()
/// and SpriteBatch.End() calls.
/// </summary>
/// <param name="spriteBatch"></param>
/// <param name="gameTime"></param>
public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
{
spriteBatch.Draw(texture, position, Color.White);
}
```
Now we're ready to create the parallax layer to render these `ISprite` objects.
\ No newline at end of file
---
title: "The Sprite Layer Class"
pre: "3. "
weight: 3
date: 2020-03-24T10:00:00-05:00
---
We're now ready to create a class to represent our parallax layer. Since our parallax layer will need to implement an `Update()` and a `Render()` method, it makes sense to make it into a game component by extending the [DrawableGameComponent](https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb196397(v%3Dxnagamestudio.42)) class. This base class implements the [IDrawable](https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb197416(v=xnagamestudio.42)) interface we saw earlier.
Let's call it `ParallaxLayer` and save it in the _ParallaxLayer.cs_ file.
```csharp
/// <summary>
/// A class representing a single parallax layer
/// </summary>
public class ParallaxLayer : DrawableGameComponent
{
}
```
Since we want to potentially draw more than one item in the layer at a time, we'll need some sort of collection to hold thier references. This can be as simple as a `List<T>` of `ISprite`s:
```csharp
/// <summary>
/// The list of ISprites that compose this parallax layer
/// </summary>
public List<ISprite> Sprites = new List<ISprite>();
```
We need to keep track of the transformation for this parallax layer. This transform will most likely be a translation, so we _might_ store it as a `Vector2`, but ultimately we'll need a `Matrix`, so let's use that instead:
```csharp
/// <summary>
/// The transformation to apply to this parallax layer
/// </summary>
Matrix transform = Matrix.Identity;
```
Remember, the `Matrix` class is in the `Microsoft.Xna.Framework` namespace, so you'll need to add the appropriate `using` statement.
Now, we'll also want a `SpriteBatch` to render with. As we need to apply a transform, and this transform applies to all sprites in the layer, it makes sense to manage the `SpriteBatch.Begin()` and `SpriteBatch.End()` within this class. So let's add a private variable to hold a unique `SpriteBatch`:
```csharp
// <summary>
/// The SpriteBatch to use to draw the layer
/// </summary>
SpriteBatch spriteBatch;
```
The `DrawableGameComponent` class requires a `Game` instance to be passed to its constructor, so we'll need to implement our own constructor that takes a `Game` instance and passes it on. We also need to construct our `SpriteBatch` instance, which needs a `GraphicsDevice` instance - which just happens to be one of the properties of the `Game` object, so we can handle that in the constructor as well:
```csharp
/// <summary>
/// Constructs the ParallaxLayer instance
/// </summary>
/// <param name="game">The game this layer belongs to</param>
public ParallaxLayer(Game game) : base(game)
{
spriteBatch = new SpriteBatch(game.GraphicsDevice);
}
```
We'll want to provide a `Draw()` method that will draw all the sprites in our layer, so let's define that method next:
```csharp
/// <summary>
/// Draws the Parallax layer
/// </summary>
/// <param name="gameTime">The GameTime object</param>
public override void Draw(GameTime gameTime)
{
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, transform);
foreach(var sprite in Sprites)
{
sprite.Draw(spriteBatch, gameTime);
}
spriteBatch.End();
}
```
Essentially, we draw each sprite in the `Sprite` collection, applying the `transform` matrix to reposition them.
\ No newline at end of file
---
title: "Adding the Background"
pre: "4. "
weight: 4
date: 2020-03-24T10:00:00-05:00
---
Now that we have our `ParallaxLayer` class, let's add instances of it to our `Game1` class.
## The Background Layer
Let's start with the background layer. First, we'll need to import our three texture, in our `LoadContent` method:
```csharp
var backgroundTexture = Content.Load<Texture2D>("background");
```
Then we can create `StaticSprite` instance:
```csharp
var backgroundSprite = new StaticSprite(backgroundTexture);
```
And finally, create our `ParallaxLayer` instance:
```csharp
var backgroundLayer = new ParallaxLayer(this);
```
We'll add our sprite to it:
```csharp
backgroundLayer.Sprites.Add(backgroundSprite);
```
And set its `DrawOrder`. This property comes from the base class `DrawableGameComponent`, and is used to set the order game components are drawn in. The lower numbers are drawn _first_, so we want our background to have a low number:
```csharp
backgroundLayer.DrawOrder = 0;
```
Finally, we need to add the layer to the `Game1.Components` list:
```csharp
Components.Add(backgroundLayer);
```
By adding it to the list, we let the `Game1` instance take care of updating and rendering the `ParallaxLayer`. If you run your code now, you should see the background:
![Background Layer]({{<static "static/images/parallax-4.1.png">}})
But what has happened to our player helicopter? Let's look at our `Game1.Draw` method:
```csharp
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
spriteBatch.Begin();
player.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
```
Notice how we invoke the `base.Draw()` _after_ we have drawn our player? The game components in the `Game1.Components` list are drawn as part of that call, so the background is being drawn _after_ our player. We could fix this in a couple of ways.
One solution is to make the player a game componet as well, and set its `DrawOrder` to a larger number than the background, and add it to the `Game1.Components` list.
A second solution would be to make the `Player` class implement the `ISprite` interface, and add it to a `ParallaxLayer`. Let's do this second approach, and create our foreground layer, next.
\ No newline at end of file
---
title: "Adding the Player Layer"
pre: "5. "
weight: 5
date: 2020-03-24T10:00:00-05:00
---
Our player is currently hidden behind our background layer. One way to correct this is to place the player into a layer as well. In fact, we can do this with all the sprites the player will interact with; and this practice can help organize our code.
## Implementing ISprite
The first step is to make the `Player` class implement the `ISprite` interface. Refactor _Player.cs_ to add the interface implementation:
```csharp
public class Player : ISprite
{
...
```
The only other change we need to make is to make our `Player.Draw()` method match the signature of our `ISprite.Draw()`. The difference is the addition of a `GameTime` argument:
```csharp
public void Draw(SpriteBatch spriteBatch, GameTime gameTime) {...}
```
## Adding the Player Layer
Now let's add a layer for our player in `Game1.LoadContent()` (after the player is created):
```csharp
var playerLayer = new ParallaxLayer(this);
```
We can then add the player to its list of sprites:
```csharp
playerLayer.Sprites.Add(player);
```
And set its `DrawOrder` to a larger number than that of the background. We'll want to draw this between the midground and foreground, so let's use a value of `2`:
```csharp
playerLayer.DrawOrder = 2;
```
Finally, we need to add the layer to the list of components:
```csharp
Components.Add(playerLayer);
```
## Refactor Game1.Draw()
Since our `playerLayer` component will take care of rendering the player, we can remove the player rendering from our `Game1.Draw()`. It should now look like:
```csharp
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
base.Draw(gameTime);
}
```
Now when you run the game, you can once again see the player helicopter, but now over the background!
![Player over background layer]({{<static "static/images/parallax-5.1.png">}})
\ No newline at end of file
---
title: "Scrolling Layers"
pre: "6. "
weight: 6
date: 2020-03-24T10:00:00-05:00
---
Parallax scrolling is almost always tied to player movement - as the player moves, the layer scrolls in relation to the player's position. But we could also tie the scrolling to a timer function - and have it scroll at a set speed. Or we could apply it to a secondary target - something like a drone the player might launch. We might also use the scrolling effect with _no_ player, maybe as some kind of level preview, or for a cutscene.
How can we keep our parallax implementation flexible to meet each of these possibilities, while making it easy to use for the most basic approach of scrolling with the player? We could use a delegate, but we probably also need to keep track of some variables. Since we need both _data_ and a _behavior_, a class is a better choice. But we want the class to be swappable for other implementations, which means we need a common interface.
## The IScrollController Interface
Let's define a new interface, `IScrollController` to play this role. It should provide us with a transformation, and a means to update that transformation. Let's structure that as a `Transform` property and an `Update()` method which matches the signature of the `IUpdateable` interface that game objects use:
```csharp
/// <summary>
/// An interface for a parallax scrolling controller
/// </summary>
public interface IScrollController
{
/// <summary>
/// The current transform matrix to use
/// </summary>
Matrix Transform { get; }
/// <summary>
/// Updates the transformation matrix
/// </summary>
/// <param name="gameTime">The GameTime object</param>
void Update(GameTime gameTime);
}
```
Now we can turn our attention to a concrete implementation of this interface.
## Auto-Scroll Controller
Let's start with a simple controller that will scroll based on time. Let's define a class `AutoScrollController` that implements our `IScrollController` interface:
```csharp
/// <summary>
/// A controller that scrolls a parallax layer at a set speed
/// </summary>
public class AutoScrollController : IScrollController
{
}
```
We'll need to define a time variable to know how long we've been scrolling:
```csharp
/// <summary>
/// The time that has elapsed
/// </summary>
float elapsedTime = 0;
```
And a speed at which the layer should scroll:
```csharp
/// <summary>
/// The speed at which the layer should scroll
/// </summary>
public float Speed = 10f;
```
To fulfill the interface requirements, we need to implement a `Transform` property with a getter. We want this to apply our scrolling, so the speed times the elapsed time. The direction of the translation needs to be negative, so the scrolling moves to the left.
```csharp
/// <summary>
/// Gets the current tansformation matrix
/// </summary>
public Matrix Transform
{
get
{
return Matrix.CreateTranslation(-elapsedTime * Speed, 0, 0);
}
}
```
The second interface requirement is an `Update()` method. We'll use this to update our elapsed time:
```csharp
/// <summary>
/// Updates the controller
/// </summary>
/// <param name="gameTime">The GameTime object</param>
public void Update(GameTime gameTime)
{
elapsedTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
}
```
To use this controller, we'll need to update the `ParallaxLayer` class.
## Refactoring the ParallaxLayer class
The `ParallaxController` needs to have an instance of `IScrollController`; let's make that an `AutoScrollController` by default:
```csharp
/// <summary>
/// The controller for this scroll layer
/// </summary>
public IScrollController ScrollController { get; set; } = new AutoScrollController();
```
We'll use its `Transform` property, so we can get rid of the current `ParallaxLayer.transform` field. Remove the lines:
```csharp
/// <summary>
/// The transformation to apply to this parallax layer
/// </summary>
public Matrix transform = Matrix.Identity;
```
We need to tweak the `ParallaxLayer.Draw()` method to use the `ScrollController.Transform` instead:
```csharp
/// <summary>
/// Draws the Parallax layer
/// </summary>
/// <param name="gameTime">The GameTime object</param>
public override void Draw(GameTime gameTime)
{
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScrollController.Transform);
foreach(var sprite in Sprites)
{
sprite.Draw(spriteBatch, gameTime);
}
spriteBatch.End();
}
```
Finally, we want to ensure that our controller's `Update()` method is called. The `ParallaxLayer` is a game component, so it already has an `Update()` method defined in it's base class, and it is being invoked by the `Game` class. Let's override that method, and use it to update our controller:
```csharp
/// <summary>
/// Updates the ParallaxLayer
/// </summary>
/// <param name="gameTime">the GameTime object</param>
public override void Update(GameTime gameTime)
{
ScrollController.Update(gameTime);
}
```
Because we're already creating the `ParallaxLayer` in our `Game1` and adding it to the `Game1.Components` list, it should automatically update and render now. Go ahead and run the program - you should see the background scroll by, and if you don't move the player, it will slip off-screen!
Let's add our remaining layers next!
\ No newline at end of file
---
title: "Adding the Remaining Layers"
pre: "7. "
weight: 7
date: 2020-03-24T10:00:00-05:00
---
Now let's turn our attention to our remaining layers. The process will be _almost_ identical to our background, with one important difference. We'll use _two_ textures for the midground, and _four_ for the background
The reason for this is we want each successive layer to be larger than the one behind it. Remember, the background will move very little relative to the players position, while the midground, which is closer to the player, will need to scroll more. So it needs to be longer. Our background is 3500 pixels long, while our midground is 7800 pixels long. Similarly, the foreground is even closer to the screen, so it is even longer - 14,000 pixels.
## Big Bitmaps
The XNA framework limits textures to a maximum size of 4096 pixels on each size. That's a substantially large texture, requiring 64MB of video memory. But it still may be smaller than the worlds we want to work with. So how do we render game worlds that are larger than that limit? One possibility is to use tiles to break the world into small, repeatable chunks. We cover that approach elsewhere.
A second strategy is to break the world into _large_ chunks - just about as large as we can manage. This strategy is known as a _big bitmap engine_. Unlike tiles, which construct large worlds by repeating small tile images, a _big bitmap engine_ typically uses only a handful of non-repeated images. This can give the game's artwork a chance to really shine, and showcase art styles like digitized painting.
## Adding the Midground
Since our midground is too large for a single texture, we've split it into two 3,900 pixel textures, _midground1.png_ and _midground2.png_. We could load them into separate variables, but it might be cleaner to load them into a single array using initializer sytax (in our `Game1.LoadContent()` method):
```csharp
var midgroundTextures = new Texture2D[]
{
Content.Load<Texture2D>("midground1"),
Content.Load<Texture2D>("midground2")
};
```
Then we can create the `StaticSprite` objects to represent these two chunks. We can do this as an array as well:
```csharp
var midgroundSprites = new StaticSprite[]
{
new StaticSprite(midgroundTextures[0]),
new StaticSprite(midgroundTextures[1], new Vector2(3500, 0))
};
```
Remember that our second midground sprite needs to be positioned _immediately after_ the first, so we give it a position vector of &lt;3500, 0&gt;.
Next we need to create our parallax layer:
```csharp
var midgroundLayer = new ParallaxLayer(this);
```
Add the midground sprites to it:
```csharp
midgroundLayer.Sprites.AddRange(midgroundSprites);
```
Set its draw order to be larger than our background's:
```csharp
midgroundLayer.DrawOrder = 1;
```
We want this layer to scroll faster than the background, so we'll tweak its controller's speed. Before we can though, we'll have to cast it so that our compiler knows it is a `AutoScrollController`, and not an arbitrary `IScrollController`:
```csharp
var midgroundScrollController = midgroundLayer.ScrollController as AutoScrollController;
midgroundScrollController.Speed = 40f;
```
And finally add it to our game components list:
```csharp
Components.Add(midgroundLayer);
```
## Adding the Foreground
The foreground works almost exactly the same, except it has more textures and we'll want it to scroll faster. First we'll load the textures:
```csharp
var foregroundTextures = new List<Texture2D>()
{
Content.Load<Texture2D>("foreground1"),
Content.Load<Texture2D>("foreground2"),
Content.Load<Texture2D>("foreground3"),
Content.Load<Texture2D>("foreground4")
};
```
Then we'll put them into sprites. Rather than initailizing an array, this time we'll use a list to demonstrate how we could handle an arbitrary number of textures:
```csharp
var foregroundSprites = new List<StaticSprite>();
for(int i = 0; i < foregroundTextures.Count; i++)
{
var position = new Vector2(i * 3500, 0);
var sprite = new StaticSprite(foregroundTextures[i], position);
foregroundSprites.Add(sprite);
}
```
These textures are all 3500 pixels long, so we'll calculate the position based on that, and use the second `StaticSprite` constructor.
We can then create the `ParallaxLayer` and add the sprites to it:
```csharp
var foregroundLayer = new ParallaxLayer(this);
foreach(var sprite in foregroundSprites)
{
foregroundLayer.Sprites.Add(sprite);
}
```
We'll set the `DrawOrder` to be in front of the player layer:
```csharp
foregroundLayer.DrawOrder = 4;
```
And give it an even faster speed than the midground:
```csharp
var foregroundScrollController = foregroundLayer.ScrollController as AutoScrollController;
foregroundScrollController.Speed = 80f;
```
Finally, we can add it to the `Components` list:
```csharp
Components.Add(foregroundLayer);
```
## Adjusting the Player Layer Speed
We want our player layer to scroll as fast as the foreground, so let's set it to the same value:
```csharp
var playerScrollController = playerLayer.ScrollController as AutoScrollController;
playerScrollController.Speed = 80f;
```
Try running the game now. You should see all three layers scrolling, and you can fly your helicopter behind the telephone lines!
![All layers]({{<static "static/images/parallax-7.1.png">}})
\ No newline at end of file
---
title: "Scrolling With the Player"
pre: "8. "
weight: 8
date: 2020-03-24T10:00:00-05:00
---
One of the benefits of the `IScrollController` interface is that we can swap out the default controller for a new one. Let's do so now.
## Player Tracking ScrollController
A second common implementation of parallax scrolling is one where the screen scrolls as the player moves. Let's implement this one by defining a new controller named `PlayerTrackingScrollController` that follows our player. The class will need to implement the `IScrollController` interface:
```csharp
/// <summary>
/// A parallax scroll controller that tracks a player's position.
/// </summary>
public class PlayerTrackingScrollController : IScrollController