Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
Bongo Bill
Jan 17, 2012

Tunicate posted:

... assuming all sprites and cameras are internally aligned with exact pixels, so when rounding occurs for where they are positioned on your small canvas it is always 100% consistent, so characters never shift at different times than the ground beneath them

Right, yes, gotta be careful about how you handle rotation in this approach too. Though some older consoles could rotate sprites and background layers, so if the idea is fidelity to some aspect of retro aesthetics, rotation artifacts would be authentic.

Adbot
ADBOT LOVES YOU

Raenir Salazar
Nov 5, 2010

College Slice

TooMuchAbstraction posted:

I've been thinking about a problem lately, and it occurs to me that there may be a well-studied solution for a variation on the problem. The problem is "how do I make high-level Metroidvania maps that adhere to certain constraints", with "high-level" meaning that I don't care about the contents of individual rooms, just how they connect to each other and how big they are. I think this could generalize to "how do I make a planar graph that adheres to certain constraints", where the constraints are things like "there is a length-5 path between these two nodes", "there are two paths between these two nodes", "the path from this node to that node must pass through this third node", etc. Does anyone know of any such algorithms, or any resources I could read up on to get a better understanding of the domain?

I've made several stabs at this problem in the past, and had a moderate degree of success, but all of my approaches boiled down to "throw a bunch of stuff at the wall and then try to make sense of it." I'd like something a bit more formal.



Wait isn't the problem for solving constraints like that basically just the Wave Function Collapse algorithm?

Raenir Salazar fucked around with this message at 15:58 on Mar 29, 2023

KillHour
Oct 28, 2007


Raenir Salazar posted:

Wait isn't the problem for solving constraints like that basically just the Wave Function Collapse algorithm?

Wave Function Collapse is an example of a backtracking algorithm, where you pick things randomly until you run into an impossible situation and then undo. It works well with very local constraints (this must be next to this) but poorly with global constraints (there must be exactly 3 paths between these areas).

Raenir Salazar
Nov 5, 2010

College Slice
Maybe I interpreted it incorrectly but I was thinking like if you had a level node with three connections and this can only connect with a node with three or more or exactly one connector I. E a dead end, you could get something that kinda but maybe more loosely gets you what Abstraction wants.

Which you could combine with higher level constraints which it accounts for in how it picks between possible nodes to fit its neighbours.

E: thinking on it you can absolutely even do things like "have exactly five lengths of (subnode) between two specific nodes" because the start node, end node, and all the in between nodes have rules that basically can only result in that outcome.

Raenir Salazar fucked around with this message at 16:39 on Mar 29, 2023

TooMuchAbstraction
Oct 14, 2012

I spent four years making
Waves of Steel
Hell yes I'm going to turn my avatar into an ad for it.
Fun Shoe

Red Mike posted:

Outside of very specific problems, I'm fairly sure there's no closed-form solution generally available for this kind of thing. It's much easier to set up a generator that can produce garbage as well as the desired outcome, and you prune/retry any failure until you meet all constraints. Because as soon as you have two separate constraints, any closed-form solution (or tailored generator that guarantees the result) will be so complex that it's very hard to tweak behaviour.

I've done some research into this in the past but there's no real unified 'domain' that you can dig into directly because the problem is so broad. You're better off focusing on particular use-cases (generating graphs for terrain generation in games, or related to analysing a story, etc).

OK, thanks. That's a bit disappointing, but it matches the feedback I've been getting elsewhere as well.

Re: wave function collapse, I think the challenge there will be in expressing the constraints in a way that WFC can work with. In particular, you have to make sure that the only way to fill in a given area is in a way that adheres to your constraints. WFC is a pattern-matching system and is generally designed to match overall style without caring too much about overall structure. So I'm not convinced it's a good fit here.

That said, it's good for me to remember that WFC exists, because it could come in handy for the more aesthetic aspects of the generator. Procedurally generating WFC input tiles, and then telling it to fill in a particular region of background art, for example, could be a good way to create stylistically consistent procgen backgrounds.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

Raenir Salazar posted:

E: thinking on it you can absolutely even do things like "have exactly five lengths of (subnode) between two specific nodes" because the start node, end node, and all the in between nodes have rules that basically can only result in that outcome.
I think you might be describing something like what I did for my mahjong solitaire board generator to ensure that every setup is solvable. If you start with the constraints, build *only* the bit of the map that matches the constraints, and then build out the rest of the map from there, you guarantee that the final map matches the constraints and minimize the "getting stuck" problem. Doing it this way may mean you have to program the constraints both forwards and backwards though.

My simple model for the mahjong was to add tiles in pairs only in places where it would have been valid to take them from, so, e.g. if there's a tile already in a row then you can't "unplay" a tile in that row except adjacent to the tile or tile-row that's already there, because you can't get to that situation from a full board.

It still occasionally has to restart generation, as it's possible for this algorithm to get stuck at the end if the last two empty spaces are e.g. positions 0 and 1 in a row, but it succeeds 90% of the time and takes an unnoticeable amount of time.

I also did one to generate word-search grids intentionally making them hostile (aiming to make words cross each other, and 'bluff' with partial words) with a similar process, placing the words on an empty grid and weighting positioning to attempt to satisfy the 'hostile' constraints, only falling back to an easy placement if it fails to place it cruelly some number of times. Same thing with occasionally locking up the board so the last word or two can't be placed, and just starting again if that happens.

Raenir Salazar
Nov 5, 2010

College Slice
Yeah you can always pre place specific elements and then let the rules take care of everything else, or have some very specific rules for example.

