Persistent Player Statistics: Questions and Thoughtful Feedback

Just wanted to start out saying that all the additional effort into the documentation is much appreciated, and the recent live stream was a great way to connect w/ developers/creators. Although this is critical in nature, I offer this feedback in good faith, and believe in the potential of the team. :pray:

Player Statistics Tutorial: Questions & Feedback:

Summary: I have found the documentation for the Player Statistics tutorial to be convoluted and potentially misguided for a couple of reasons which I will go over in detail below.

Key Areas of Confusion: Justification of why code its being written in this manner doesnā€™t check out for me, even after multiple reviews.

Format: I have some general feedback in areas I feel more certain on, and questions in others. Ill be weaving between them and keeping things sequential:

Starting from the top, we a few key term and a very general explanation making an arguably fair assumption, but an assumption none the less:

I think some areas to be included up to this point would be:

  • There are other pages that cover ways to use persistent data (with links), and this is an expansions on base knowledge that you will benefit from starting with in order to understand the contents of this tutorial

  • Wrapping the data together in a class isnā€™t required to associate it in verse inherently. Since the constructor isnā€™t actually persistent and we have to use unique names with persistence that we reference (and whoā€™s namespace is retained between sessions) whenever reconstructing (aka updating) the class.

  • An example of a real world scenario youā€™d use this in. (BR Victories in FN, Goals in RL/Races in RR, etc. Thereā€™s not a single thing more relatable in UEFN, than Fortnite. This carries across to code examples currently containing dogs and catsā€¦ wherever thereā€™s an opportunity to use direct Fortnite examples, take that route. :white_check_mark:)

  • What Iā€™d suggest as an alternative:
    • Iā€™d begin with an example of a normal int value update, and lead into handling intrinsically tied values before jumping straight into a nested constructor. Iā€™d also highlight it isnā€™t required, but can be an effective way to encapsulate the association in the persistent data itself.
    • Immediately wrapping the class and nesting the constructor for partial updates, before writing a partial update and understanding why had me lose my bearing almost immediately.

Key Takeaway: A clear understanding surrounding the ā€˜whyā€™ would go a long way.

Argument:
You donā€™t necessarily have to change the way that the tutorials currently organized and presented, however without linking to the other documentation or providing more insight into the decision making process, it seems to lack essential context.

Reasoning:
It includes advanced concepts in tandem with a novel/unique data storage type that up until this point was rarely (if ever) used. This can create confusion as to what is being done out of necessity and whatā€™s simply ā€œone of manyā€ ways to approach things.

In the exercise, what is the purpose of creating the StatType module and the score_stats w/ Debug Strings?

I noticed these classes arenā€™t ever referenced in persistence data, nor is the module included in a session based weakmap:

An the only perceivable way to me that these classes are associated is only through the partial constructor function:

If a lesson isnā€™t directly optimizing or solving a problem, it lacks a practical application. This approach left me feeling unsure about how things worked, leading to a lot of extra time spent refining the information into a more straightforward process.

Introducing concepts purely for their own sake starts to clash with the higher levels of tutorials, as the way we approach coding, logic, and problem-solving becomes more complex and rooted in reason.

Iā€™m uncertain about whether or not I have an inaccurate understanding of what happens when adding persistence to my map, and would like some clarification on the following:

Quote from code comments:

?: Are all enums final from that point on, even outside of permissible classes?

Others Iā€™ve consulted werenā€™t aware of this as an issue, which has been a major source of confusion for me. Clarity here is requests, and unless new light is shined upon the justification for the Debug String to make it relevant here, I would consider removing it to simplify the already complex process.

Suggested Solutions:

Assumptions:

  • StatType module isnā€™t part of the persistent data
  • Enum is an acceptable data format as long as its only referenced outside of persistent classes

A more effective strategy may be to define stat_type as an enum within the same module, which would simplify the implementation and allow for more dynamic applications. Additionally, creating a single enum along with a ā€œToStringā€ function could potentially be less memory-intensive, eliminating the need for an abstract parent class or any class implementation. Even though any memory savings might be negligible at this scale, itā€™s still worth considering for overall efficiency.

  • For the purpose of comparison in partial updates you can use case instead of ifā€™s, and for the multifunctional desire to have a string ToString adds the addition of auto resolving.

  • In the case that we really need to avoid enum, I would also address the potential of simply including a string within the wrapper itself. While this is likely not an adequate solution, and would result in a debug identifier exceeding the memory cost of the class it exists to identify, addressing that when presenting an alternative would be a healthy pairing.

Iā€™m very open to the possibility that Iā€™ve missed something fundamental here, and am merely suggesting these points in hopes of receiving clarification. I hope this provides insightful and useful feedback.
Cheers.
-Lion

Hey Lion!

This is excellent feedback, and I appreciate the depth of it and the solutions youā€™ve suggested. There are a lot of good callouts here and this helps us a lot not only in refining our docs but also with things to keep an eye out for in the future. Iā€™ll take a look and see what can be done! (Oh and the images an quotes are incredibly helpful, thank you!)

2 Likes

Awesome.
Also, I just started looking into more advanced use cases for persistence and seeing how the debug string correlates to generic values within your persistent data, and how youā€™re using the correlated values in the manager to access the data. Iā€™m starting to gather how you can leverage this to encode data that you define externally.
Not sure if that was the intention or not but I do like that thereā€™s an example of it, just still a bit confused on the overall message of this tutorial. I will just keep pluggin away though and trust in the process.
Will let you know when I come up with a system if this ended up being helpful to that process.

Edit:

method int_data.Serialize():dictionary =
    {
        "Value": self.Value
    }

I think youā€™re trying to teach us fundamentals for encoding data?

Thatā€™s the only way Iā€™ve been able to make sense of it and it took a few days of continued development, a conversation with another developer, and a chatGPT search. Iā€™m not sure if its supposed to be that hard to figure out, but thatā€™s how my experience has gone.

Iā€™ll stop presupposing on what it is you guys may have ultimately been after here in the approach, but will double down on more supportive reasoning.

TL;DR: Realize its likely just a debug print
Iā€™ve come conclusion that while itā€™s possible that we can serialize data and associate it external to the persistent data itself, itā€™s likely the purpose is really just to have a debug print here. In the case of serializing data into strings I got about as far as designing a system, but Iā€™m unsure how it would affect write times or interact with backend optimizations and definitions on Epicā€™s end.

TL;DR: Class inheritance seems to prefer DebugString:string, where DebugString() allows interface usage & becomes ubiquitous. Enum being unique/explicit still seems superior for data integrity.
Something I realized about using a debug string here is if it were an attribute rather than a function it would resolve here within the if statement allowing us to retain complete failure context:
image

So by doing so you reduce the need to keep an up to date enum list, which also can only convert to string if you add all those strings to a case function that you then need to update. That being said you lose the ability to ensure that the value truly is a unique key, which does make it more prone to error. That being said I do really like the debug strings and have been adding the DebugPrint() and have been adding debug strings to everything.

With new patch Iā€™m seeing printā€™s transacts now so maybe this will impact things.

While I still donā€™t really understand how this gets used for persistence, I have adopted the methodology elsewhere.
Essentially, I create enum key values that turn my immutable computed module level classes into swift enums (as far as i can tell) so I can reliably retrieve and maintain data for modular class construction.
So, just wanted to circle back and say it did teach me a lot, just not about persistence data :sweat_smile:
Cheers.