Creating a dishwashing minigame in UE5

Hi everyone!

I’m attempting implement a little plate cleaning minigame where you drag a sponge around to clean dirt off a plate. I followed this tutorial (link) and learned about render targets, but I’m lost what to do next. I’ve found that changing the brush to pure white and the Clear Color of the Clear Render Target 2D to non-white makes a pretty desirable effect, but how can I change the clear color to a dirt texture? And how would I be able to check for completion?

Another approach I thought of was to use a decal projected onto the plate in a BP, but I don’t know how I would mask it out.

I only have a few months of experience in UE5. Any advice on how I could get this to work would be much appreciated! Thank you in advance!

output

Hi @_artescape,

That’s a cool tutorial! Here’s how you can build on top if it to make a dishwashing effect.



To get the plate to show either clean or dirty, you’ll use a Lerp node in the material that you’re drawing onto. You’ll still be drawing black images on a white canvas, but instead of rendering that directly, we’ll pipe it into Lerp to choose which of the clean or dirty textures to show at each pixel.


Calculating the surface area that’s clean is going to be a bit harder, since we’ll need to dive into C++ (I couldn’t find a way to do it in Blueprints). But we’ll keep the C++ to an absolute minimum - just one class with one function.

If you’ve got a Blueprint project, to add C++ in Unreal Engine 5.1, you can select “Tools” in the topnav, then New C++ class. I selected the parent class as “None.” At this point you’ll have to close and recompile your project with Visual Studio to keep going.

There will be two files - a .h file and a .cpp. I named mine DishUtil.h and DishUtil.cpp. The .h Header file will be:

#pragma once

#include "CoreMinimal.h"
#include "DishUtil.generated.h"

class UTextureRenderTarget2D;

UCLASS()
class QA_DISHWASHING_API UDishUtil : public UObject
{
public:
	GENERATED_BODY()

	UFUNCTION(BlueprintCallable)
	static float GetAverageGrayscaleColor(UTextureRenderTarget2D* Texture);

};

This just declares the things necessary to create a new node called GetAverageGrayscaleColor that you’ll need for it to show up in the engine.


And the .cpp file will be:

#include "DishUtil.h"
#include "Engine/TextureRenderTarget2D.h"
#include "ImageUtils.h"

float UDishUtil::GetAverageGrayscaleColor(UTextureRenderTarget2D* Texture)
{
	// Stick the raw data into an array
	// Need to make sure the Render Target 2D format is RTF RGBA8 - 4 bytes of data
	TArray64<uint8> RawData;
	FImageUtils::GetRawData(Texture, RawData);

	// Loop through all the Red pixels - those are ones where the indexes are divisible by 4.
	// We don't need the other colors, since we're working in grayscale anyway.
	// Only pulling the red pixels would be more performant, but FImageUtils::GetRawData isn't setup to do that in 5.1
	float Sum = 0;
	for (int i = 0; i < RawData.Num(); i += 4)
	{
		Sum += RawData[i];
	}

	// Get the average.  
	// We divide raw data num by 4, since we're only working with the red pixels.
	// The max number is 255 per red pixel, but we'll convert it to a 0 - 1 float scale, so we divide by 255 as a float.
	return (Sum / (RawData.Num() / 4)) / 255.0;
}

This actually does the work of getting the grayscale value by taking the average of all the red components of the pixels. Since we’re drawing in grayscale, we can skip doing math on the blue and green color channels, since they’ll be exactly same values for a nice little performance boost.

I know this works in Unreal 5.1, but I see some comments in the code implying they might change how it works in later versions - not sure if they did or not.


This code is also expecting the Render Target 2D to have the RTF RGBA8 format. That just means it’s going to have 4 bytes per pixel, in the order of Red, Green, Blue, and Alpha.



Once this is in your project, and you’ve compiled with Visual Studio, you’ll have access to a new node called GetAverageGrayscaleColor that you can plug your RenderTarget into. I tacked mine on to the end of the Drawbrush() function for simplicity. GetAverageGrayscaleColor will return a number between 0 and 1, where 1 means the texture is totally unpainted, and 0 means totally painted.


I then subtract that number from one, as I found it easier at this point to think of “Percent Clean” than “Percent Dirty”


I also divide by a hand-entered number of about 0.3. For the plate model I was using, it actually only has about a third of its texture area on the plate surface. The plate will look completely clean once the previous node is reporting about .3 (or 30%). I found this number just by printing out the value to the screen as I cleaned the plate and seeing what it said when I was done.

From there I just stuck some bells and whistles on like a progress bar.


Credit for the plate model goes to:

“Dirty Plate” (Dirty Plate - Download Free 3D model by DownRev [1bd8fe9] - Sketchfab) by DownRev is licensed under Creative Commons Attribution (Deed - Attribution 4.0 International - Creative Commons).

I tweaked the dirty texture to have a darker background, to distinguish it from the clean color.

Big thanks to DownRev for leaving us their dirty dishes :sweat_smile:

2 Likes

I can’t thank you enough for the detailed and helpful response! Unfortunately I probably should’ve clarified I’m using UE5.4. I’m a newbie programmer so all this C++ is crazy new to me! I attempted following your steps and ran into a WALL of errors, primarily this “exited with code 6” BS along with a variety of declaration and syntax errors.

Spent a few hours manually debugging with copilot and didn’t get far :frowning: Sadly I can’t find a way to successfully build with the code given, though of course, none of this is your fault! I just need to get better at programming to figure this out! I feel like there has to be some backwards workaround in blueprints to get a somewhat similar effect, but that’s for another day I suppose.

Thank you so much for the help regardless though! :smiley:

No worries - good on you for stepping out of your comfort zone to give it a try!

A different approach you could take that would work in blueprints: create a grid of invisible cubes in front of the plate. As you’re painting the pixels, also do line traces against the grid and destroy any cubes you hit. Then your progress will be equal to:

number_of_cubes_destroyed / starting_number_of_cubes

The smaller each cube is on the grid, the more accurate your progress will be, though it’ll also take more processing power.

Hope this helps!

That cube grid idea is genius! I spent a while trying to implement something like that but of course I’m still running into roadblocks lol. If you have any time to help (no worries if not!!), here’s what I’ve done:

I figured out how to spawn a grid of cubes in the construction script using this function:


Where I’m stumped is how to make it so each cube is individually destructible. I figured adding it to an array would allow me to target each one but I’m not sure how to do that. I’m using a line trace in my FirstPersonBP and then casting to the dishBP to trigger an event, but I’m not sure how to figure out which cube is hit.

Another little issue is getting the grid root to be on the corner not the center, but I’m doing my best to figure that out!

Any help would be super appreciated! Thank you again for everything you’ve helped me with so far!

I think I solved it!!! I used an interface to send the hit result from the FirstPersonBP to the dishBP and then destroyed the hit component and it’s working great! Now I just need to figure out how to change the root and calculate the total cubes!

1 Like

Nice work!

Total cubes would just be Cube Rows * Cube Columns. If you mean total cubes destroyed, probably easiest is just having an integer variable go up by one each time a cube is destroyed.

Thanks for the help! Here’s what I made and I’m super happy with it! I have two questions though that you might know the answer to :pray:

  1. How did you get the brush to be circular? When I change the BrushTexture to be anything other than pure white it just doesn’t do anything, no cleaning no nothing.

  2. How can I randomize the stain texture so each plate gets a random texture from a choice of many? I’d imagine I’d have to do something similar to the RenderTarget layer in the material but I’m not sure how to implement that…

Once again thank you so much for the help!! :smile:

Video:


Hm… I’m not sure what the difference would be there. In the tutorial you linked, he used an all-black square - are you using an all white one instead? I’m using a black one like in the tutorial. I just made a black circle png in GIMP with a transparent background. Here’s the png I used:

brush
If you’re using white instead of black, you could try inverting the colors and see if that works for you.


You can try temporarily plug your render_target directly into the base color (and saving it) to see if it’s coming out the way you expect. Could be useful for visualizing what’s happening - particularly if it doesn’t seem to be doing anything.


dish_random_dirt

Yep, you’ll follow a similar process.

  1. Make your random textures, and import them into Unreal.
  2. In your material Right click the dirty texture node, and Convert to Parameter. I gave mine the Parameter Name dirty_texture
  3. In your dish blueprint, after CreateDynamicMaterialInstance, drag off the “Return Value” and select promote to variable to make a new Material Instance variable. I named mine Dish Material.
  4. Make a new variable that’s an array of Textures, and fill it with the dirty textures you’d like to use
  5. Use the setup above to pull a random texture out of the array and assign it to the dirty_texture parameter of the material.

If this was helpful, you can mark one of the answers I submitted as the solution - it’d be a big help to me!

Everything is working flawlessly! I can’t thank you enough for the assistance. Have a great new year!!! :heart:

2 Likes

Very cool project!