Suppse I have a "Blue" node and a "Red" node, and I want there to be one instance in which there are five spaces between them.

First we can always design our tile generator so there is like a queue or a pile of available nodes, so exactly one red node and exactly one blue node to fulfil the requirement of exactly one instance of a five space spacing between them (and then define duplicates of your red and blue nodes, mauve and purple which don't have to be five apart).

You can have it weighted where the random chance of picking a particular tile for a cell favours the smallest piles first to guarantee they pop out sooner like a weighted priority queue.

Then when red pops out, and you do the space to the right, since red is to the left by the rules only the cell "A1" can be placed, and then similarly only "A2", and so on until "A5" and then only blue can be placed next to the right of A5. And vice versa, of blue pops out first the same rule applies. This does require disallowing the paired node from being output while its pair is pending etc though.

Raenir Salazar
Nov 5, 2010

College Slice
But anyways I had a dream last night, and I was intrigued enough to write down the bits I could remember because it seems like it could make for a cool idea for a game project.

So pretty sure this was a horror game, and it takes place in like an abandoned mansion. As if you entered a room there's a chance there's a monster and it chases you.

From there it seemed like in my dream the primary task was gathering supplies, like flour to make bread and coal for heat, I think this can be expanded in that the coal or whale oil or whatever can also be used to power the oven to bake bread.

Outside your safe from the monster for some reason but it's desolate, presumably you'll starve or freeze to death (winter time?)

I got the sense others were there also struggling so it could be multiplayer, I wonder if it's more Soulsian in that maybe they're just there as phantoms and can't really help you directly but they can warn you?

I also had the idea the more people survived the more scarce or fewer resources to go around, so you're incentivize to use people as bait or let them get got, but you can rescue people if you act fast?

I think the main thing I'm missing is:
- why are you there.
- what's the end goal other than survival? Why can't you just escape on foot, what's the goal, waiting for rescue or escape via some other means?
- should the player be there because they're searching for something ala Resident Evil?

I think I vaguely recall from my dream this being pixel art or retro psx graphics, which could be cool, not a lot of rpg maker style horror games.

I think there's a solid core game loop there just different enough to separate it from other similar horror games out there.

Kinda reminds me of like if you took Resident Evil and combined it with Among Us and Dead by Daylight?

Red Mike
Jul 11, 2011

roomforthetuna posted:

I think you might be describing something like what I did for my mahjong solitaire board generator to ensure that every setup is solvable. If you start with the constraints, build *only* the bit of the map that matches the constraints, and then build out the rest of the map from there, you guarantee that the final map matches the constraints and minimize the "getting stuck" problem. Doing it this way may mean you have to program the constraints both forwards and backwards though.

This is one approach to simplifying generation like this, you turn your exact constraints into minimal constraints and you ensure only the minimal version first, then restart if not fulfilled.

So the given examples:

"There is a length-5 path between these two nodes." becomes "before actual generation, run a sub-generator that makes a length-5 path between these two nodes in a different way than the rest of the map".
"There are two paths between these two nodes" becomes "run a sub-generator to make a path between these two nodes twice before the actual generation (and make them not overlap/etc)".
"The path from this node to that node must pass through this third node" becomes "run a sub-generator for a path from A to B, then again from B to C before the actual generation".

Once you have your initial constraints filled, you just generate the rest of the map normally, and then apply inverse constraints you might have because of the initial generation ("there are two paths between these two nodes" => "trim any new/the worst paths between those two nodes", etc).

Basically, it's doable/"easy" to make a generator that works for a small number of constraints (ideally just one), and it's doable/"easy" to combine generators like this to make a more complex result that is overall unconstrained (because the combined generated object might not fit all constraints any more). This is all because checking a small number of constraints is easy to write "closed-form" stuff for.
But it's "hard" to make a bigger generator (whether by combining generators and then cleaning the result/restarting until it fits or just making one big generator) that does respect many constraints. Because checking a large number of constraints is hard to write "closed-form" stuff for.

xzzy
Mar 5, 2009

Anyone familiar with Unity Entities/ECS/DOTS/whatever? It's been a fun brain exercise messing with demos and reading up on it. So now I'm trying to actually make stuff and I got stuck trying to have entities react to data on other entities. This seems like a pretty fundamental concept so it's annoying that I got stuck here but it feels like I just haven't internalized the ECS way of thinking yet.

Situation: I got a world with a bunch of "Enemies" that are happily wandering about the terrain. I have it set up so I can click in the world to generate a new entity with a component that represents "Gunfire". I want the Enemies to react to the existence of that entity and "do stuff" in response. I can write a system to query for Enemies and modify values on those entities so that half is okay. However I'm stuck figuring out how to set up the system so I can work with a list of Gunfire entities in my job. With Entities 1.0 they have "ComponentLookup" that allows me to get an array of entities that have the Gunfire component, using an Entity as index. The documentation specifically states "You can use ComponentLookup to look up data associated with one entity while iterating over a different set of entities" so this feels like the right path.

What I'm getting stuck on is actually using this data. All the available examples have a reference stored of another entity (eg, a parent entity) that is used as an index the results of the ComponentLookup. eg, from the official docs in the "MoveTowardsJob" Execute function:

https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-looking-up-data.html

To my thinking this is kinda weird as it means I would have to add a component to my Enemies to store a reference to an entity and when a Gunfire entity is created, that entity is written to all my Enemies. At this point it feels cleaner to me that my Enemy job iterates through a list of any Gunfire entities and "does stuff" but given that I can't find anyone doing it this way it seems like I'm missing something basic.

So I guess my question is if storing entity references on entities is the correct way to do this sort of thing or if there's some other approach that I'm ignorant to.

(if one finds it useful, the current state of my system is here: https://pastebin.com/hyD8p1Kf. The ??? towards the bottom is where I'm having issues)

jizzy sillage
Aug 13, 2006

Could you have a job (system?) that is something like HearGunfire, which:

- iterates over all entities with a Transform and a Hearing component
- iterates over all Gunfire + Transform components
- checks the distance, and
- if it's below a certain limit, add a FleeingGunfire component to that entity

I'm not super familiar with ECS specifically but in my exploration of Data Oriented programming this would be the approach I'd try.

Contentato
Jan 19, 2023

yay [short tooting]

Alterian posted:

Neat! I am talking. I have to go every year for my job so talking is an easy way to get a VIP pass.

Cool! Looking forward to it. Hope to catch your talk

Raenir Salazar
Nov 5, 2010

College Slice
God my current task at work (as mentioned or alluded, we use Unreal) was a trip.

So there was a concern that our Licensing system that needs an internet connection for its initial first time start up seems to have had a freeze when the Internet at the office went out and due to user error, the cache files that save the local licensing info were deleted, yay!

After poking around working on trying to move around how the code executes I realized that the problem has nothing to do with the license system as it only loads after the UI/HUD loads.

I then actually check if the issue actually is at all reproducible and surprise, it isn't!

Testing the packaged build I do see there's a black window for several seconds while its "loading" so I figured I'd look into how to have a "preloading" screen that comes after the splash window.

I try the following:
- Loading a UserWidget on the BeginPlay in the empty entry level; doesn't work, black screen isn't the Empty start map.
- I tried to load a widget in the Init of GameInstance but this didn't work.
- What did kinda work was making a Module set to PreLoadingScreen which draws an image using Slate.

However when I on the BeginPlay went to turn off this PreLoadingScreen it doesn't seem to turn off right away and remains a blackscreen after the pre-loading screen loads.

After some experimenting I assume this is because the GameMovie or whatever its called "ends" it doesn't "exit" leaving a blackscreen for a few seconds longer before the game properly loads the first level.

I then do try some things and yes! The post-pre-loading screen void goes away.

What a trip.

KillHour
Oct 28, 2007


This took me literally weeks to figure out and get working and the immediate result is the least impressive thing ever, but I'm going to share, goddamnit!

https://www.youtube.com/watch?v=DJtHrqq65_s

This is a shader running a simple SDF path tracer. Basic stuff. But the part that was hard is that both the sphere and the cube are rendered with the same shader without branching or recompiling. I'm using a class interface that defines a getDistance function, and both the cube and sphere SDF functions inherit that interface. The fragment shader just declares the raw interface as a variable - I'm swapping out the actual implementation function at runtime.

Doing this required me to write my own DX11 renderer in C++ and hook into it as a DLL from Unity :suicide:


I originally tried to do it in DX12. I do NOT recommend that.

KillHour
Oct 28, 2007


I've gotten far enough that I feel like I have something worth sharing. It's pretty dumb - it's just linearly interpolating between completely random mashups of SDF functions without any kind of reactivity or control, but I think this is something I can build on, and I think the stupid amount of effort to kludge the shaders into doing what I want will be worth it.

https://www.youtube.com/watch?v=Ea5FPpOyL-U

KillHour
Oct 28, 2007


The random jumps are from a weird intermittent issue with shuffling buffers around and debugging this is going to literally drive me insane :negative:

I'm repacking the data into structs on the C# side so I can stop trying to compare 1D arrays hundreds of floats long. I hope that doesn't cause more problems later with marshalling the data to the GPU

We really need a :foreshadowing: smiley :v:

KillHour fucked around with this message at 05:53 on Apr 4, 2023

KillHour
Oct 28, 2007


While refactoring, I also changed the data packing/ordering algorithm to be more space efficient and one of those two things appears to have fixed it :iiam:

Edit: it's still there :(

KillHour fucked around with this message at 07:29 on Apr 4, 2023

KillHour
Oct 28, 2007


It was because the structured buffer I created on the GPU wasn't large enough. But because I only populate the data I need, it only mattered if there were randomly enough parameters to need that much space. And because it was only an issue on the GPU, it just silently glitched out instead of crashing.

The byte packing improvements I made just made the problem rarer, because I was more aggressive in cutting out unnecessary data.

:stonklol:

KillHour
Oct 28, 2007


I got this poo poo hooked up to my audio capture engine because I was desperate to see it working properly after all the headache. The animation and color systems are both very, very crude... but I think this is genuinely the coolest thing I've ever made.

Epilepsy warning for this one - there's some fast-ish movement.
https://www.youtube.com/watch?v=53-Y7I6Id20

Raenir Salazar
Nov 5, 2010

College Slice
Remember if you had any expenses related to game development, such as paying artists for concept art, try to see if you can deduct it for tax purposes. :getin:

KillHour
Oct 28, 2007


Turning off shader optimization via the D3DCOMPILE_SKIP_OPTIMIZATION flag improved my compile times by like 50x, but also improved my framerate by ~3x. :psyduck:

KillHour
Oct 28, 2007


I think I finally fixed a really annoying crash that would happen randomly and sometimes take hours.

If you think you're being clever by only allocating as much memory as you need on the C# side but you pass it as a pointer to a fixed buffer on the C++ side, DirectX is going to expect to be able to access memory for the entire length of the buffer. And it will be able to - until it suddenly can't because some other application claimed that memory.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!
Last night I dreamed I was explaining my career choices to an Amish person. He asked why I do software engineer stuff rather than game developer stuff where my interests really are, and, thinking how to explain it in terms that would make sense to an Amish person, I said working on games with other people is like spending 8 hours a day unpicking a gordian knot, whereas software engineer stuff is more like drawing maps.

Raenir Salazar
Nov 5, 2010

College Slice
Alright I *think* I cracked the secret to an Interfaceable event system. (C++ Unreal)

Afaik for an Interface you need a set of functions that classes inheriting from it need to implement; this is so anything that interacts with a class using that interface all share a common set of public functions like "Interact()" or "Use()" etc. I assume most people understand this but for the benefit of anyone new to gamedev who may
find this sort of thing informative.

If there's too many functions such that this becomes impractical then maybe there is bad design somewhere.

For an event system; I'd like it to have an interface, namely so I can use it with a ServiceLocator and do all sorts of interesting things with it suggested in the Design Patterns for Game Programming book.


However a game could have... Like 100+ events? It makes no sense to add those functions to the Interface, the UIEventSystem has no need to know something involve the core gameplay experience from the GlobalEventSystem; there should just be a common set of functions.

In the case of an event system, I was thinking a single function for Binding a delegate/event, for Broadcasting an event, and also for Unbinding a given event.

To bind a delegate the way I am used to it in C++ is like so:

code:
OnSomeEventDelegate.AddUniqueDynamic(this, &SomeClass::SomeFunction);
(Glossing over needing to declare a Multicast Dynamic Delegate of type FOnSomeEventDelegate etc).

Previously I would then need some means of retrieving this delegate from the Event system, to bind it to a member function that gets called when the event is Broadcast.


i.e like this:

code:
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->GetSomeEventDelegate().AddUniqueDynamic(this, &SomeClass::SomeFunction);
	}
And then to Broadcast in some other class:
code:
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->BroadcastSomeEvent();
	}
Which provides a degree of loose coupling for events because the event manager doesn't need to know about who is using it; the Listeners and Observers don't need to know about each other, but it's using a Singleton pattern to get the Event Manager and it would be better if I could use a ServiceLocator to access it and to do that we need an interface.

So ideally I want a design like this:

code:
class EVENTSYSTEM_API IEventManager
{
	GENERATED_BODY()

public:
	virtual void BindEvent(/* The Listener */, /* The Function to Bind */, /* The Event ID */)=0;
	virtual void UnBindEvent(/* The Listener */, /* The Function to Bind */, /* The Event ID */)=0;
	virtual void BroadcastEvent(/* The Event ID */)=0;
};
I spent a few hours trying to figure out how to pass a function to a function as a parameter in a way that binds to AddUniqueDynamic for Multicast Dynamic Events, detailed below.

Does anyone have a better idea for how I should do this?

Attempt #1, using a Wrapper Object. And passing it.

code:
#pragma once

#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "GenericPlatform/GenericPlatformMisc.h"

#include "WrappedFunction.generated.h"

UCLASS()
class UTILITIES_API UWrappedFunction : public UObject
{
	GENERATED_BODY()
public:
	template<typename T>
	void Bind(T* Instance, void(T::* Function)())
	{
		CallFn = [&]() { (Instance->*Function)(); };
	}

	UFUNCTION()
	void Dispatch() { CallFn(); }

private:
	TFunction<void()> CallFn;
};
Usage:

code:
void ABGameDevCPPTemplateGameModeBase::BeginPlay()
{
	UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("BeginPlay"));

	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(this, &ABGameDevCPPTemplateGameModeBase::TestFunction);

	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("Bound event"));
		TheGlobalEventManager->BindEvent(MyWrappedFunc);
		TheGlobalEventManager->BroadcastOnTestEvent(); // to test it right away
	}

	Super::BeginPlay();
}

void UGlobalEventManager::BindEvent(UWrappedFunction* TheFunction)
{
	OnTestEventDelegate.AddUniqueDynamic(TheFunction, &UWrappedFunction::Dispatch);
}
I dislike having to include the UWrapper header and doing this boilerplate.

I simplify it a little using Attempt #2 TFunction.

code:
void ABGameDevCPPTemplateGameModeBase::BeginPlay()
{
	UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("BeginPlay"));

	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("Bound event"));
		TFunction<void()> Functor = [&]() { TestFunction(); };
		TheGlobalEventManager->BindEventSimple(Functor);
		TheGlobalEventManager->BroadcastOnTestEvent();
	}

	Super::BeginPlay();
}
code:
// Imagine something like this in UWrapper
	void BindSimple(TFunction<void()> InFunction)
	{
		CallFn = InFunction;
	}
code:
void UGlobalEventManager::BindEventSimple(TFunction<void()> InFunction)
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->BindSimple(InFunction);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
I had to ask ChatGPT how to assign a function to a TFunction, I weirdly couldn't find any examples.

It still needs to be wrapped by UWrapper but this is now internal to the Event Manager, great!

But I still have to wrap a function in a TFunction to pass that in just to avoid including my Utility wrapper object.

What I'd really prefer is this:

code:
void ABGameDevCPPTemplateGameModeBase::BeginPlay()
{
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->BindEventSimplest(this, &ABGameDevCPPTemplateGameModeBase::TestFunction);
	}

	Super::BeginPlay();
}
code:
void UGlobalEventManager::BindEventSimple(UObject* Listener /* ??? */, void(UObject::* Function)() /* ??? */)
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(Listener, Function);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
But the problem is I can't just pass in the function without it not caring about the Object/Class it came from.

Sadly I can't have a template function be virtual, otherwise I'd just make (T* Instance, void(T::* Function)()) the signature.

Raenir Salazar fucked around with this message at 15:41 on May 7, 2023

leper khan
Dec 28, 2010
Honest to god thinks Half Life 2 is a bad game. But at least he likes Monster Hunter.

Raenir Salazar posted:

Alright I *think* I cracked the secret to an Interfaceable event system.

Afaik for an Interface you need a set of functions that classes inheriting from it need to implement; this is so anything that interacts with a class using that interface all share a common set of public functions like "Interact()" or "Use()" etc. I assume most people understand this but for the benefit of anyone new to gamedev who may
find this sort of thing informative.

If there's too many functions such that this becomes impractical then maybe there is bad design somewhere.

For an event system; I'd like it to have an interface, namely so I can use it with a ServiceLocator and do all sorts of interesting things with it suggested in the Design Patterns for Game Programming book.


However a game could have... Like 100+ events? It makes no sense to add those functions to the Interface, the UIEventSystem has no need to know something involve the core gameplay experience from the GlobalEventSystem; there should just be a common set of functions.

In the case of an event system, I was thinking a single function for Binding a delegate/event, for Broadcasting an event, and also for Unbinding a given event.

To bind a delegate the way I am used to it in C++ is like so:

code:
OnSomeEventDelegate.AddUniqueDynamic(this, &SomeClass::SomeFunction);
(Glossing over needing to declare a Multicast Dynamic Delegate of type FOnSomeEventDelegate etc).

Previously I would then need some means of retrieving this delegate from the Event system, to bind it to a member function that gets called when the event is Broadcast.


i.e like this:

code:
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->GetSomeEventDelegate().AddUniqueDynamic(this, &SomeClass::SomeFunction);
	}
And then to Broadcast in some other class:
code:
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->BroadcastSomeEvent();
	}
Which provides a degree of loose coupling for events because the event manager doesn't need to know about who is using it; the Listeners and Observers don't need to know about each other, but it's using a Singleton pattern to get the Event Manager and it would be better if I could use a ServiceLocator to access it and to do that we need an interface.

So ideally I want a design like this:

code:
class EVENTSYSTEM_API IEventManager
{
	GENERATED_BODY()

public:
	virtual void BindEvent(/* The Listener */, /* The Function to Bind */, /* The Event ID */)=0;
	virtual void UnBindEvent(/* The Listener */, /* The Function to Bind */, /* The Event ID */)=0;
	virtual void BroadcastEvent(/* The Event ID */)=0;
};
I spent a few hours trying to figure out how to pass a function to a function as a parameter in a way that binds to AddUniqueDynamic for Multicast Dynamic Events, detailed below.

Does anyone have a better idea for how I should do this?

Attempt #1, using a Wrapper Object. And passing it.

code:
#pragma once

#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "GenericPlatform/GenericPlatformMisc.h"

#include "WrappedFunction.generated.h"

UCLASS()
class UTILITIES_API UWrappedFunction : public UObject
{
	GENERATED_BODY()
public:
	template<typename T>
	void Bind(T* Instance, void(T::* Function)())
	{
		CallFn = [&]() { (Instance->*Function)(); };
	}

	UFUNCTION()
	void Dispatch() { CallFn(); }

private:
	TFunction<void()> CallFn;
};
Usage:

code:
void ABGameDevCPPTemplateGameModeBase::BeginPlay()
{
	UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("BeginPlay"));

	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(this, &ABGameDevCPPTemplateGameModeBase::TestFunction);

	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("Bound event"));
		TheGlobalEventManager->BindEvent(MyWrappedFunc);
		TheGlobalEventManager->BroadcastOnTestEvent(); // to test it right away
	}

	Super::BeginPlay();
}

void UGlobalEventManager::BindEvent(UWrappedFunction* TheFunction)
{
	OnTestEventDelegate.AddUniqueDynamic(TheFunction, &UWrappedFunction::Dispatch);
}
I dislike having to include the UWrapper header and doing this boilerplate.

I simplify it a little using Attempt #2 TFunction.

code:
void ABGameDevCPPTemplateGameModeBase::BeginPlay()
{
	UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("BeginPlay"));

	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		UE_LOG(BGameDevCPPTemplateLog, Log, TEXT("Bound event"));
		TFunction<void()> Functor = [&]() { TestFunction(); };
		TheGlobalEventManager->BindEventSimple(Functor);
		TheGlobalEventManager->BroadcastOnTestEvent();
	}

	Super::BeginPlay();
}
code:
// Imagine something like this in UWrapper
	void BindSimple(TFunction<void()> InFunction)
	{
		CallFn = InFunction;
	}
code:
void UGlobalEventManager::BindEventSimple(TFunction<void()> InFunction)
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->BindSimple(InFunction);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
I had to ask ChatGPT how to assign a function to a TFunction, I weirdly couldn't find any examples.

It still needs to be wrapped by UWrapper but this is now internal to the Event Manager, great!

But I still have to wrap a function in a TFunction to pass that in just to avoid including my Utility wrapper object.

What I'd really prefer is this:

