I was printing from the server (see the game mode BPs). From what I’ve read online and even seen videos on, the GameInstance class is precisely what should be used to persist data between levels. I ended up messing with this for 2 days trying every variation I could think of, ( BP only, C++ only, BP + C++, replicated, not replicated, client, listen server, standalone, etc…).
I ended up saying screw it and went with what I know from my day job… just save it to a database and call it a day. I know for sure that the game mode is server only and I’m already connecting to a database so I just persisted the “active” character to the character table as a flag. It seems to work well enough for now. I truly don’t understand the purpose of the GameInstance anymore and will probably never mess with it again after wasting so much time for a trivial task.
For anyone interested, I’m using NetDBPlugin (https://www.unrealengine.com/marketplace/en-US/product/netdb) and this is the sql I’m using to create my tables.
CREATE OR REPLACE FUNCTION manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE player (
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
net_id VARCHAR(128) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
SELECT manage_updated_at('player');
CREATE TABLE character (
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
player_id UUID NOT NULL REFERENCES player (id) ON DELETE CASCADE,
name VARCHAR(128) NOT NULL UNIQUE,
x real NOT NULL DEFAULT 0,
y real NOT NULL DEFAULT 0,
z real NOT NULL DEFAULT 0,
yaw real NOT NULL DEFAULT 0,
pitch real NOT NULL DEFAULT 0,
roll real NOT NULL DEFAULT 0,
-- Whatever else you want to track like attribute sets, etc...
active BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX idx_active_player ON character (player_id, active)
WHERE active = TRUE;
SELECT manage_updated_at('character');
And a little info of the game code… way too much to post here but essentially it goes:
- MainMenuPlayerController class
- CoreGameMode class
- MainMenuGameMode class
- GameplayGameMode class
CoreGameMode: has some helper functions and common state, such as the players connected mapped to their ID, or a utility to get their unique net ID as a string.
MainMenuGameMode : Has all the functionality to create a new player, character, etc… basic crud stuff, as well as “log a character” into the game. This essentially just updates the DB with the flag I was talking about and returns the IpAddress that should be used to open the level with.
MainMenuPlayerController: works as my interface between UI and game mode and creates some delegates and utilties
GameplayGameMode: validates the player and character in the DB on post login, tracks player, creates character, possesses and spawns.
For something like a main menu I think this will suffice for now, my in-game code uses a HUD, controllers, etc but that seemed like overkill for this.