inkle / ink

inkle's open source scripting language for writing interactive narrative.
http://www.inklestudios.com/ink
MIT License
4.12k stars 492 forks source link

Some example code - Traveler's Tale Game - a 5-character Party System #390

Closed CarterG81 closed 6 years ago

CarterG81 commented 6 years ago

In here I'd like to post some my Ink code, if it will be of any help to anyone as an example.'' The number 1 thing for me when learning a new language or trying to implement something awesome like INK, is seeing some examples of how other people are handling their game logic. It's sparse pickins in some areas (I'm making a lot more than just a purely text based game), so I really hope this helps others feel more comfortable with how they're doing things.

I am still trying to find out how to get as much of the Game Logic / Game Code into INK as possible. Preferably, I'd use Unity only for Rendering/Input, and maybe the Dice System (Conflict Resolution).

THE GAME: Traveler's Tale.

travelerstale

Type of Game: It basically plays as a mixture of a Board Game, a Choose your own Adventure, and a Dice Game. In other words: I'd really like to make a game that gives off the feel of a singleplayer "PnP" experience. One of my favorite aspects of PnP is the interactive on-the-fly storytelling, which INK is quite amazing it. The other aspect is the dice rolling. I love rolling those dice for conflict resolution. I'm also a big fan of board games, so the WorldMap travel/exploration like a board game is also awesome.

I am reminded heavily of some of my favorite interactive fiction games of old:

gallery_gaming-the-hobbit-3 thehobbit1982_7429

Curious Expedition's gorgeous scene art & complete lack of animation, really inspired me to create a modern day 2017 version of the above style of games.

Goal: Have as much of the Game Logic / Code in INK as possible, to allow Traveler's Tale to become so moddable it can be a sort of "Create your own Adventure" game. Write your own stories, create your own Items, use our art or your own, etc. What can't be done in Ink, hopefully will be done in exposing JSON. Moddability may vary in final product.

I have separate .ink files which I include in my game. These are my game's variables. Separating everything into their own .ink files. Each LOCATION is its own file, each playthrough is a single questline/storyline, and all of it is linked together with CORE.ink & lots of INCLUDE Locations/LocationName.ink


First, I have a "PARTY" .ink file called "PartyVariables.ink".

A "Party" in my game can have a maximum of 5 characters. One is always the player's hero, and the remaining 4 are crew members. This holds any relevant variables, such as how much GOLD the player has, the current MORALE of their entire party, and any other party-centric variables. (All characters are in a separate file).

The most important part of this class is actually the functions. This is where you can get knowledge of whether or not a PartyMember exists, as well as get a RandomPartyMember (including or excluding the HeroCharacter). This is paired with bool PartyMember#_Exists in the character file.

//Party Stats
VAR myMorale = 100 //The party's current Morale integer
VAR myGold = 0 //How much gold the party has
VAR DaysTraveled = 0 //How many days has the Party been traveling?

//This LIST is used to keep a record of who is currently in the party. As shown, only the HERO is currently in. So this party is empty.
LIST PartyMembers = (Hero) = 0, PartyMember1 = 1,  PartyMember2 = 2,  PartyMember3 = 3,  PartyMember4 = 4

//World Map
VAR currentTilePosition_X = 0 //World Map coordinate X
VAR currentTilePosition_Y = 0 //World Map coordinate Y
VAR LineOfSightDistance = 5 //The Line of Sight of the Party, relative to its tileMap
//VAR myCurrentRegion = "Grasslands" //This might need to be saved via Unity/ProD.Map

//This function gets a random (IN) party member.
=== function GetRandomPartyMember(includeHero) ===
{UpdatePartyMembersLIST()}

{includeHero:
~return LIST_RANDOM_IN(PartyMembers, 0)
-else:
    {LIST_COUNT(PartyMembers) == 1:
        ~return () //No PartyMembers & !includeHero, so nothing.
    -else:
        ~return LIST_RANDOM_IN(PartyMembers, 1) //Skip hero, who is always in, so start Random number at 1.
    }
}

//This function resets the ```PartyMembersList``` of who is IN or OUT, and then checks if a PartyMember is currently in based on the ```bool PartyMember#_Exists```
//So if you remove a party member, set PartyMember#_Exists = false, then the PartyMember will no longer be grabbed.
=== function UpdatePartyMembersLIST ===
~PartyMembers = (PartyMembers.Hero) //Clear LIST

{PartyMember1_Exists:
~PartyMembers += (PartyMembers.PartyMember1)
}

{PartyMember2_Exists:
~PartyMembers += (PartyMembers.PartyMember2)
}

{PartyMember3_Exists:
~PartyMembers += (PartyMembers.PartyMember3)
}

{PartyMember4_Exists:
~PartyMembers += (PartyMembers.PartyMember4)
}

//Check if Hero is alone or if there is at least 1 other party member
=== function CheckIf_HeroIsAlone ===
~temp PartyCountNum = LIST_COUNT(PartyMembers)
{PartyCountNum > 1:
~return false //Hero is NOT alone
-else: ~return true
}

//Get a random element, but only from partymembers who are (IN)
//bool skipHero = skipFirstElement
=== function LIST_RANDOM_IN(list, skipHero) ===
{skipHero:
    ~return LIST_VALUE(LIST_ENTRY_i(list,RANDOM(1, LIST_COUNT(list) - 1)))
-else:
    ~return LIST_VALUE(LIST_ENTRY_i(list,RANDOM(0, LIST_COUNT(list) - 1)))
}

//Equivalent to InPartyMembers[i] - will only iterate through PartyMembers that are (IN)
//example: VAR thirdActivePartyMember = List[3]
=== function LIST_ENTRY_i(list, i)
{ list:
       ~ temp entry = LIST_MIN(list)
       { i <= 0:
            ~ return entry 
       - else:
            ~ return LIST_ENTRY_i(list - entry, i - 1)
       }
- else:
     ~ return () // if you run out return empty list
}

Since Characters have a lot of VARiables, I put them in a different file called "CharacterData.ink"

A PartyMember has a Name, Race, Class, Gender, 3 inventory slots for items, and an IMAGE (Texture name) so Unity knows what to render. One of the most important parts here is PartyMember#_Exists = false . This tells whether or not a Character is still residing in their party slot. If set to false, there is no party member, so they will not be returned when using Party functions like GetRandomPartyMember().

Since we don't have Classes or Arrays/Containers, I simply have a different VAR for each variable, and copy-paste this with a different name, keeping with the convention PartyMember#.

SetPartyMember() is an important function. This creates a new character based on the parameters, and sets Exists = true.

Inventory has Getter/Setters for all 3 slots per character. The rest of the functions are just Getters which use a Switch.

//HERO
VAR Hero_Name = ""
VAR Hero_Race = ""
VAR Hero_Class = ""
VAR Hero_Gender = ""
VAR Hero_Image = ""
VAR Hero_InventorySlot_1 = ""
VAR Hero_InventorySlot_2 = ""
VAR Hero_InventorySlot_3 = ""

//PartyMember#1
VAR PartyMember1_Exists = false
VAR PartyMember1_Name = ""
VAR PartyMember1_Race = ""
VAR PartyMember1_Class = ""
VAR PartyMember1_Gender = ""
VAR PartyMember1_Image = ""
VAR PartyMember1_InventorySlot_1 = ""
VAR PartyMember1_InventorySlot_2 = ""
VAR PartyMember1_InventorySlot_3 = ""

//PartyMember#2
VAR PartyMember2_Exists = false
VAR PartyMember2_Name = ""
VAR PartyMember2_Race = ""
VAR PartyMember2_Class = ""
VAR PartyMember2_Gender = ""
VAR PartyMember2_Image = ""
VAR PartyMember2_InventorySlot_1 = ""
VAR PartyMember2_InventorySlot_2 = ""
VAR PartyMember2_InventorySlot_3 = ""

//PartyMember#3
VAR PartyMember3_Exists = false
VAR PartyMember3_Name = ""
VAR PartyMember3_Race = ""
VAR PartyMember3_Class = ""
VAR PartyMember3_Gender = ""
VAR PartyMember3_Image = ""
VAR PartyMember3_InventorySlot_1 = ""
VAR PartyMember3_InventorySlot_2 = ""
VAR PartyMember3_InventorySlot_3 = ""

//PartyMember#4
VAR PartyMember4_Exists = false
VAR PartyMember4_Name = ""
VAR PartyMember4_Race = ""
VAR PartyMember4_Class = ""
VAR PartyMember4_Gender = ""
VAR PartyMember4_Image = ""
VAR PartyMember4_InventorySlot_1 = ""
VAR PartyMember4_InventorySlot_2 = ""
VAR PartyMember4_InventorySlot_3 = ""

///DICE
//LIST DiceType = Attack, Defend, Support, Stealth, Perception, Charisma, Magic
//LIST CombatDiceType = Combat, Defense
//LIST ConflictDiceType = 

=== function SetPartyMember(partyNumber, Name, Race, Class, Gender, Image, Item1, Item2, Item3) ===
{partyNumber:
-0:
~Hero_Name = Name
~Hero_Race = Race
~Hero_Class = Class
~Hero_Gender = Gender
~Hero_Image = Image
~AddItemToInventorySlot(1,1,Item1)
~AddItemToInventorySlot(1,2,Item2)
~AddItemToInventorySlot(1,3,Item3)
-1:
~PartyMember1_Exists = true
~PartyMember1_Name = Name
~PartyMember1_Race = Race
~PartyMember1_Class = Class
~PartyMember1_Gender = Gender
~PartyMember1_Image = Image
~AddItemToInventorySlot(2,1,Item1)
~AddItemToInventorySlot(2,2,Item2)
~AddItemToInventorySlot(2,3,Item3)
-2:
~PartyMember2_Exists = true
~PartyMember2_Name = Name
~PartyMember2_Race = Race
~PartyMember2_Class = Class
~PartyMember2_Gender = Gender
~PartyMember2_Image = Image
~AddItemToInventorySlot(3,1,Item1)
~AddItemToInventorySlot(3,2,Item2)
~AddItemToInventorySlot(3,3,Item3)
-3:
~PartyMember3_Exists = true
~PartyMember3_Name = Name
~PartyMember3_Race = Race
~PartyMember3_Class = Class
~PartyMember3_Gender = Gender
~PartyMember3_Image = Image
~AddItemToInventorySlot(4,1,Item1)
~AddItemToInventorySlot(4,2,Item2)
~AddItemToInventorySlot(4,3,Item3)
-4:
~PartyMember4_Exists = true
~PartyMember4_Name = Name
~PartyMember4_Race = Race
~PartyMember4_Class = Class
~PartyMember4_Gender = Gender
~PartyMember4_Image = Image
~AddItemToInventorySlot(5,1,Item1)
~AddItemToInventorySlot(5,2,Item2)
~AddItemToInventorySlot(5,3,Item3)
}

=== function GetPartyMemberName(partyNumber) ===
{
- partyNumber == 0:
~ return Hero_Name
- partyNumber == 1:
~ return PartyMember1_Name
- partyNumber == 2:
~ return PartyMember2_Name
- partyNumber == 3:
~ return PartyMember3_Name
- partyNumber == 4:
~ return PartyMember4_Name
}
=== function GetPartyMemberClass(partyNumber) ===
{
- partyNumber == 0:
~ return Hero_Class
- partyNumber == 1:
~ return PartyMember1_Class
- partyNumber == 2:
~ return PartyMember2_Class
- partyNumber == 3:
~ return PartyMember3_Class
- partyNumber == 4:
~ return PartyMember4_Class
}
=== function GetPartyMemberRace(partyNumber) ===
{
- partyNumber == 0:
~ return Hero_Race
- partyNumber == 1:
~ return PartyMember1_Race
- partyNumber == 2:
~ return PartyMember2_Race
- partyNumber == 3:
~ return PartyMember3_Race
- partyNumber == 4:
~ return PartyMember4_Race
}
=== function GetPartyMemberGender(partyNumber) ===
{
- partyNumber == 0:
~ return Hero_Gender
- partyNumber == 1:
~ return PartyMember1_Gender
- partyNumber == 2:
~ return PartyMember2_Gender
- partyNumber == 3:
~ return PartyMember3_Gender
- partyNumber == 4:
~ return PartyMember4_Gender
}
=== function GetPartyMemberInventoryItem(partyNumber, inventorySlot)
{partyNumber:
-0:
    {inventorySlot:
    -1: ~return Hero_InventorySlot_1
    -2: ~return Hero_InventorySlot_2
    -3: ~return Hero_InventorySlot_3
    }
-1:
    {inventorySlot:
    -1: ~return PartyMember1_InventorySlot_1
    -2: ~return PartyMember1_InventorySlot_2
    -3: ~return PartyMember1_InventorySlot_3
    }
-2:
    {inventorySlot:
    -1: ~return PartyMember2_InventorySlot_1
    -2: ~return PartyMember2_InventorySlot_2
    -3: ~return PartyMember2_InventorySlot_3
    }
-3:
    {inventorySlot:
    -1: ~return PartyMember3_InventorySlot_1
    -2: ~return PartyMember3_InventorySlot_2
    -3: ~return PartyMember3_InventorySlot_3
    }
-4:
    {inventorySlot:
    -1: ~return PartyMember4_InventorySlot_1
    -2: ~return PartyMember4_InventorySlot_2
    -3: ~return PartyMember4_InventorySlot_3
    }

}

=== function AddItemToInventorySlot(partyNumber, inventorySlot, itemToAdd) ===
{partyNumber:
-0: 
    {inventorySlot:
    -1: ~Hero_InventorySlot_1 = itemToAdd
    -2: ~Hero_InventorySlot_2 = itemToAdd
    -3: ~Hero_InventorySlot_3 = itemToAdd
    }
-1: 
    {inventorySlot:
    -1: ~PartyMember1_InventorySlot_1 = itemToAdd
    -2: ~PartyMember1_InventorySlot_2 = itemToAdd
    -3: ~PartyMember1_InventorySlot_3 = itemToAdd
    }
-2: 
    {inventorySlot:
    -1: ~PartyMember2_InventorySlot_1 = itemToAdd
    -2: ~PartyMember2_InventorySlot_2 = itemToAdd
    -3: ~PartyMember2_InventorySlot_3 = itemToAdd
    }
-3: 
    {inventorySlot:
    -1: ~PartyMember3_InventorySlot_1 = itemToAdd
    -2: ~PartyMember3_InventorySlot_2 = itemToAdd
    -3: ~PartyMember3_InventorySlot_3 = itemToAdd
    }
-4: 
    {inventorySlot:
    -1: ~PartyMember4_InventorySlot_1 = itemToAdd
    -2: ~PartyMember4_InventorySlot_2 = itemToAdd
    -3: ~PartyMember4_InventorySlot_3 = itemToAdd
    }
}

When creating a new story, I set up all the characters, locations, starting gold/items, etc in a main file.

INCLUDE Core.ink //This is a file which includes all the data needed for ALL adventures. This includes PartyVariables/CharacterData, as well as other files.

//LORD RAGNAVALDRY AND THE KWAYNOSIAN CIVIL WAR//
//=== QUESTLINE ===//
//Antagonist
VAR OrangeArmyName = "Orange Army"

//=== QUEST1 - THE GREAT LIBRARY ===//
//Timeline
VAR QUEST1_DaysPassed_StillEarly = 60 //Achieved goal early
VAR QUEST1_DaysPassed_TooLateNow = 100 //Achieved goal late
//NAMING
VAR QUEST1_REGION = "Northeastern Kwaynosian Hills"
VAR GreatLibraryName = "The Great Kwaynosian Library"
//Locations
INCLUDE Locations/Old Battlefield/Old Battlefield.ink //Quest1 includes the Old Battlefield location.
INCLUDE Locations/Watchtower/Watchtower.ink //Quest1 includes Watchtower location.
//INCLUDE Locations/Peasant Town/Peasant Town.ink //Quest1 will need a Peasant Town location
//INCLUDE Locations/Peasant Village/Peasant Village.ink //Quest1 will need a Peasant Village location

//=== QUEST2 - KWAYNOS, CITY OF MEN
//Fill in Timeline Data, Naming, and Included Locations

//=== QUEST3 - THE RIVER ELVES

//=== QUEST4 - THE MISTY FOREST

//=== QUEST5 - ACADEMY, CITY OF SCIENCE

//=== QUEST6 - THE WIZARD'S TOWER

//===Global Variables===//
//Quest Data
VAR TotalQuestNumber = 6 //Total number of Quests

//Below is an example of how to create new characters, using the CharacterData.ink function.
//=== SET HERO ===//
//SET PARTY CHARACTER DATA
{SetPartyMember(0, "Lord Ragnavaldr", "Kwaynosian", "The Green Knight", "Male", "Lord Ragnavaldr", "Alcohol", "Rope", "Sword")}
{SetPartyMember(1, "Sir Jordan", "Kwaynosian", "Knight", "Male", "Kwaynosian_Knight_Male_vBrownHair_Green", "Armor", "Sword", "")}
{SetPartyMember(2, "Lady Katherine", "Kwaynosian", "Knight", "Female", "Kwaynosian_Knight_Female_vBlackHair_Green", "Rations", "Horn", "")}
{SetPartyMember(3, "Victor Pendragon", "Kwaynosian", "Crossbowman", "Male", "Kwaynosian_Crossbowman_Male_vNoHood_Green", "Bolts", "", "")}
{SetPartyMember(4, "Aylia", "Kwaynosian", "Bannerman", "Female", "Kwaynosian_Bannerman_Female_Green", "Flag", "", "")}
{UpdatePartyMembersLIST()}

//Output some story to make sure everything was set correctly.
Your Party consists of the following:
{Hero_Name}, {Hero_Class}
{PartyMember1_Exists: {PartyMember1_Name}, the {PartyMember1_Gender} {PartyMember1_Race} {PartyMember1_Class}. }
{PartyMember2_Exists: {PartyMember2_Name}, the {PartyMember2_Gender} {PartyMember2_Race} {PartyMember2_Class}. }
{PartyMember3_Exists: {PartyMember3_Name}, the {PartyMember3_Gender} {PartyMember3_Race} {PartyMember3_Class}.}
{PartyMember4_Exists: {PartyMember4_Name}, the {PartyMember4_Gender} {PartyMember4_Race} {PartyMember4_Class}.}
//SET GOLD
~myGold = 100
{myGold} Gold.
//START
-> Prologue

I hope that helps. If you think there's a better way to do any of this, I am all ears. I don't pretend to be an expert. It's very difficult for me to engineer this without the use of Classes/Objects/Arrays.

willhowlett commented 6 years ago

Thanks for sharing this. Just starting having a play with Ink and interesting to see how other people are using it

CarterG81 commented 6 years ago

Currently I am restructuring Traveler's Tale to be a highly moddable, nearly completely Ink based game.

To do this, I am creating my own game engine using Unity & Ink. Basically, I ak creating an Ink parser, interpreted by C# code (the engine) and then rendered/output using Unity.

I wanted a way to write my game within Ink. So I am using my own set of API commands. What cant / shouldnt be in INK will likely be done in JSON (maybe procedural generation logic or tilemap data).

The Unity side will handle input, but assisted by Ink tag commands. "#DELAY:1" "#AUTOCONTINUE", "#RenderGUI:Backpack", etc.

Same for Rendering

"Some story text written here. #Background:Spaceship12.png #AddCharacter(Clown2.png, Position, Flipped) #AddProp(Barrel.png, Position, Flipped) #PlayAudio:Music7.ogg

Etc. Etc.

Unity side would include GameModes, which do more than just display Text & Images, but a WorldMap travel & movement, inventory system, combat positioning system, and a Polyhedral Dice mechanic.

Eventually I would like to sell my game not just as a full game experience, but (as a bonus) also as a Command-Filled, fully featured Ink engine for gamers to write theit own fan-fiction or make their own games. Editing everything, choosing any systems, or replacing them entirely if distributed as a unity C# project alongside the game build.

Right now I am part time gamedev... so I need some way to generate revenue to go full time. I'd prefer to make a custom engine for Ink (maybe even using SDL or Godot instead of Unity) and have it be free & open source...to give back and grow Ink & gamedev - maybe adding lots of requested commands / features, but I need to finish my game first to afford making a fully featured engine for ppl to make games in.

With my second project Away Mission, since it requires animation in scenes, I hope to update this engine idea to a 2.0 version which adds animation and a lot of AI commands (for animation or story). But done using Ink writing. Commands like #AI.Pathfind(Engineer, Scientist) #AI.Interact(Scientist, ScienceConsole) which begin animating inbetween or alongside text. To tell a more animated, interactive, procedural story

But clearly those are rough draft plans for much later. I will just be creating Traveler's Tales with this in mind. Write a story in a text editor using Ink & then just have the engine parse the text and tags into an actual game.

CarterG81 commented 6 years ago

Maintaining updates here along with the devlog (if I can even maintain that) is a bit too much for me. I'll close this for now. The devblog is here https://forum.unity.com/threads/travelers-tale-a-party-based-procedural-storytelling-travel-game.505682/