code:
void ABGameDevCPPTemplateGameModeBase::BeginPlay()
{
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->BindEventSimplest(this, &ABGameDevCPPTemplateGameModeBase::TestFunction);
	}

	Super::BeginPlay();
}
code:
void UGlobalEventManager::BindEventSimple(UObject* Listener /* ??? */, void(UObject::* Function)() /* ??? */)
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(Listener, Function);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
But the problem is I can't just pass in the function without it not caring about the Object/Class it came from.

Sadly I can't have a template function be virtual, otherwise I'd just make (T* Instance, void(T::* Function)()) the signature.

If you have events, you probably don't need a service locator for anything related. Especially if you're in unity.

Events managed by ScriptableObject's work very well. Check out Hipple's 2017 unite talk.

Raenir Salazar
Nov 5, 2010

College Slice
This is Unreal Engine (C++) which I just edited my message to more clearly mention. :)

The point of the Service Locator is instead of having to do UGlobalEventManager::Get(...) to access my service via Singleton, I add a layer of Abstraction where I use ServiceLocator->GetGlobalEventManager(), and return the Object by Reference instead of a pointer and in the case this fails I can return a Null Object which has the same interface as my Event Manager but does nothing; instead of having to do checks like "If (myObject != nullptr) etc".

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
I'll be honest, sometimes it just makes a ton of sense to have your program blow up with an immediately-debuggable stack trace when it's gotten into a really messed up state like "there's no global event manager somehow".

For your actual problem, you just need to pass two parameters when registering your callback - the object pointer, and the member function pointer. Then you have everything you need to invoke the member function on that specific object when the event fires.

TooMuchAbstraction
Oct 14, 2012

I spent four years making
Waves of Steel
Hell yes I'm going to turn my avatar into an ad for it.
Fun Shoe

Jabor posted:

For your actual problem, you just need to pass two parameters when registering your callback - the object pointer, and the member function pointer. Then you have everything you need to invoke the member function on that specific object when the event fires.

I concur with this. It's C# code, but this is the event bus I made for my game. It's very easy to use:

code:
// In object initialization or wherever you feel is appropriate
GlobalPubSub.Subscribe<FooEvent>(this, OnFoo);

// When you're done
GlobalPubSub.Unsubscribe<FooEvent>(this);
// or
GlobalPubSub.UnsubscribeAll(this);

// The handler
private void OnFoo(FooEvent evt) {
  // react to the event.
}

// Posting events
GlobalPubSub.Publish(new FooEvent(whatever data you like));
The only interface in the entire thing is an empty interface that all events need to inherit from. A single class can listen to many different events if it wants to. It has to manage subscribing and unsubscribing by itself, but IME that's rarely an issue.

I did need to handle some edge cases in the event bus code itself. In particular, objects unsubscribing from events in response to events resulted in the subscriber list changing as I iterated over it, so I have to copy the list to a temporary container that I iterate over, instead. But at this point I'm quite happy with it; I haven't needed to change the event bus code in something like two years. Feel free to copy the code / port it to your own systems. It'll save you a lot of labor.

dupersaurus
Aug 1, 2012

Futurism was an art movement where dudes were all 'CARS ARE COOL AND THE PAST IS FOR CHUMPS. LET'S DRAW SOME CARS.'

Raenir Salazar posted:

However a game could have... Like 100+ events? It makes no sense to add those functions to the Interface, the UIEventSystem has no need to know something involve the core gameplay experience from the GlobalEventSystem; there should just be a common set of functions.

Ignoring everything else, you’re close but a little off here. Instead of an interface it could be a base class with every event function defined but empty. It has some theoretical problems that I think you’ve identified (like a world with a buttload of events), but it is a tried and true method of doing it. Another, exact opposite approach is just having a receiveEvent(id, data) function on the interface that receives every event, and it’s up to the listeners to ignore or use each event.

Raenir Salazar
Nov 5, 2010

College Slice

Jabor posted:

I'll be honest, sometimes it just makes a ton of sense to have your program blow up with an immediately-debuggable stack trace when it's gotten into a really messed up state like "there's no global event manager somehow".

The null object can still handle this, either by putting in an exception or a log message when you try to call the function.

quote:

For your actual problem, you just need to pass two parameters when registering your callback - the object pointer, and the member function pointer. Then you have everything you need to invoke the member function on that specific object when the event fires.

This is precisely the stumbling block though!

This is an error I'm getting when I try to compile:

quote:

C2664 'void UGlobalEventManager::BindSomeEvent(UObject *,void (__cdecl *)(void))': cannot convert argument 2 from 'void (__cdecl ABGameDevCPPTemplateGameModeBase::* )(void)' to 'void (__cdecl *)(void)'

When I try to do the below:

code:
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->BindSomeEvent(this, &ABGameDevCPPTemplateGameModeBase::TestFunction);
		TheGlobalEventManager->BroadcastOnTestEvent();
	}
code:
void UGlobalEventManager::BindSomeEvent(UObject* Listener, void (*InFunction)())
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->BindSimple(Listener, InFunction);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
I want it to not care about the class who the function is a member of, because I want to be able to have any class pass any in this case "void" function into this function.


dupersaurus posted:

Ignoring everything else, you’re close but a little off here. Instead of an interface it could be a base class with every event function defined but empty. It has some theoretical problems that I think you’ve identified (like a world with a buttload of events), but it is a tried and true method of doing it. Another, exact opposite approach is just having a receiveEvent(id, data) function on the interface that receives every event, and it’s up to the listeners to ignore or use each event.

Yeah, I'd prefer that I can have a clean separation between UIEvent, AIEvents, etc. That's the design constraint I'd like so getting this to work as an interface is still preferable. I have a framework that basically works the way I want it and makes events mostly loosely coupled; I just want it to work a little better.

And in general clean code/design pattern best practices/loose coupling afaik basically requires the use of interfaces so that's what I'm trying to stick to as a learning experience. If my only consideration was "whatever works" then that's one thing, but I want to try to do this the on paper "best" way.


TooMuchAbstraction posted:

I concur with this. It's C# code, but this is the event bus I made for my game. It's very easy to use:

The only interface in the entire thing is an empty interface that all events need to inherit from. A single class can listen to many different events if it wants to. It has to manage subscribing and unsubscribing by itself, but IME that's rarely an issue.

I did need to handle some edge cases in the event bus code itself. In particular, objects unsubscribing from events in response to events resulted in the subscriber list changing as I iterated over it, so I have to copy the list to a temporary container that I iterate over, instead. But at this point I'm quite happy with it; I haven't needed to change the event bus code in something like two years. Feel free to copy the code / port it to your own systems. It'll save you a lot of labor.

Yeah this is definitely what I'm trying to go for but I'm limited by the existing code that I can see and work with that I know works, and mainly stumbling over the precise C++ syntax to do what I want to do; and not veering away from the existing Delegate/Event system Unreal already provides (i.e compatibility with UFUNCTION functions and Blueprints). This would presumably be easier in C# :haw:

Raenir Salazar fucked around with this message at 22:56 on May 7, 2023

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
I think you just need to define your function parameter as a member function pointer rather than as a function pointer? They're two different types and you can't just treat one as the other.

Note that you can static_cast a derived-class member function pointer to a UObject member function pointer as long as you're sure it will only ever be invoked on a derived class that actually has that member function.

Raenir Salazar
Nov 5, 2010

College Slice

Jabor posted:

I think you just need to define your function parameter as a member function pointer rather than as a function pointer? They're two different types and you can't just treat one as the other.

Note that you can static_cast a derived-class member function pointer to a UObject member function pointer as long as you're sure it will only ever be invoked on a derived class that actually has that member function.

I tried a quick google but I'm not sure what you're suggesting. Can you post an example? Can it be a member to an arbitrary class? Because that's the thing, it has to be any function from any class, so it can't be from a specific class.

e: I got it!

This?
code:
void UGlobalEventManager::BindSomeEvent(UObject* Listener, void(UObject::* InFunction)())
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(Listener, InFunction);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
code:
	if (UGlobalEventManager* TheGlobalEventManager = UGlobalEventManager::Get(GetWorld()))
	{
		TheGlobalEventManager->BindSomeEvent(this, reinterpret_cast<void(UObject::*)()>(&ABGameDevCPPTemplateGameModeBase::TestFunction));
		TheGlobalEventManager->BroadcastOnTestEvent();
	}
My googling suggested reinterpret cast, I haven't tried it with static_cast yet, it compiles, runs and seems to work!

e2: Static cast seems to also work. Do I have a reason to prefer one over the other? Google suggests I want static_cast over dynamic_cast usually, but with Unreal I imagine general I just usually want Cast<type>(SomeObject), so this doesn't come up often for me.

Raenir Salazar fucked around with this message at 03:16 on May 8, 2023

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
I'd be inclined to template the bind function so that you can move the cast inside it, instead of having to cast at every single call site. In that case you'd definitely want static_cast so that you'll get a compile-time break if someone passes something that isn't convertible.

Raenir Salazar
Nov 5, 2010

College Slice

Jabor posted:

I'd be inclined to template the bind function so that you can move the cast inside it, instead of having to cast at every single call site. In that case you'd definitely want static_cast so that you'll get a compile-time break if someone passes something that isn't convertible.

Unfortunately I can't template it since my intent is to make it a virtual function as part of an interface.

And seems like success was premature, one of my other functions must've been used instead or something.

I get an access violation after I did some cleanup and tried to delete the unused commented stuff.

code:
void UFunction::Invoke(UObject* Obj, FFrame& Stack, RESULT_DECL)
{
	checkSlow(Func);

	UClass* OuterClass = (UClass*)GetOuter();
	if (OuterClass->IsChildOf(UInterface::StaticClass()))
	{
		Obj = (UObject*)Obj->GetInterfaceAddress(OuterClass);
	}

	TGuardValue<UFunction*> NativeFuncGuard(Stack.CurrentNativeFunction, this);
	return (*Func)(Obj, Stack, RESULT_PARAM);
}
e: Err wait, doing some ctrl z's I'm not sure why it's going that, everything seems to be working. How mysterious.

e2: It happens when I comment out the other test "Bind" functions I made, but none of them for sure were being called. I'm not sure why they need to "exist".

Raenir Salazar fucked around with this message at 04:31 on May 8, 2023

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
You can have a templated function that does the cast and then calls your virtual function, if that's more convenient for you.

Raenir Salazar
Nov 5, 2010

College Slice
Well this is weird, this function has to "exist" but otherwise doesn't do anything; and if I delete it, the event breaks and I get a access violation exception.

