Reworking the Compound Clouds (Tutorial)

Published August 01, 2018
Advertisement

Hello once again to this entry to our devblog.
This one will show the basics how we reworked the compound clouds to look - let's be honest - a lot better than they did before.
The last version consisted of one class CompoundCloud which was represented by a procedural mesh component. That one started out as a simple cube and then offset the positions of the vertices by a randomly chosen amount between two previously chosen values. That looked... functional at best. And interaction with the player was pretty much not practical and computationally expensive.
So the next idea we came up with was to make a cloud of sphere meshes and just have the player collect every one of those. This resulted in a new hierarchy.

At the top there's still the CompoundCloud but it now has an array of Compound_ParticleComponents for its representation. Each one of those has a sphere mesh  to handle collision and a particle system to look pretty.

A short demo of the new system can be found here.


We should probably start with the particle system. Not many steps requiered here.

1. Create a new particle systemparticleSystem_1.thumb.PNG.d18d28af38785aac8b760932ea76c7bc.PNG
 

2. Click on "Required" and in the first section ("Emitter") set a material (will be shown later) and the Screen Alignment to "PSA Rectangle" (this might not make a difference to the standard but it's set to that for our system). Then scroll down and set a cutout testure in the section "Particle Cutout". We used our heart icon you might have seen in one of our update videos. (we also changed the background color to something light so you can actually see what is happening)

particleSystem_2.thumb.PNG.1f3da966d34d447be132ba63e7202d73.PNG

3. Click an Spawn.
There's a little more to do here. First of all set the rate in the "Spawn" section to "Distribution Float Uniform". Same for the rate scale. Play around with the values a bit or just take what we got there.
Next set the particle burst method to interplated. The burst scale distribution should be "Distribution Float Particle Parameter". Once again play around with the values until you have something you like.

particleSystem_3.thumb.PNG.50defeddd2bbf7ee768e22a6e1a90355.PNG

4. Go into "Lifetime" and set Min to 2 and Max to 1.1 (Again, once you're done with this tutorial just play around with this values)

5. Go to "Initial Velocity" and reduce the Min and Max values. We recommend between 2 for all Max and -2 for all Min. This will keep your particles closer together and not have them shoot in one direction.

6. Next up add a seeded initial location and set its bounds to something you like.

particleSystem_4.thumb.PNG.4ab271e92385f48a9b592155f156131a.PNG

7. And the final step is to add a seeded sphere. You should now have something roughly looking like this.particleSystem_5.thumb.PNG.daab4e1da62f9077196775b7dbeb4c09.PNG

The Material: (absolutly nothing special)

M_CloudEmission.thumb.PNG.256057ee9664f870c1f4c7e9fd76b39b.PNG

 

Now for the interesing part: The code. This is perfectly doable in blueprints, too but since we're mostly working in C++ I'll show how we solved it in code. I'm also only gonna show the constructors of the CompoundCloud class and the Compound_ParticleComponent class since this tutorial mostly deals with the look of the clouds. If you're interested in how any other part works just let me know and maybe I'll make a short explanation for that in the future.

 

The code then:


uint8 particleCount = StaticMaths::RR(CLOUD_PARTICLE_MIN, CLOUD_PARTICLE_MAX);

We get a random value for the number particle systems we want to use. In our case this is our own function that simply determines a number between two other numbers.


std::string center = "CenterSystem";
UCompound_ParticleComponent_Cell* temp = CreateDefaultSubobject<UCompound_ParticleComponent_Cell>(FName(center.c_str()));
particles.Add(temp);
RootComponent = temp;

Then we set up the center component. All the other systems will circle around this one. This also functions as the rootComponent for the actor. The UCompound_ParticleComponent_Cell is the second class and we will deal with it later.


for (uint8 i = 0; i < 100; i++)
	{
		for (int j = 0; j < pow(i, 2); j++)
		{
			//define the name for the current particle system
			//ParticleSystem_<circleNum>_<Num>_<randomSeed>
			std::string name = "ParticleSystem_";
			name.append({ static_cast<char>((i + 1)) });
			name.append({ "_" });
			name.append({ static_cast<char>(j + 1) });
			name.append({ "_" });
			name.append({ static_cast<char>(j + i) });

			//create the particle system with the newly defined name
			UCompound_ParticleComponent_Cell* temp = CreateDefaultSubobject<UCompound_ParticleComponent_Cell>(name.c_str());
			particles.Add(temp);

			temp->SetupAttachment(RootComponent);


			//set up random location within a circle
			double a = (((double)rand() / (RAND_MAX)) + 1) * 2 * PI;
			double r = (CLOUD_RADIUS_STEPS * (i + 1)) * sqrt(((double)rand() / (RAND_MAX)) + 1);

			double x = r * cos(a);
			double y = r * sin(a);

			FVector location = FVector(x, y, 0);

			temp->SetRelativeLocation(location);

			//finally: check if number of elements in array is particle count
			//if so, stop this loop
			if (particleCount - 1 == particles.Num())
			{
				break;
				i = 100;
				j = pow(i, 2);
			}
		}
		if (particleCount - 1 == particles.Num())
		{
			break;
			i = 100;
		}
	}

This part is where the magic happens. Basically we want to have somewhat circular shapes around the center system. So the outer for-loop with i counts the circles. The 100 is a dummy value since there will never be that many circles and it would be a waste of resources to actually calculate the true number of circles. We only need to know the number of particleComponents which is our particleCount.

The inner loop with j counts from 0 to i to the power of 2. So on every circle there are i*i particleComponents.

Next up is a bit of naming. Not really relevant.
Then we create another particleComponent and add it to the actor and the array.
What comes next might be interesting for some: this formular basically determines a random position on a circle. So we take (i + 1) times our pre-defined cloud radius steps to get the radius of our current circle and we have all the data we need. Everything else can be determined from that and a random number.
Whe then set that location for the particleComponent.

At the end of the inner loop we check if we already have all the particles we need. If so set i and j to their max values so the loops stop. This is why we didn't need to calculate how many circles there will be when we start the loops.

 

Don't worry, the particleComponent involves less maths.


	//instantiate mesh component and particle system component
	particleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ParticleSystem"));
	mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	particleSystem->SetupAttachment(this);
	mesh->SetupAttachment(this);
	
	//Collision binding
	mesh->bGenerateOverlapEvents = true;
	mesh->OnComponentBeginOverlap.AddDynamic(this, &UCompound_ParticleComponent_Cell::BeginOverlap);
	mesh->OnComponentEndOverlap.AddDynamic(this, &UCompound_ParticleComponent_Cell::EndOverlap);

We create default subobject for the mesh and the particles system and then bind the collision functions. Easy as that.


	//get the mesh and set it
	auto meshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Game/Meshes/ball.ball'"));
	if (meshAsset.Object != nullptr)
	{
		mesh->SetStaticMesh(meshAsset.Object);
		mesh->SetVisibility(false);
		mesh->RelativeScale3D = FVector(5.f);
	}
	else
	{
		Logging::Log("Could not find Asset 'ball' at path in Compound_ParticleComponent_Cell");
	}

Then we load the mesh and set it. We also scales up the sphere mesh since it turned out to be way too small and the player would miss it a lot of the time. (That Logging::Log there doesn't really concern you. It's a function we wrote that simple writes a message into a file and onto the screen. Helpful for debugging. I left it in there for this tutorial because I think you should always have something in your code tell you when something goes wrong.)


//get the needed particle system and set it in the component
	try
	{
		//static ConstructorHelpers::FObjectFinder<UParticleSystem> psAsset(TEXT("ParticleSystem'/Game/ParticleSystems/PS_CompoundCloud_SingleCelled.PS_CompoundCloud_SingleCelled'"));
		auto psAsset = ConstructorHelpers::FObjectFinderOptional<UParticleSystem>(TEXT("ParticleSystem'/Game/ParticleSystems/PS_CompoundCloud.PS_CompoundCloud'"));
		if (psAsset.Succeeded())
		{
			particleSystemType = psAsset.Get();
			particleSystem->SetTemplate(particleSystemType);
		}
		else
		{
			Logging::Log("Could not find Asset 'PS_CompoundCloud_SingleCelled' at path in Compound_ParticleComponent_Cell");
		}
		//particleSystem->Template = particleSystemType;
	}
	catch (int e)
	{
		Logging::Log("Could not find Asset 'PS_CompoundCloud_SingleCelled' at path in Compound_ParticleComponent_Cell\nCause: FObjectFinder Access Violation");
		Logging::Log(e);
	}

We had some trouble loading particleSystems this way so I left in both ways to do it and the try-catch block in case one of you might have a similar problem.

So that's basically it. Bye and keep on evolving.

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement