(Technical, Code architecture) How are large sandbox games with lots of customizability designed? (Strive4Power, COC)

fassass

Newbie
Nov 12, 2017
22
10
I've got over 5 years making games & doing software, and for my next project, I want to make a porn sandbox with a lot of character customization. I have the combat, character stats, and some basic UI done, however I struggle a lot with the procedural elements/sandbox elements.

This game will be very similar to TITS, COC and Liliths throne with its character customizaiblity. It will have a lot of menuing, and be fully 2d with lots of text.

I'm making this game in Godot, although I'm open to general advice on this topic. My questions are as follows:

1. How is the architecture of sandbox interactivity -> custom event scenes -> back to sandbox interactivity usually handled? As in, if I have a character exploring, then I have them do something, how do we go back to the exploring section? Is it like a separate layer that's overlaid on top of the overworld logic? Or is it more like there's an overworld loop, and we break out of it for a scene, then go back to the loop? If this question sounds stupid, then I understand. Im not really explaining it that well.

2. How is saving state between scenes & save files usually handled? Is it all serialized in a huge JSON, or is there a more elegant solution?

3. Does anyone actually care about the performance of text-based games (liliths throne, No Haven)? Both these games are quite laggy, but I don't see many references to that fact during discussion.

4. How are things like appearance/race actually handled in code? TITS has over 20 values with unique identifiers ( ) - and making a dedicated class for this aint to difficult for someone of my skill. Still, I was wondering if theres a blogpost/devlog on this or something similar.

5. How are procedural sex events usually scripted (like in liliths throne and strive4power). Do these games have source code I can analyze?

That's most of the questions I can think of currently. I'll ask more later.

Overall, the hardest part is DEFINETLY the procedural generation in these games.
 

Satori6

Game Developer
Aug 29, 2023
492
934
I've got over 5 years making games & doing software, and for my next project, I want to make a porn sandbox with a lot of character customization. I have the combat, character stats, and some basic UI done, however I struggle a lot with the procedural elements/sandbox elements.

This game will be very similar to TITS, COC and Liliths throne with its character customizaiblity. It will have a lot of menuing, and be fully 2d with lots of text.

I'm making this game in Godot, although I'm open to general advice on this topic. My questions are as follows:

1. How is the architecture of sandbox interactivity -> custom event scenes -> back to sandbox interactivity usually handled? As in, if I have a character exploring, then I have them do something, how do we go back to the exploring section? Is it like a separate layer that's overlaid on top of the overworld logic? Or is it more like there's an overworld loop, and we break out of it for a scene, then go back to the loop? If this question sounds stupid, then I understand. Im not really explaining it that well.

2. How is saving state between scenes & save files usually handled? Is it all serialized in a huge JSON, or is there a more elegant solution?

3. Does anyone actually care about the performance of text-based games (liliths throne, No Haven)? Both these games are quite laggy, but I don't see many references to that fact during discussion.

4. How are things like appearance/race actually handled in code? TITS has over 20 values with unique identifiers ( ) - and making a dedicated class for this aint to difficult for someone of my skill. Still, I was wondering if theres a blogpost/devlog on this or something similar.

5. How are procedural sex events usually scripted (like in liliths throne and strive4power). Do these games have source code I can analyze?

That's most of the questions I can think of currently. I'll ask more later.

Overall, the hardest part is DEFINETLY the procedural generation in these games.
1: Overworld
> Visit Area
> Check flags & variables for pre scripted scenes (plot) and trigger the special scene instead of loading the area (or redirect before rendering the area).
> If that didn't trigger, check for random events and trigger them if the conditions are met, instead of loading the area (or redirect).
> If no special/random events were triggered, load the area as usual.
> Otherwise save the area and when appropriate (ie; the event doesn't move you to a different place) load it once the current plot or random scene is over.
> Add flags as needed to avoid random events from chaining or repeating right after they're over.

2: Most sandbox games use Twine with SugarCube, which automatically handles saves. For other cases, JSON seems like the simplest approach. Just make sure not to save any images on b64 or other stuff that bloats the size of a save file.

3: Lilith's Throne had awful performance. It wasn't the only reason why I stopped playing it (I disliked the game in general), but it was one of them. There's no reason for a text-based game to be laggy on 2024 hardware.

4: Not sure, but maybe the could be used to get an idea.

5: You can analyze DoL's code, which is probably the best modern example of a sandbox with random event/sex generation.
 

fassass

Newbie
Nov 12, 2017
22
10
Holy shit, thank you so much.

I considered using Twine (and honestly I might make a simple game to get my name out there), but for a more complex game like the one I want to make, Godot is the best choice for me. I'll post something soon...
 

papel

Member
Game Developer
Sep 2, 2018
370
490
3 - People may not complain in the "proper" channels, but a text game that is slow/laggy speaks volumes about the (lack of) skill of the developer.

4 - My best guess here is that it uses a similar approach to databases: you have a "table" with number of "rows", with unique identifiers, that bring the rest of the desired information. For instance:

Python:
head_stuff = [0, human, "your face looks human"],
[1, canine, "your face looks canine, with a big snout"],
[2, equine, "your face is long like a horse's"]
# Effectively, you have an array of arrays
# Repeat for each body part that can transform, like arms, torso, legs

# And the player only needs to carry/point to that id, like so:
Player.class
   arms == 0
   head == 2
   legs == 0
   torso == 1

function describe_player():
  print(head_stuff[player.head][2]) # With player.head being 2, it will print the Equine description
Also, in Godot, you can use , so that you can use words as key identifiers instead of position in the array. The above would then look like this
Python:
head_stuff = {"equine":"your face is long like a horse's",
"human": "your face looks human"}

Player.class
  arms == "human"
  head == "equine"

function describe_player():
  print(head_stuff[player.head]) # As head is "equine", it will print the value of the "equine" key.
I think saving and loading dicts as JSON objects shouldn't be too hard. You can also create an external file to be read once the game starts, to fill the descriptions' arrays or dicts, this will make it much easier to edit said text and even work on translations.

These Dict objects can also be super useful as means of adding, checking and saving gameplay flags, as Satori mentioned, because you only ever need to save them when they're TRUE, so a check of "does dict with the key BLAH exist?" will return false unless it was triggered. An example, very close to actual godot code:

Python:
# Make sure to define this inside a GLOBAL script.
var game_flags:Dict = {} #Creates an empty dictionary

# Whenever you attribute a value to a key, if said key doesn't exist, it is created and added to the Dict object
game_flags["event_bob_talked1"] = true

# Keep in mind that trying the get the value of a key that doesn't exist will crash the game with an error
func get_flag_value(key):
  if game_flags.has(key):
    return game_flags[key]
  else: #it does not have the key, so it's automatically false
    return false

func first_talk(): #Script of bob's first talk
  if get_flag_value("event_bob_talked1"):
    print("Bob has already talked here")
  else: #If the key doesn't exist, it'll enter here
    print("Bob arrives to talk for the first time")
You can also save answers and decisions as their own keys as well, if you want to create branches caused by dialogue choices. As an example: Bob can encounter a fairy (1st_encounter flag) and, depending on what he answers, he may or may not find the fairy again (1st_encounter_answer flag)

Python:
if get_flag_value("1st_encounter_fairy"): #If true, Bob has triggered the scene the first time
  if get_flag_value("1st_encounter_answer") == 2:
    print("The fairy finds Bob before he can even think about looking for her")
  else:
    print("Bob tries to find the fairy, but he fails")
else:
  print("Bob finds a fairy for the first time")
 

fyrean

New Member
Jul 1, 2024
7
17
Just wondering but why Godot and not Html5 (like twine or such)? Since its mostly a text-based game, you might benefit more from web, more compatibility with any device with a web browser (I know Godot has web export but its not as stable nor has good performance), and much better and responsive GUI for any screen size.
 

fassass

Newbie
Nov 12, 2017
22
10
Just wondering but why Godot and not Html5 (like twine or such)? Since its mostly a text-based game, you might benefit more from web, more compatibility with any device with a web browser (I know Godot has web export but its not as stable nor has good performance), and much better and responsive GUI for any screen size.

To give a few answers:

1. The UI in Godot is extremely easy to work with. I can set up a ton of menus and have my own custom theming really quick. I don't like javascript/html as much, I prefer a visual editor. I know the size on mobile devices can be tricky, so I'm trying to make the scaling-down functionality work.

2. Familiarity. I've used it a lot before, and having pre-built themes and scripts make it easy for me.

3. Extendibility. While my basic features probably can be done in twine (text-based everything), longer term Godot is the best choice for me. I want to target the fact that theres a huge demand for lower-graphic high-player customizability games. I'm actually making a narrative CYOA in godot for this exact reason. There's a lot of "trainer" type games that were popular a few years back, and I can see why people like them.

4. string manipulation/loading modified strings from resources is a BREEZE. If I want to have functions for custom strings ("You try to insert your [genitalia description] cock") I find it super easy to do. I know I can do it in sugarcube/renpy, but idk, godots just so much easier for me to use.

I see two main downsides .
The biggest downside to using Godot is the lack of .webm support. I have to convert everything to .gif or .ogv if I want animations.

Of course, having contributors won't be as easy as using twee (where if people wanted to mod they could just see the visual graph/all the logic in one file, and then change things around) but Im ok with that.

So overall, I think Godot's my best choice.
 
  • Red Heart
Reactions: fyrean

Quintillian

Member
Apr 15, 2019
105
205
I’m late to this discussion, but seeing that I’m also working on my own text-base-ish sandbox, I thought to provide my two cents. I’ll focus on the 5th inquiry (Sex Encounters), and since the question was about software architecture, I’ll provide my approach on the subject.

Disclaimer first though, this is still an early work in progress from my part, so there could be issues that I still haven’t encountered, and also, I suck at explaining myself when it comes to deep technical solutions. So I hope the code can speak for itself.

I’ll use Renpy and Python, because that’s what I’m working on, but the general concepts are language independent.

Short and simple, I’m using a combination of Finite State Machines and Fussy Pattern Matching to make my Sex Simulation Manager.

I’m not going to explain the specifics of what a Finite State Machines is, you can google it as there are ample resources on that. And as for Fussy Pattern Matching, you should watch to get a better idea of what I’m talking about.

Anyways here is a working code snippet of the general approach:
You don't have permission to view the spoiler content. Log in or register now.

And a demo of it working:

You don't have permission to view the spoiler content. Log in or register now.

Notes:
I’m using two third party libraries, I think I should note that out in case you or anyone else wants to run this thing on your own to better understand it.

The libraries are:



You can read here: how to install them

If you have questions let me know, I’ll make my best to answer them.
 

papel

Member
Game Developer
Sep 2, 2018
370
490
Anyways here is a working code snippet of the general approach:
Everything above the ###SCREENS### looks overengineered as fuck to me, at least considering how little actual logic is needed to achieve the result in the video and still be modular enough to grow. Effectively, you have 2 states (idle, kissing) and 3 actions (kiss, threaten, stop kissing). You only need to know what's your current state and what actions it can lead to, also keep track of the previous action if it doesn't immediately trigger a state change, but can affect chances later

In other words, it's better to tackle this problem like a database first. You have a table of states. You have a table of actions. Each state can have N possible actions, which may or may not trigger a state change. Therefore, the "complete identifier" is STATE + ACTION + NEXT_STATE
Actions may or may not have a side effect. Actions have at least ONE description.

Organizing this in a .csv file, it'd look something like this:
STATEACTIONNEXT_STATESIDE_EFFECTDESCRIPTION1
idlekissidle*"Your kiss was rejected"
idlekisskissing"You begin kissing"
idlethreatenidle*THREATENED"You threaten the girl"
kissingkisskissing*"You continue kissing"
kissingstopidle"You stop the kiss"
* It could either lead to the same state, or just be an empty value

You could also organize STATE and ACTION as separate tables/files, with a third being the RESULTS, which would point to the row number of STATE and ACTION while only listing the other columns.

With data organized like that, you could use an Array, always being aware of the column each position represents (0 = state, 1= action, 2=next_state, 3=side_effect, 4+ = description). The variable would be an array of arrays, or:
Code:
var full_state_machine = [
  [idle, kiss, <idle or empty>, <empty>, "your kiss was rejected"],
  [idle, kiss, kissing, <empty>, "your begin kissing"],
  [idle, threaten, <idle or empty>, THREATENED, "you threaten the girl"]
  #etc...
]
The problem here is that a big enough list of state-action will be hell to sift through, as you'll need to know the row number to get the appropriate data - full_state_machine[1][3] (next state of idle/kiss state/action) - You'd probably have to set up some lookup logic on the reading of the file, which again gets very close to a database organization. I suppose using SQLite would be faster than reading a csv.

The alternative to such an array is Dict, used by Python and Godot and can be nested. Manually transforming the above table into a Dict var would look something like this:

Python:
# PSEUDO CODE
# State - Action - Next State - Side Effect - Description
var state_machine = {
  "idle": {
    "kiss": { #***
      "idle": {
        "side_effect": ,
        "description1": "You tried to kiss, but were rejected"
      },
      "kissing": {
        "side_effect": ,
        "description1": "You started to kiss"
       }
     }, #***
    "threaten": {
      "idle": {side_effect: , description1: "You threaten the girl"}
    }

##***ALTERNATIVE##
    "kiss": [ #If you decide to have NEXT_STATE accept empty values, you can't use it as a Dict key
      #So, you have to use an array instead
      [ , , "You tried to kiss, but were rejected"], #empty next_state, empty side_effect
      ["kissing", , "Your lips entwine softly"]
    ]
   ##This alternative also works better with more than one description, as
   ## checking for [random(2, array.length - 1)] is easier than looking for keys and picking one at random

##
##Alright, so how to apply logic to this data?

var current_state = "idle" #initializes as idle
var action_list = { #this simply lists all the actions for each state
  "idle": ["kiss", "threaten"],
  "kissing": ["kiss", "stop"]
}
var current_side_effect = ""

#Logic for making the clickable buttons goes here
function show_menu():
  #current_state is available to all functions, as it was defined in the "class"/"file" scope
  print("Available actions:")
  for acti in action_list[current_state]:
    print(action_list[current_state][acti])

function show_description(state_key, action_key, next_state):
  print(state_machine[state_key][action_key][next_state]["description1"])
  #The array alternative would be state_machine[state_key][action_key][next_state][2], or any number above 2 for more descriptions

#Logic for clicking an action button
function execute_action(which_button):
  #which_button should be the action key identifier
  if state_machine[current_state][action].size() > 1: #if one action has more than one NEXT_STATE/outcome...
   #for Python, you'd use   len(state_machine[current_state][action])  to get the size
    next_state = state_machine[current_state][action].keys().random() #In Godot, Dict.keys() returns the list of keys.
    #Here, for simplicity, a random NEXT_STATE key is chosen, but this is where any actual logic for specific actions would go,
    #Possibly calling another function
  else:
    next_state = state_machine[current_state][action].keys()[0]

  show_description(current_state, which_button, next_state)
  if next_state != current_state:
    current_state = next_state
With that final bit at the bottom, supposing I can properly feed a state_machine variable from an external file, I can travel through any number of states and actions in a text-only game, even when a state/action can lead to N different states. If I need to insert a character name, I can leave a keyword in the description and use the replace function somewhere in the show_description: "PLAYERCHAR moves in to kiss CHARAC" -> desc = description.replace("CHARAC", passive_char) -> desc = desc.replace("PLAYERCHAR", active_char) == "Bobby moves in to kiss Maria"
 

Quintillian

Member
Apr 15, 2019
105
205
Yeah, I agree it is overengineered for how little it does in this example.

If you have a simple game with simple descriptions, some other approach might be best. But for that same reason, I think don't it is overengineered when compared with the games OP mentioned. In that context, I think it's just as complex as it needs to be.

I mean, I only added the kiss, reject, threaten cycle, to showcase a subset of what an entire sex encounter would be. Going from Lilith's throne's inspiration (which is the one I have more recently in mind), we are talking undressing, using objects, changing positions, tarjeting different parts of the body, etc. The amount of posible combinations just grow, and grow to the point you find yourself juggling nested if-else statements across months of development time, so you better document that thing right, or you're going to be lost when get back to it to add content or fix a bug.

That's way a Finite State Machine comes useful. At the one time payment cost of building the boilerplate code, adding in new states becomes easy to reason about, and more importantly, safe to do so.

Same thing for the descriptions. Using Fuzzy Pattern Matching, instead of having a bunch of ifs to determine which text to show among all the posibilities, you just make a big query, execute it to find a rule that matches, and you get a text. If none was found, then you know where writting needs to be added.

Being text-based that is the most important part of the game or close to it, so the last thing I as a sole developer want is to have my already limited writer inspiration be sucked dry because my programmer side is groaning at the chore of implementing all the logic required to support a new line of dialogue or description.

The approach above takes care of that worry for me.