code:
// in the header
void BindEventSimple(UObject* Listener, TFunction<void()> InFunction);
code:
// in the cpp file
void UGlobalEventManager::BindEventSimple(UObject* Listener, TFunction<void()> InFunction)
{
	UE_LOG(EventSystemLog, Log, TEXT("[bb]BindEventSimple."));
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->BindSimple(Listener, InFunction);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
Nothing is calling this function, but for some reason it's load bearing?

Jabor posted:

You can have a templated function that does the cast and then calls your virtual function, if that's more convenient for you.

Like this?

code:
class EVENTSYSTEM_API IEventManager
{
	GENERATED_BODY()

public:
	virtual void BindEvent(UObject* InListener, void(UObject::* Function)())=0;

	template<typename T>
	void BindEvent(T* Listener, void(T::* Function)()); // non-virtual, inherited by derived classes?
};
In my GlobalEventManager:

code:
void UGlobalEventManager::BindEvent(T* Instance, void(T::* Function)())
{
    BindEvent->(T, static_cast<void(T::*)()>(Function));
}
code:
void UGlobalEventManager::BindEvent(UObject* Listener, void(UObject::* InFunction)())
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(Listener, InFunction);
	OnTestEventDelegate.AddUniqueDynamic(MyWrappedFunc, &UWrappedFunction::Dispatch);
}
Something like this?


e:

I managed to reduce the code of the load bearing function to just this:
code:
void UGlobalEventManager::BindEventSimple(TFunction<void()> InFunction)
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
}
If I comment out this line then I get an access violation when Unreal tries to invoke the delegate; if I don't uncomment it then it works perfectly. This is baffling because this code never actually gets run, but at compile time I guess Unreal might be reserving something as a result?

I notice in particular that the "name" for the UWrappedFunction does change, from _0 to _1 if it stays.

e2: Yup, for some reason I need to declare two instances at least of my UWrapperFunction class.

code:
void UGlobalEventManager::BindSomeEvent(UObject* Listener, void(UObject::* InFunction)())
{
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	TheWrappedFunction = NewObject<UWrappedFunction>(); // uproperty class member in this it was being garbage collected
	TheWrappedFunction->Bind(Listener, InFunction);
	OnTestEventDelegate.AddUniqueDynamic(TheWrappedFunction, &UWrappedFunction::Dispatch);
}
So this will work if the other code is commented out.

Raenir Salazar fucked around with this message at 05:36 on May 8, 2023

Raenir Salazar
Nov 5, 2010

College Slice
The exception seems to not occur if I change the capture clause from by reference to by value:

code:
UCLASS()
class UTILITIES_API UWrappedFunction : public UObject
{
	GENERATED_BODY()
public:
	template<typename T>
	void Bind(T* Instance, void(T::* Function)())
	{
		CallFn = [=]() { (Instance->*Function)(); }; // was [&]
	}

	UFUNCTION()
	void Dispatch() { CallFn(); }

private:
	TFunction<void()> CallFn;
};
Which still doesn't explain why when by reference it only works if there's 2 UWrappedFunction's declared (and not even in the same scope at that).

I'm also not sure if this is a good idea of course.

The same issue also occurs if I try to use a template function to call the other function.

It doesn't seem to be something like the function not existing with UObject because this does work but only if I do something really weird, which suggests a not-weird way of solving it.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

Raenir Salazar posted:

The exception seems to not occur if I change the capture clause from by reference to by value:
It's surprising that it doesn't crash pretty much all the time, capturing by reference, because the references are references to the T* and function-pointer parameters which immediately go out of scope as soon as you exit Bind(). Since they're both pointers anyway, capturing them by value 100% makes more sense here.

(Also it's probably a massive disaster waiting to happen because bare pointers, yuck, so 1990.)

Raenir Salazar
Nov 5, 2010

College Slice

roomforthetuna posted:

It's surprising that it doesn't crash pretty much all the time, capturing by reference, because the references are references to the T* and function-pointer parameters which immediately go out of scope as soon as you exit Bind(). Since they're both pointers anyway, capturing them by value 100% makes more sense here.

(Also it's probably a massive disaster waiting to happen because bare pointers, yuck, so 1990.)

Huh, the more you know! It makes sense if the original code I got was "wrong" because the original poster for it had the caveat that it was basically untested pseudocode that I somehow managed to almost work.

But the weird thing was it seemed to work fine if it was used more directly like:
code:
	auto MyWrappedFunc = NewObject<UWrappedFunction>();
	MyWrappedFunc->Bind(this, &ABGameDevCPPTemplateGameModeBase::TestFunction);
	TheGlobalEventManager->BindEvent(MyWrappedFunc);
	TheGlobalEventManager->BroadcastOnTestEvent();
Where BindEvent just looks like this:
code:
void UGlobalEventManager::BindEvent(UWrappedFunction* InWrappedFunction)
{
	OnTestEventDelegate.AddUniqueDynamic(InWrappedFunction, &InWrappedFunction::Dispatch);
}
But anyways, is there a way to test if it's actually the capture by reference that was the culprit? Like having it capture by reference the actual objects the parameters are pointing to and see if that makes it work as well? Or is only capture by value what's wanted in circumstances like this?

Like:

code:
void Bind(T*& Instance, void(T::*& Function)()) // No idea how I'd do this for a function pointer!

Adbot
ADBOT LOVES YOU

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

Raenir Salazar posted:

But anyways, is there a way to test if it's actually the capture by reference that was the culprit? Like having it capture by reference the actual objects the parameters are pointing to and see if that makes it work as well? Or is only capture by value what's wanted in circumstances like this?
If you're not planning to have the lambda change the values of the original pointers, there is no point in capturing pointers by reference, it's worse in every way than capturing them by value.

The best case is it optimizes away and does nothing different. The worst case is what you got, the referenced values go out of scope and things gently caress up. The medium case is a reference to pointer is essentially the same thing as a pointer to a pointer, you're adding an unnecessary layer of indirection to the calls.

I like to think of a reference as "a pointer that you're asserting is never null, and that you can't change."

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply