Friday, February 28, 2020

Managing the lifetime of child actors created from C++ OnConstruction methods in Unreal Engine 4

I have been working on a little example for my UE4 video series based on Space Invaders and designed to talk about some issues in object-oriented decomposition. Along the way, I ran into a technical problem that I know I had seen last summer, but it took me hours to find the solution. I may make a video about this later, but for now, I just want to get the basics explained here on the ol' blog.

The specific modeling problem I am dealing with is the swarms of enemies in Space Invaders. It's pretty clear to even the most novice programmer that "alien" should be an object. The Alien class would be an Actor, and it would be a great place to deal with things like collision detection and explosion effects. However, it gets a bit tricky when dealing with the movement patterns of the aliens: the whole swarm changes direction when one of them on the edge hits the side of the play area. This leads to a less obvious object-oriented modeling idea: that the swarm itself is another kind of Actor.

In my sample solution, I wanted to be able to create a swarm with any number of rows and columns and have it create that many aliens in a grid. In Blueprint, this is pretty easy to do. BP_Swarm has Rows and Columns variables that specify the size of the swarm. Its construction script can loop through all the "cells" of the grid and add a child actor of type BP_Alien for each. Presto, this allows you to drop a BP_Swarm object into a level and configure its placement and size.

The trouble arose when I decided I wanted to do this in C++ instead of in Blueprint. I added what seemed like the right code to the OnConstruction method. Here's the basic idea, leaving out for the moment the code that handles the individual placement of aliens:

void ASwarm::OnConstruction(const FTransform & Transform)
{
    if (AlienClass) {
        check(Columns >= 0 && Rows >= 0);
        for (int i = 0; i < Columns; i++) {
            for (int j = 0; j < Rows; j++) {
                UChildActorComponent* AlienChild = NewObject<UChildActorComponent>(this);
                AlienChild->RegisterComponent();
                AlienChild->SetWorldTransform(GetActorTransform());
            }
        }
    }
}

When I dragged a swarm into the level, it showed up, but its children persisted every time I moved the swarm or changed its parameters. This is clearly a different behavior than in the Blueprint construction script, but it wasn't obvious to me at all how to get that behavior to work in a C++ implementation. I remembered having come across it when working on the building logic in Kaiju Kaboom, but it took me hours to find it again. Indeed, I even posted to the Unreal Engine 4 Developers Community on Facebook, but the best answer I got was one I was aware of and left me unsatisfied: manually check for and destroy child actors at the beginning of each call to OnConstruction. After stepping away and searching again this morning, I found the forum post that I'm sure is the same one I tripped over last Summer—the post that mentions a barely-documented feature of UE4 called the component creation method. Here's the magic line that saved my ability to trust my memory:
AlienChild->CreationMethod = EComponentCreationMethod::UserConstructionScript;

EComponentCreationMethod has four possible values, and this one must be what is used internally by components created via Blueprint construction scripts. It aligns the behavior of the C++ implementation with the Blueprint one.

For reference, here's the full implementation of OnConstruction from my working demo. It uses a designer-configurable Spacing value that specifies the distance between aliens in the grid, and then it makes use of range mapping to translate from array index to geometric space. One other bit of weirdness here that I'll mention is that while UE4 is generally an "X-forward" system, with the X-axis pointing in the direction an actor is facing, UPaperSpriteActor by default has sprites facing in the negative Y direction. Hence, the horizontal placement of aliens is along the X axis and the vertical placement of aliens is in the Z.

void ASwarm::OnConstruction(const FTransform & Transform)
{
    if (AlienClass) {
        const int Width = (Columns-1) * Spacing;
        const int Height = (Rows-1) * Spacing;
        const FVector2D ColumnsRange(0, Columns - 1);
        const FVector2D RowsRange(0, Rows - 1);
        const FVector2D HorizontalRange(-Width/2, Width/2);
        const FVector2D VerticalRange(-Height/2, Height/2);
        const FTransform ActorTransform(GetActorTransform());

        check(Columns >= 0 && Rows >= 0);
        for (int i = 0; i < Columns; i++) {
            for (int j = 0; j < Rows; j++) {
                UChildActorComponent* AlienChild = NewObject<UChildActorComponent>(this);
                AlienChild->RegisterComponent();
                AlienChild->SetChildActorClass(AlienClass);
                const FVector Location(
                    ActorTransform.GetLocation().X + FMath::GetMappedRangeValueUnclamped(ColumnsRange, HorizontalRange, i),
                    ActorTransform.GetLocation().Y,
                    ActorTransform.GetLocation().Z + FMath::GetMappedRangeValueUnclamped(RowsRange, VerticalRange, j)
                );
                FTransform AlienTransform(ActorTransform);
                AlienTransform.SetLocation(Location);
                AlienChild->SetWorldTransform(AlienTransform);
                AlienChild->CreationMethod = EComponentCreationMethod::UserConstructionScript;
            }
        }
    }
}

One other note about this code, potentially for future-me who comes back to remember how I fixed this problem before. I had forgotten for a while to call RegisterComponent, which omission makes the whole thing a bit squirrely. Also, there is temporal coupling in the component's methods: calling RegisterComponent at the end of the loop creates strange editor-time behavior as well, whereas calling it immediately after NewObject gave the expected behavior.

Tuesday, February 18, 2020

The Poor Man's Gameplay Cue in UE4

I have some time this morning to collect my thoughts, so one thing I want to do is share a quick post here about something interesting I encountered a few days ago.

On Feb 13, Epic hosted a Live from HQ stream on Game Jam Tips and Tricks, which you can find archived on YouTube. In this stream, Michael Noland introduced a useful pattern for UE4 Blueprint programming that I had not seen or thought of before. He mentions that it is inspired by the Gameplay Abilities system, which I am sure I have tried to learn more than once, and each time I think, "This is amazing and also way out of scope for my project!" Then, I forget the important details and carry on. The Gameplay Abilities system is still (or again?) something I want to invest some time in understanding, and I'm hoping in part that by recording this smaller version here, I'll give myself and others an anchor to understand the more complex version.

At 54:15, Noland begins an explanation of why Sound Cues are a useful extension of normal Sound classes. I've been a fan of Sound Cues at least since working on Kaiju Kaboom, where I wanted variation on sound effect volume and modulation. He builds on this, starting around 56:26, explaining the value of decoupling game effects ("juice") from the core game rules. In particular, he mentions that certain blueprints will be high-contention, such as pawns, game modes, and player controllers. Separating the game effects from the other rules means that multiple people can be working simultaneously without version control conflicts, since these behaviors are now in different files.

After a few minutes, he ends up with a hierarchy like this (changing his naming scheme slightly to follow the Gamemakin Style that I prefer):

When the BP_StandardGameplayCue is spawned, it plays any sound, particle effect, or camera shake that has been set, checking via validated get. Noland points out that a slightly more robust system would have to account for who instigated the effect so that it could, to cite his example, do something like set that character on fire or give him a halo.

I hope that write-up is useful, if for nothing else than to help me remember this interesting pattern next time I'm working on a collaborative jam project or trying once again to make sense out of Gameplay Abilities. I wonder if I had known this a few weeks ago if I could have unleashed my son a bit more freely to work on visual effects in Disarmed.

Monday, February 3, 2020

Global Game Jam 2020: Disarmed

I hosted Global Game Jam 2020 last weekend at Ball State University, with generous cosponsorship and co-organization from the STEM Living Learning Community, Office of Housing and Residence Life. The theme, as announced in the keynote address, was Repair. I thought this was a good theme, being broad in interpretation yet clearly constrained.

My oldest son came with me again, and like last year, we planned on working together on a project. We have been enjoying Brief Battles and Towerfall recently, and so we talked before the event about trying to create some kind of local multiplayer competitive arena game. After hearing the theme (and taking care of a bit of site administration), we hit the whiteboards and started sketching ideas. My son pitched the idea that you were robots trying to repair yourselves, and you could use the things you pick up as weapons against the other robots. That became the core gameplay, and the rest emerged from there.

We called the result Disarmed. Here are the title and gameplay stills from our Global Game Jam project page:

The players click in from their controllers, and the game runs with a 60-second timer. You start with no arms, but you can jump to a nearby platform to grab a harpoon gun arm. That gives you three shots with which you can try to hit the other robots: a successful hit on an armed robot knocks its arm off, and a successful hit on a disarmed robot counts as a kill. After the 60 seconds are up, you get a report about how many deaths and kills you accumulated.

My son and I were using found audio, but right across the table from us was a composer working with just one other person on a project. On Saturday, I showed him what we were working on and invited him to create an original soundtrack, if he felt so inspired. His name is Nathan Cobel, and it turns out he's a professional musician and tutor. He ended up creating two pieces for the game: a short loop for the title screen and a longer one for in-game—pieces much nicer than the placeholders we had tossed in from browsing the Web.

We used Unreal Engine 4.24 to make Disarmed. My son ended up drawing all of the art using Piskel, which I had only shown him once before, many weeks ago. He quickly came to like it and became able to export sprite sheets and bring them into the engine. The only place where I really got my hands dirty in the artwork was in making a simple custom material to tint the single grey robot into the four different player colors. It may be worth mentioning that my son has made his own pixel art using Construct 2 in the past, and he's played a little bit of Dead Cells, but I'm sure he's never really studied pixel art. I don't know a whole lot about it myself, and in fact, I'm not really a fan of the genre. There were a few cases where I asked him to redraw some things to improve the contrast, and I think the final result is pretty good for a pair of guys who were winging it on the visuals.

In the project page, I checked the two diversifiers that matched this game. One is "!Coding", which is defined as " Don't write a single line of text to create your game. Only use game engine built-in features and visual scripting." I checked it even though I don't agree with it, since we technically followed it. However, it repeats a dangerous idea that visual scripting is somehow not coding. UE4 Blueprint is as much programming as any other programming language. Making a video about this is on my list for the semester. The other diversifier I selected was Protip, which is defined as "All members of your game jam team get 8 hours of sleep, 10 minutes of exercise and eats a meal that includes vegetables each day of the game jam event. Upload a brief development diary along with your game upload to document these actions." I checked that even though we technically didn't get it. First, we didn't upload a development diary, although maybe this description counts. Second, I know from talking to my son that neither of us slept eight hours the first night of the jam despite spending eight hours in our beds. If I had known that he also was not sleeping, we could have done some middle-of-the-night development! Since we live a ten-minute walk from campus, it was easy to get the exercise, and we do always make sure to eat our veggies.

I want to share a few technical notes about the game, in part so that others can understand how it works, and in part so that I can come back here and remember how it works :)

Local Multiplayer in UE4. In order to allow a player to "click in" to join the game, they have to already have a PlayerController created. There's always one player controller (number 0), and so the title screen game mode creates three extra controller objects. These controllers listen for the player to click in, and then tell the game instance to increment the number of players. This worked except that then when going into the main game mode, we always got four robots in the level because there were four player starts. Our solution was, just before leaving the title game mode, to remove some of the player controllers. This did the job, although we realized in testing that it's a bit sloppy. We should have kept track of which controller ID was clicked in, because our current approach is prone to a problem if there are more controllers plugged in than actual players: someone might "click in" on controller #4, then the game starts, and it removes controller #4, leaving that player high and dry. Aside from some of this management, local multiplayer was really a breeze to set up.

Physics vs Projectile Movement Component. The harpoons were launched by enabling physics on them and adding an impulse. This seemed to work fine when testing in editor, but when we launched the game from a built executable, we saw weird things happen: the harpoons would often get stuck in the robot who was firing them, despite the fact that our collision handling should not have let this happen. It wasn't until just before the end of the jam that I realized I could try to work around the problem by switching from physics to projectile movement component. This solved the problem, although I'm still unsure as to why we would see different behavior in PIE vs in the packaged game. The rule of thumb that I forgot was not to mix and match physics and non-physics ideas into the game.

Spreading sprites across an area. This is a pretty minor point, but it struck me as interesting. I have an object that is responsible for spawning one sprite onto the screen for each player, such as at the end of the game to show the scores. There's a horizontal range over which I want to spread the objects, and so I started trying to determine the X positions of each sprite algebraically. It dawned on me that this is really mapping from one range (player number, which goes from 0-3) to another range (X coordinates, in this case -300 to +300 from the position of the spawner). This can readily be done with the Map Range node. Here's an example from BP_RobotSelectionSpawner:
Like I said, it might be kind of obvious, but when you've been jamming for 40 hours already and your brain is a bit addled, it feels good to remember that if you think about a problem declaratively, you can solve it more elegantly than with ugly math nodes.

You can download Disarmed from the project page, although it will require 2-4 controllers and Windows to be playable.

I don't think I'll have the time to write up any more thoughts about the Jam, but I will say that we had the best turnout at our site in four years. There was a lot of energy in the room. I think that offering catered lunch on Saturday was a big influence in having people come back and work. I was very pleased to see the first analog game created at our site, too; it's something I've encouraged for years, but this is the first time one was finished.

Thanks for reading! If you have a chance to try Disarmed, let us know what you think. The folks who played it at the Jam seemed to really enjoy it.