Lesson 63 - Multi-Dimensional Arrays and Procedural Platforms

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

In this lesson we're going to create platforms in the game world sort of procedurally. Now, procedural design is sort of a buzz word that you've probably been hearing a lot of lately. Well the idea's been around for quite a while, but nowadays it usually means a world that's being dynamically populated so that you're unlikely to get the exact same experience twice. Well by those standards, what we're going to be doing this lesson is not truly procedural but rather a simplification of the general idea, which is basically to set the conditions in code that allow for some sort of controlled randomness. That's essentially all that procedurally generating stuff in games is all about. So this will go into a bit of procedural design, as well as specifically how to create platforms with unique attributes like physics materials and something called effectors. I’ll show you all that in a moment, but first let's start by importing some assets. So we’ll create a new folder called World, sort of arbitrarily, and here we’ll import our asset images for platforms. So we have a rubber platform (you can probably guess how that's going to behave), a soft platform, and a stone platform. And as usual, we’ll set that to point.

Alright, so we’re going to want to create prefabs out of each one of these platforms as our base GameObjects that we’ll be using to instantiate from. So let's start off by creating temporary GameObjects for each of these prefabs. So we’ll say PlatformGrassy for this GameObject. Zero these values. From the background sorting layer, sort of for no particular reason, we’ll put it on order 25 to make sure it stands out. And we’ll give it a box collider, and we’ll say ‘Used by effector’ and all we’ll see this in action in a moment. Here, see, it gives a warning that we needed an effector, essentially, which we’ll do in a second. So first of all, size it by giving it this X and Y value. Now we'll have to add an effector, so we’ll add that, a Platform Effector 2D.

So we’ll use a collider mask, and set here what it collides with: the platform. So collide with our CheeseHead, as well as our eyeballs. Set this to zero for surface art. Actually surface art should be 10. And I'll show you in a moment what this will do. Basically what it will do is it will allow the CheeseHead to, or the eyeball for that matter, to pass through the platform going from the bottom up, so jumping upwards, which is a common mechanic in platforms, but not going from the top down. So in other words, we can land on the platform and jump up through it. So we can jump through it on the way up. That's the usefulness of the effectors. Actually, we’ll copy this the basis for the other GameObject. Actually, this should be zeroed out. And this will be the rubber platform. We’ll say PlatformRubber. So now what we're going to do is apply a physics material to this platform, which is a handy and easy way to add some behavior easily to a physics object. So in this case, bounciness So we have to create the material first and then add it to the box collider’s material slot here.

We’ll create a new folder, keep this organize, called Materials and I'm going to create a Physics 2D material, call this bouncy. And we’ll give it zero friction, and bounciness value of 1.1. You can experiment with this and see what it does how much more bounciness it gives it. And for testing, just show you how this will be, and we just assign this to the physics material for the box collider. There you go, it gives us an automatic bounciness. Pretty cool. And we’ll copy this, call this one PlatformStoney. So we'll give the stone platform, take away the bounciness. And for this one actually, we won’t use the effector because we just want it to be a solid platform. So just as we'd expect, getting stuck there. So as you remember maybe before, we turned this into prefabs. We’ll create a prefabs folder, and just click and drag to create our sort of bundled-up GameObject with all these components. So that one, this one, and the stone platform. Those are prefabs, and we can get rid of these now. We don’t need them.

So we’ll want to randomly instantiate these platforms whenever a floor GameObject is created. So let's create and attach to the floor GameObject a script called PlatformFactory, that w’ll actually create in Scripts > World folder. Say PlatformFactory. The first thing we'll do is grab a reference to these prefabs as a basis, that will randomize which one will actually instantiate. And we’ll set this in the inspector. So it's a StoneyPlatform. That's why marking this is public, so you can do it in the Inspector. GrassyPlatform, and RubberPlatform. And back in the inspector, the floor GameObject, we’ll put this. It’s going to want those GameObjects, so Prefabs folder, just click and drag Stoney, Grassy, Rubber, in no particular order. Now with this, we’ll set out to create a sort of grid-like set up of all the possible positions that a random platform can be instantiated in.

So to do this, we're going to use the simplest type of multi-dimensional array that is available to us. A two dimensional array. Sounds kind of scary maybe, but it's not it's actually pretty simple if you already understand arrays. A two dimensional array is not too difficult to wrap your head around. So basically all that means is that it's an array and at each index of that array, it stores another array. Hence two dimensions. And in that array, at those indexes also have their own indexes. You can visualize this actually much better than the way I'm describing it, as a common table-like structure. Like a spreadsheet or a multiplication table. And so we’ll be using this inherent structure of the array to sort of the actually have a visualization of it with our platforms taking up spots in each section or cell of that grid-like structure.

So for our multi-dimensional array, let's initialize it here in Start(). So this is how you initialize a multidimensional array, going to be an array of Vector3’s. Three possible positions, and so an ordinary array, we would have this sort of box-like looking syntax here, signifying a single dimensional array in other words. Whereas for a two dimensional array, we use a comma to signify two levels of the array. And if it was three dimensional, we add a comma and so on and so forth. So it maybe looks a little wonky, but that's not too hard to wrap your head around. So it's a two dimensional Vector3 array, you call it possiblePositions. We’ll assign to it or we’ll actually initialize the two-dimensional array to a Vector3 of five indexes on the first level arrays. And each each index of those five indexes has three indexes, right? So again, I'll give you a visual for this. It'll be easier to visualize it as a table- like structure.

And we’ll say float yPosition. I'm going to sort of hammer out a bunch of code. Not going to explain tot much of this. I’ll try to explain as much as I can. yPosition is -2f. And float xPosition is -3.6f. We’ll say int MaxRows = equals 5, MaxColumns = 3. We’ll set up in a for loop now to populate this array of possiblePositions. So we’ll say for(int i = 0; i < maxrows;="" i++)="" and="" then="" within="" this="" for="" loop,="" we="" have="" another="" for="" loop.="" so="" this="" is="" especially="" useful="" when="" doing="" things="" like="" multidimensional="" arrays.="" so="" it’ll="" look="" a="" little="" funny,="" but="" if="" you="" do="" this="" a="" couple="" times,="" you'll="" get="" used="" to="" this.="" this="" is="" just="" pretty="" much="" one="" way="" of="" doing="" it="" and="" the="" easiest="" way="" that="" i="" that="" i="" find="" is="" to,="" setting="" the="" values="" for="" multidimensional="" arrays.="">

So we’ll have for this for loop, int n.for the incrementer. So if n < maxcolumns,="" n++.="" and="" then="" here="" finally,="" what="" we’ll="" do="" is="" we’ll="" set="" possiblepositions,="" and="" this="" is="" all="" going="" to="" increment.="" so="" the="" first="" pass="" through,="" we’ll="" go="" once="" through="" this="" for="" loop,="" and="" it="" will="" be,="" say,="" 0.="" so="" first="" of="" all="" set="" 0,="" and="" then="" n="" will="" also="" be="" 0="" the="" first="" pass="" through.="" sorry="" i'm="" not="" going="" to="" write="" 0,="" i’m="" going="" to="" say="" i="" and="" n,="" because="" it’s="" going="" to="" change="" as="" it’s="" incrementing="" through="" the="" for="" loop.="" it's="" a="" new="" vector3="" thransform.position.x="" +="" xposition.="" so="" basically="" it's="" going="" to="" create="" platforms="" at="" consistent="" places="" on="" the="" screen="" relative="" to="" the="" platforms="" position.="" and="" you're="" going="" to="" have="" basically="" this="" grid-like="" structure="" that="" i'll="" show="" here.="" and="" that's="" what="" we're="" setting="" here="" with="" this="" code.="" so="" yposition.="" and="" 1="" for="" the="" z="" value.="">

So you can imagine as this is iterating through these for loops, the first time it goes through it’s set at the [0,0] index the Vector3 to this position. Whatever is calculated here. And then the next pass through is going to [0,1] is going to set another Vector3, and then [0,2]. Well I guess there's only three indexes there, so they'll move down or move back to this for loop. You'll have at the one index, [1,0], so possible solutions [1,0] will have another Vector3 and it’ll go through this iterative process until we're finally done with all those iterations. As it’s iterating will change the xPosition. We’ll set xPosition +=, and I’ll do a little ternary. So if xPosition is equivalent to 3.6f, which is basically the furthest platform to the right, that will reset it back to -3.6f. So we’ll just say += -7.2, which will reset it back to that. Kind of a funny way of doing it, but that’s the way I want to do it. So basically, it’ll be either -3.6 when it goes through one iteration, 0 as we’ll plus equal 3.6 on the next iteration. Then finally 3.6 after that. It will plus equal -7.2 to to bring us right back to -3.6 when it goes on to the next row? Yeah, platform row.

So I will do something similar, a similar type of thing with the yPosition, we’ll say plus equals 1.5f.Then we’ll have just a single dimensional array of platform GameObjects to choose from, so for that I will just say GameObject, an array GameObjects, that square syntax. We’ll say randomPlatforms. We’ll say equals new GameObject. And of course there’ll be three platforms in the array, and this will be a different way of initializing the array. This is called a a array -initializer, so it's an easier way of doing this. We could just set each index; zero, one or two, and we can just do it this way, have these curly brackets, and then for the zero index, put in StoneyPlatform, and for the one index. It's implied that this is the one index, and the other one is a zero index. GrassyPlatform for that one, and RubberPlatformfor the second implied index. That's an array initializer You can also do this with objects. An object initializer is pretty much the same kind of syntax. Just put a little comment here.

Alright, we're going to need Update(), we’re just going to to want to instantiate this one. So all we need is Start() every time a floor platform is being made, in other words. So now we're going to use one of two possible methods of creating these platforms. One is more patterned, the other one more chaotic. So we’ll start off with the patterned version first. Say private void CreatePatterned() and it will take in the multidimensional array, Vector3 array of possiblePositions. Give it a variable local to this that we pass in. The call, we’ll also want the array of GameObjects to determine exactly what kind of platform randomly we want to instantiate. So it's just going to have a simple algorithm, I guess you could say, to instantiate platforms randomly, but to favor a more patterned appearance. So we’ll kind of create like this dice roll effect. So we’ll start off with int percentChance. We’ll start off with 74. This will change as we're iterating through our. I'll say foreach Vector3, we’ll take the possiblePosition, so we’ll say foreach actualPosition in possiblePositions, we’ll roll the dice. Say int diceRoll = Random.Range(). This will give us basically a percentage chance, so say of 1 to 100 we’ll roll the dice. 100 sided dice I guess you could say. And if (diceRoll < percentchance),="" so="" it's="" less="" than="" 75="" at="" this="" iteration="" point,="" we’ll="" make="" a="" createdplatform.="" we’ll="" say="" gameobject.instantiate(),="" we’ll="" say="" randomplatforms,="" which="" is="" a="" an="" array.="" so="" there's="" three="" in="" the="" index,="" so="" we'll="" have="" a="" random,="" w’ll="" select="" a="" random="" one.="" so="" with="" random.range(0,3).="" so="" it’ll="" be="" 0,="" 1="" or="" 2.="" three="" is="" the="" exclusive="" member="" we="" pass="" that="" right="" into="" the="" method,="" right="" into="" the="" index.="" so="" it="" will="" return="" either="" zero="" one="" or="" two,="" and="" we’ll="" instantiate="" at="" the="" actualposition.="">

And we’ll have to give it a transform.rotation by default. And we can’t convert type Object to GameObject, so we’ll need to cast this as follows. Cast the Object that's returned to GameObject like that. That reference of the createdPlatform, we’ll use here and parent it to the floor GameObject. And we’ll decrease the percentChance for each created platform. So this sort of disfavors platforms that are created higher in the index, as it iterates through the multidimensional array. And actually, it will decrease regardless, it will decrease more if it's create the platform than if it didn't, but nonetheless it still will decrease the chance of platform create at higher levels by writing that outside of the conditional.

I made a mistake there. Random.Range() is there, used to be a method called a random range. And now create a more chaotic type of method. So private void CreateChaotic, and we’ll do the same thing here for input parameters. Actually, I got this backwards. This is the more chaotic one actually, so just rename these. So this is CreatePatterned() and the other one up here we just wrote is CreateChaotic(). Right, there's a more patterned kind of approach. We’ll say int patternCOunter = 0. Say int evenOrOdd = Random.Range(0,2). We’ll say foreach Vector3 actualPosition. I’ll just grab this, because I’m lazy. Foreach (actualPosition in possiblePosition), this time we’ll use this kind of way of instantiating platform. So if patternCounter % 2 == evenOrOdd. So this will be random, either 0 or 1, and patternCounter < random.range(5,15).="" this="" is="" going="" to="" be="" fifteen="" platforms="" in="" total,="" copy="" and="" paste="" from="" before="" same="" way="" of="" instantiating.="" take="" the="" patterncounter="" and="" increment="" it.="">

So if you look at how those methods working. You can probably pick it apart. I don't really want delve too deep into it. So now we’ll also randomly call one or the other of these two different methods of creating platforms. So weI'll do that over here under the array initializer. We’ll say if pattern, actually first we’ll say int patternOrRandom = Random.Range(1,4).We’re getting a lot of mileage out of this Random.Range() method. We’ll say if you patternRandom < 3.="" so="" this="" will="" favoring="" createpattern()="" method,="" because="" it's="" more="" likely="" that="" you'll="" have="" a="" random.range()="" that’s="" less="" than="" three.="" we’ll="" pass="" in="" possiblepositions,="" randomplatforms.="" else,="" we’ll="" createchaotic(),="" the="" same="" passed="" in.="">

Alright, so we’ll test this out. Make sure the platforms are properly randomizing. That must be the more chaotic type. Now you're not going to get what's happening, but each floor GameObject, it's copying over the previously instantiated platform, so that's no good. So we're going to need to change that before we move forward. So in Awake(), we’ll clear clear this out sort of with a clean slate and creat a platform. So void Awake() and just so it's obvious what's happening when we come back to look at this later, I'll make a private method for this called private void CleanSlate(), and we'll just iterate through those previously created platforms that were from the duplicated floor GameObject, and we'll just destroy them. So we’ll say foreach transform child in transform. Destroy(child.gameObject) and here we’ll just call CleanSlate(). Now it should never overlap with our previous platforms that we created.

The other thing we need to do is, we don't want platforms to instantiate when the game starts. So we’ll just simply wrap all this into a conditional. So all this in Start(), we’ll say if (transform.position.x != 0 ), which is basically the start of the game, then and only then do we actually create these platforms and go through all this code.

Alright so that's about it, we'll get everything wrapped up in the next lesson. So I’ll see 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