Lesson 59 - Infinitely Tiling Background Sprites

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

OK, well you saw how easy it was to get parallax scrolling in the previous lesson. Now we're going to do is something a little bit more tricky, which is make the background tiles so as we're moving along we'll be basically filling in, generating copies right before they're needed, so that we never really see the wizard behind the curtain of our illusionary game world, that there's actually stuff going on outside of what you're able to see with the camera. Then we'll also want to destroy these copies when they fall outside of the camera's scope. So this will be a little bit more involved coding wise but it should be pretty simple. I'll explain everything as much as I can, so first thing to do is set up a new script in the Parallax folder. Create a new script called BackgroundTile, and as before we'll attach these to the relevant GameObjects later.

Actually, before we write anything in this script, the first thing we'll write is this thing it's called an attribute. So we'll say RequireComponent(typeof())and we use the type of keyword of sorts in C#. So RequireComponent(typeof(SpriteRenderer)), which does exactly what it says. This is an attribute that we append to the top of our class or our script here, which basically it's like a header of sorts. And in this case it's going to require us to have a SpriteRenderer, in this case it's actually nothing required in order to make this script work. We didn't have to put in this attribute, I just want to show you this case and see it out there and it's got some handy functionality. Mainly in reminding us the developer that hey you know, if your GameObject doesn't have a SpriteRenderer, this is not going to be of much use. So that's why we're using it here. And anyhow let's get right to creating the fields. So we'll start with a private constant int called LookAheadOffsett, we'll that to 2. Also public bool HasrightCopy and public bool HasLeftCopy, and we'll right away say HasRightCopy= HasLeftCopy, so we'll assign them both here in the Start() to false. And we'll want a private float CamwidthX and a private float SpriteWidthX. And we'll want an enum to handle whether or not we're copying, creating a tile that's copied to the right or left hand side of the current Sprite in the camera's view. So for that we'll create an enum called CopyTo, we'll do it right here in this script. And another... Actually we can make it private, because no other script needs to access it.

So we can put it within this class. It's perfectly possible to go private enum CopyTo, so we're to creating an enum definition within a class definition and because private it's only going to be visible or usable within this BackgroundTile class. No other class is going to need this, so that's why we're doing it as a private enum within this class. And we'll have one of two possible states, copying to the right, not so much states in this case, or copying to the left. And we'll also assign handy numerical values for that, which we'll make use of. And in Update(), we'll say we'll have a float called spriteRightEdge, we'll assign to that transform.position.x+ SpriteWidthX/2. So this will give us the farthest right edge of the sprite in our camera's view. And float spriteLeftEdge equals the same thing, but minus instead of plus. And we'll compare this against, in a minute we'll compare them, against the camera right edge, so camRightEdge= Camera.main.transform.position.x+ CamwidthX. We'll get the right edge by comparing the camera's position relative to the Camwidth. And I just realized we didn't actually set these in Start() which is important so let's do that, these are actually very important.

SpriteWidthX is determined by using GetComponent() get the SpriteRenderer and bounds.size.x property will return the width of the sprite in question that this script is attached to. And for cam width dot x, or CamwidthX, we'll get the Camera.main.orpthographicSize* Camera.main.aspect. That's how we get the camera width. Yeah maybe this will make a lot more sense now. So yeah, we'll then get the camLeftEdge, store in that variable same thing, but minus. And I'll have a conditional here. So if (camRightEdge+ LookAheadOffset > spriteRightEdge). So basically it's kind of self-explanatory, but when the right edge of the camera when it finds it overlapping the right edge of the sprite. We're going to want to make a new copy, but we're also going to do this ahead of time with an offset so that it's not just in time but a little bit ahead of time.

So first we actually got to see if it has already has a right copy, so if (!HasRightCopy) if it doesn't already have a copy, we'll make a new copy. So for this we're going to need a new method we're going to have to create here. So let's say private void MakeCopy() and we'll take in a CopyTo enum, calling it side. I'll try to explain all this in a moment, right now we'll just focus on writing code. So we'll say Vector3. We're going to make a new copy, so Vector3 copyPosition= new Vector3(), we'll base it upon the current transform.position.x+ SpriteWidthX. That's what will replace the copy at the furthest edge of the sprite on either side. So multiply it by side, which is an enum so we'll just cast it to int. So either 1 or -1, right, when we cast to int, we'll use that value. We'll aslo have to pass in transform.position.y and transform.position.z, being a Vector3.

Now what we're going to do is we're going to need to specify the the GameObject essentially that we're copying to and or copying from. We're going to need both references, so here we're going to need two more fields. So it's going to be a transform, which I'll mention a little bit as to why later, called CopiedTo, we're using this current GameObject with this script as a basis for the other copy. So we're going to have a reference to what we copy to, and to that copy we're going to have a reference to where we copied it from. Which again this will be a little bit more clear as to why in a moment. And we'll create the new copy, so we'll say Instantiate() the transform, which will be basically a copy of this current transform which will include the GameObject, and copyPosition, and we'll give it a transform.rotation because it asks for one when it instantiates. And we're going to want to cast this to a transform, otherwise it's just an ordinary object. And then assign that to CopyTo, which is a transform.

So we're instantiating it and then we're immediately creating or assigning to this reference where this was copied to, right? And then we can go CopiedTo.GetComponent(), in the copy we just made in the line of code above. Grab it's BackgroundTile script and then set it's CopiedFrom reference to this.transform. So that's how we can keep these references as to what was copied from and to what. And also want to parent these GameObjects, say CopiedTo.parent= this, this keyword is not really necessary in these cases, I just think it makes a lot more sense when using it. So this.transform.parent. Otherwise we're looking at a lot of transforms, transform this transform that, and it's just a little handier to use this in this context. No pun intended. And with this conditional, now we will make use of the CopyTo enum.

So if (side == CopyTo.right) we'll say this.HasRightCopy, as well as the CopiedTo.GetComponent<BackgroundTile>.HasLeftCopy. They both equal true, right? That's a method. And say else if (side== CopyTo.left) we'll say this.HasLeftCopy as well as the HasRightCopy of the object we copied to, equal true. That should be good. Now we got to call this here. If we don't already have a copy, make the copy by calling the method. So MakeCopy() and here we can actually set the enum and pass it in within this input argument, or parameter. So we can actually say Copy= CopyTo.right. Because if this is the case and it doesn't already have a right copy, we're going to make a new copy to the right. And then here we'll do much the same but the opposite. Excuse me, I never did set the field for Copy here. Easy to forget.

So we'll say CopyTo Copy, for the field and we'll set that and pass it in right here. And then we'll do pretty much the same for the other side. We'll say if (camLeftEdge- LookAheadOffest < spriteleftedge),="" and="" if="" (!hasleftcopy)="" already,="" then="" we'll="" makecopy(),="" copying="" to="" the="" right,="" or="" to="" the="" left="" sorry.="" easy="" to="" get="" confused="" at="" this="" point.="" alright="" so="" just="" to="" explain="" a="" little="" bit="" about="" what's="" going="" on="" here,="" we'll="" be="" creating="" copies="" off="" of="" the="" current="" gameobject="" that's="" in="" scope="" that="" this="" script="" will="" be="" attached="" to.="" so="" in="" other="" words,="" the="" gameobject="" that's="" currently="" visible="" on="" the="" screen="" in="" the="" camera's="" bounds.="" and="" so="" that="" we="" don't="" create="" multiple="" copies="" in="" the="" same="" spot,="" we="" have="" to="" manage="" this="" by="" having="" each="" copy="" know="" whether="" or="" not="" it="" already="" has="" a="" copy="" to="" its="" left="" or="" right.="" we="" do="" that="" with="" the="" hasleftcopy="" or="" hasrightcopy="" bools,="" right?="">


So when we create the Copy, we use the reference to the transform we copied to, the CopyTo field, to set whether or not it was copied from the left or from the right basically. So we used that to set its HasLeftcopy and HasRightCopy bool respectively. The way I like to think of this is sort of like a line of communication between a mother and a daughter, the mother being in this case the GameObject that we base the copy off of, and the daughter being the GameObject that was created as a copy from the mother. So I guess that's kind of I can think of what's going on here with these references for CopyTo and CopiedFrom, right? Now before we attach these and run this, I realize I made a bit of an error with setting the HasRightCopy and HasLeftCopy bools to false in the Start() method. It needs to be set as false right at instantiation so it's not going to work properly. As a matter of fact, it might even cause Unity to hang, so just take out this reference, and in the fields set them as false.

Right there. Alright, that’ll fix that and now we can attach the scripts and see if our copies are going to be made properly. So we're going to want to attach it to pretty much every child object that we’re going to want to tile. That has a SpriteRenderer, remember needs that, it has that requirement. So put it there, and we’ll be able to see the status of the HasRight copy and HasLeft copy, because we made those public fields. Also add that to here. And here, here and here. And finally also the Floor. We don't add to the sky because it's stationary, so it will never need to be tiled, so otherwise we should be good to go and try this out. So as you can see there we already have some clones being made, copies. But as we move, we’ll notice that more and more are going to be created just before the camera extends to the point where it’s the sprite width on the right or left hand side. And there we go so. We see that these all have copies. Go to the left.

First I'll show you actually the scene, you can actually see all of these that have been instantiated. There are our clouds and our CheeseHead somewhere here. I guess it’s there hidden behind that icon. Yeah if we go to left they'll be much the same. So this is actually a bit of a problem if we're continuously going to left and to the right in our game. We're just going to create more and more GameObjects in memory that’ll eventually clog up memory and, you know they're no longer useful. So we're going to need to destroy them. So this will be a little bit tricky, but we can handle this in code with our CopyTo and CopiedFrom references. So make a new method say private void DestroyIfInvisible(). I continue my habit of explicit names for my methods and variables. And we’ll take in camRightEdge, camLeftEdge, the spriteRightEdge and spriteLeftEdge.

And here we’ll say a local bool which will evaluate to the right, so we’ll say IsSpriteInvisibleToRightOfCamer. It's a pretty long looking variable, huh? Well, it's descriptive nonetheless. And we’ll store this evaluation right into that bool. So spriteLeftEdge– camRightEdge, if it's greater than SpriteWidthX. And pretty much the same for the other side, IsSpriteVisibleToLeftOfCamera. So there is no doubt as to what I'm intending with this code. camLeftEdge, if camLeftEdge– spriteRightEdge > SpriteWidthX. And then we’ll, with the conditional, check out the status of that bool or those two bools. So first IsSpriteVisibleToRightOfCamera. If (CopiedFrom!= null) I'll go into this a bit more in a moment. If it’s not equal to null, CopiedFrom.GetComponent() get the BackgroundTile script and say HasRightCopy = false, because it's going to get destroyed.

So it has to notify the other GameObject to the left of this destroyed GameObject and say that it no longer has a right copy. And same for the other side, or I should say same for if it's the CopiedTo GameObject that we need to notify. If (CopiedTo!= null). We’ll say this, except the CopiedTo has to be notified. And now we’ll actually destroy the GameObject. So GameObject.Destroy(gameObject), the property for this GameObject. And for the other side we’ll say else if (IsSpriteInvisibleToLeftOfCamera)… I never did name that correctly, did I? Invisible. I knew something was wrong. And invisible. Not batting a thousand am I? Easily fixed nonetheless. Let’s say, well actually pretty much use the same code, but now on this side we’ll say if it doesn't have a left copy. And in Update(), we’ll need to call this method to destroy the gameObject that's gone out of scope.

So DestroyIfInvisible(), this will destroy the GameObject if it falls out of scope. So we’ll pass in the camRightEdge, camLeftEdge, spriteRightEdge and spriteLeftEdge. Right, we should be good to go now. It should be creating and destroying GameObjects as we're moving along in our game infinitely, right? So here we see that the CopiedFrom reference comes into play along with the CopiedTo reference. Now the way I wrote this code hopefully kind of speaks for itself. I certainly tried to write it as cleanly as I could, but when you read the conditional, the first conditional, that basically states “If the sprite is invisible to the right of the camera.” What was apparent at that point is that we need to have a way to tell the copy to its left that it will no longer have a right copy because it's fallen out of scope, right? As we're presumably moving to the left, then that copy will get destroyed.

But what's probably not obvious is whether or not this copy to its left was copied from this copy that's going to get destroyed or if it was copied to. Is the one on the left that needs to be notified of the copy on the right getting destroyed, a mother or a daughter? Referring back to what I was saying about that sort of communication relationship with those CopiedTo and CopiedFrom references. So imagine that we’re making copies moving left to right. We expect that the one to the left to be the mother, while if we're going right to left, we expect the one to the left to be the daughter. So we figure this out in code where we have the CopiedFrom not equal null, or CopiedTo not equal null references. If CopiedFrom isn't null, then we know that the one to the left is the mother, and she needs to be notified that she no longer has a daughter copy on the right.

That may sound a bit dark come to think of it, but anyways hopefully that as a little helpful in understand what’s going on. So we know how this is the case because the CopiedFrom, the mother, would be null if it was copied from the right. Null because it would have already been destroyed being even further to the right of the camera in this case. But since it's not null, we know that the mother is to the left of this daughter that's going to get destroyed. And we then use this CopiedFrom reference to tell the mother it no longer has a daughter on the right. If that is making any more sense. Now that I'm hearing this coming of my mouth I'm not so sure it is. But at any rate, same thing with CopiedTo that's not null, that we know that the GameObject to the left is a daughter and needs to be notified that the mother to its right is falling out of scope.

So obviously it's kind of easy to get the logic all twisted up with this, so don't take it to heart if you're not seeing it. I honestly I have to debug this for a couple of hours before I got it working just right and making sense even to myself. So anyways that's my explanation as to how this is all working managing the copies that are getting destroyed. So we’ll keep an eye on these GameObjects as we're moving about our world, making sure that they're getting created and destroyed. So at some point we should expect to see GameObjects drop out and the ones get created. Awesome. Alright, now just one little final thing to add for a little bit of visual intrigue, is to kind of have a, not a day/night but just an overcast actually effect to sort of darken and lighten certain elements. So it's easily enough done. So let's just create a couple scripts here to facilitate that.

So in Scripts, in Parallax, we’ll create a script called DarkenCycle. Just write some quick code here, I think it’ll be pretty self-explanatory, so we’ll write this fairly quickly. We’ll get the renderer. I will set… Well actually we don't need it to be public because we'll set this in the Script. So we’ll just say float minBrightness, float maxBrightness and float cycleSpeed. And for the Render, we’ll say GetComponent<SpriteRenderer> () And we’ll set the minBrightness to 0.86f, maxBrightness to 1, and cycleSpeed. And in Update(), the script will be affecting with Color.Lerp(), the color property of the SpriteRenderer in question. We’ll do this by doing Color.Lerp() and it’s easier to do this on a separate line, so we’ll just say new Color(), a Lerp for maxBrightness for each value, which is 1.

Could’ve just wrote 1 I suppose, but it's the way I'm doing it. And that's the opacity, just so it's clear that we don't want to affect that. I’ll actually write 1 for that, and then new Color() we’ll Lerp to the minBrightness for all these for these R.G.B. properties essentially, of a color and property for the SpriteRenderer. And to make this happen, instead of the usual way of Lerping from you know 0 to 1, we’ll use this Mathf.PingPong, it's kind of how it sounds it's just going to go back and forth from 0 to 1, 1 to 0, 0 to 1, 1 to 0. So that's what will create the cycle effect. So we’ll pass in Time.time, * cycleSpeed, and it’ll go from… And we’ll pass in maxBrightness. And we of course have to assign the output of that Lerp to the Renderer.

Actually, the color property of the Renderer. We’ll want to attach this to this GameObject, 2_mound and 3_hills of the BG3_Close, we’ll want those to darken. So we’ll do the same thing with the sky. But a little bit differently. this time with opacity. Same principle though. So create a new script for this, call it OvercastCycle, without any explanation really needed I'll just hammer this out. Renderer. minOpacity, float maxOpacity and cycleSpeed. Renderer= GetComponent<SpriteRenderer> () minOpacity is 0.6f, maxOpacity= 1. cycleSpeed is the same as the other script, .05f, so this pretty much syncs up. This will be going on the sky background, so we'll use that just for a little difference to how we did it with the darkened cycle.

And in Update(), we’ll do this Color.Lerp() yet again. Color.Lerp(), we’ll say new Color(1, 1, 1) and we're modifying max, or modifying opacity at this time, so we’ll pass in from maxOpacity we’ll Lerp to 1, 1, 1, to minOpacity, and we’ll want to do again the Mathf.PingPong with the same passed in as before. The cycleSpeed, maxOpacity. And this will be opaque so we'll actually be able to see a little bit, it’ll change the color a little bit relative to this background color. So I want to keep it at this value. I guess the default value. And we should be good to go, let’s try it out. It’s going to be subtle, it’ll add a little bit of extra visual appeal perhaps.

See here, if the sky is in fact… it's not. Oh, I forgot to assign this to the Renderer. Silly me. Now it should work. And yeah it will be subtle, but it’ll just give a little bit of visual appeal I think. The sky becomes a little opaque. Kind of looks interesting, you know it's a little darker. Right, now we’ll cycle and we have our game world pretty much now, our scrolling platformer. Right so that's about it for this lesson. In the next lesson we'll look at creating enemy classes with a bit of an object-oriented approach. I’ll see you there.


Related Articles in this Tutorial:

Lesson 1 - Who This Course is For

Lesson 2 - What to Expect from this Course

Lesson 3 - Installation and Getting Started

Lesson 4 - Starting the First Project

Lesson 5 - Prototype Workflow

Lesson 6 - Basic Code Review

Lesson 7 - Game Loop Primer

Lesson 8 - Prototyping Continued

Lesson 9 - C# Fundamentals and Hello World

Lesson 10 - Variables and Operations

Lesson 11 - Variables and Operations Continued

Lesson 12 - Floats, Bools and Casting

Lesson 13 - If Statement Conditionals

Lesson 14 - If Statements Continued

Lesson 15 - Complex Evaluations and States

Lesson 16 - Code Syntax vs. Style

Lesson 17 - Variable Scope

Lesson 18 - Object-Oriented Programming Intro

Lesson 19 - OOP, Access Modifiers, Instantiation

Lesson 20 - Object Containment and Method Returns

Lesson 21 - "Has-A" Object Containment

Lesson 22 - "Is-A" Inheritance Containment

Lesson 23 - Static Fields and Methods

Lesson 24 - Method Inputs and Returns

Lesson 25 - Reference vs. Value Types

Lesson 26 - Introduction to Polymorphism

Lesson 27 - Navigating the Unity API

Lesson 28 - Applying What You've Learned and Refactoring

Lesson 29 - Constructors, Local Variables in the Update Method

Lesson 30 - Collecting Collectibles, Items and Powerups

Lesson 31 - Spawning and Managing Prefab Powerups

Lesson 32 - Implementing Powerup State Logic

Lesson 33 - Displaying Text, OnGUI, Method Overloading

Lesson 34 - Referencing Instantiated GameObjects, Parenting

Lesson 35 - Understanding the Lerp Method

Lesson 36 - Creating Pseudo Animations in Code

Lesson 37 - Understanding Generic Classes and Methods

Lesson 38 - Animations Using SpriteSheets and Animator

Lesson 39 - Working with Arrays and Loops

Lesson 40 - Debugging Unity Projects with Visual Studio

Lesson 41 - Camera Movement and LateUpdate

Lesson 42 - Playing Audio Clips

Lesson 43 - Routing Audio, Mixers and Effects

Lesson 44 - Adding Scoring Mechanics and Enhancements

Lesson 45 - Scene Loading and Game Over Manager

Lesson 46 - Understanding Properties

Lesson 47 - Controller Mapping and Input Manager

Lesson 48 - Understanding Enums

Lesson 49 - Dealing with Null References

Lesson 50 - Handling Variable Framerates with time.DeltaTime

Lesson 51 - Preparing the Project for Final Build

Lesson 52 - Final Build and Project Settings

Lesson 53 - Introduction to the Unity Physics Engine

Lesson 54 - Understanding FixedUpdate vs. Update

Lesson 55 - Movement Using Physics

Lesson 56 - Attack Script and Collision Events with OnCollisionEnter2D

Lesson 57 - Projectiles and Stomping Attack

Lesson 58 - Parallax Background and Scrolling Camera

Lesson 59 - Infinitely Tiling Background Sprites

Lesson 60 - OOP Enemy Classes

Lesson 61 - OOP Enemy Classes Continued

Lesson 62 - Trigger Colliders and Causing Damage

Lesson 63 - Multi-Dimensional Arrays and Procedural Platforms

Lesson 64 - Finishing Touches

Lesson 65 - Series Wrap


Comments

Please login or register to add a comment