Skip to content

Player Input Guide

Learn how to collect text and image input from players to create interactive, personalized story experiences.

Overview

Player input allows you to:

  • Collect custom text - Names, locations, answers, descriptions
  • Request image selection - Photos from the player's gallery
  • Personalize the story - Use player's responses in dialogue
  • Create interactive investigations - Gather witness statements, evidence
  • Validate and process input - Check responses and react accordingly
  • Integrate with AI tools - Analyze images with object detection

Types of Player Input

Text Input

Collect free-form text from the player using the #text_input: tag.

Use cases:

  • Character names
  • Location descriptions
  • Witness statements
  • Answers to questions
  • Passwords or codes
  • Custom responses

Image Attachment

Request the player to select an image from their gallery using the #attach_image: tag.

Use cases:

  • Identity verification
  • Evidence submission
  • Location photos
  • Proof of completion
  • Visual clues

Text Input

Basic Text Input

Structure:

ink
* [Continue]
  # text_input:Prompt text here
  ~ variable_name = GetPlayerInput()
  -> next_knot

Example - Asking for a Name:

ink
EXTERNAL GetPlayerInput()

VAR player_name = ""

-> start

== start ==
Can you tell me your name? #typing:2s
-> ask_name

== ask_name ==
* [Continue]
  # text_input:Enter your name
  ~ player_name = GetPlayerInput()
  -> confirm_name

== confirm_name ==
Thank you, {player_name}. Nice to meet you! #delay:2s
-> END

How It Works

  1. Display the prompt - The message before the choice asks the question
  2. Show input field - The #text_input: tag displays a text input field
  3. Player types - Player enters their response
  4. Retrieve input - GetPlayerInput() gets the text they entered
  5. Store in variable - Save to a variable for later use
  6. Use in dialogue - Reference the variable with {variable_name}

Important Requirements

Must use with a single-option choice:

ink
* [Continue]  // Single choice only
  # text_input:Your prompt

Cannot use with multiple choices:

ink
// ❌ WRONG - Don't do this
* [Option 1]
  # text_input:Enter something
* [Option 2]
  # text_input:Enter something else

Declare external function:

ink
EXTERNAL GetPlayerInput()

Declare variable to store input:

ink
VAR player_name = ""

Complete Example - Detective Interview

Based on the detective.ink example from the sample story:

ink
EXTERNAL GetPlayerInput()
EXTERNAL GetPlayerName()
EXTERNAL GetPlayerFirstName()

VAR player_name = ""
VAR witness_location = ""

-> start

== start ==
This is Detective Morgan from the Metropolitan Police. #initial
I'm investigating a missing person case. #initial
I need to ask you a few questions. First, can you tell me your name? #initial
-> ask_name

== ask_name ==
* [Continue]
  # text_input:Enter your name
  ~ player_name = GetPlayerInput()
  -> confirm_name

== confirm_name ==
Thank you, {player_name}. I appreciate your cooperation. #delay:2s #typing:3s

// Optional: Cross-check with actual player name
{player_name != GetPlayerName():
  Wait... I have your details here. Your real name is {GetPlayerFirstName()}, isn't it? #delay:2s
  No worries, let's continue. #delay:2s
}

Now, you mentioned you found Sarah's phone. Can you tell me exactly where you found it? #delay:3s
-> ask_location

== ask_location ==
* [Continue]
  # text_input:Where did you find the phone?
  ~ witness_location = GetPlayerInput()
  -> confirm_location

== confirm_location ==
I see. You found it at {witness_location}. That's very helpful information. #delay:2s #typing:4s
We'll need to send a forensics team to that location immediately. #delay:3s
-> END

Using Player Input in Dialogue

Reference the variable:

ink
Thank you, {player_name}.
You found it at {witness_location}.
So, {player_name}, what do you think about {witness_location}?

Use in conditionals:

ink
{player_name == "Sarah":
    Wait, you're Sarah? But you're supposed to be missing!
- else:
    Okay, {player_name}, let's continue.
}

Combine with string functions:

ink
~ temp name_upper = StringToUpper(player_name)
~ temp name_length = StringLength(player_name)

{name_length < 2:
    That name seems too short. Are you sure?
- else:
    {name_upper}, got it.
}

Input Validation

Check if empty:

ink
~ temp input = GetPlayerInput()
~ temp cleaned = StringTrim(input)

{StringIsEmpty(cleaned):
    You didn't enter anything. Please try again.
    -> ask_name
- else:
    ~ player_name = cleaned
    -> confirm_name
}

Check length:

ink
~ temp input = GetPlayerInput()

{StringLength(input) < 3:
    That's too short. Please enter at least 3 characters.
    -> ask_name
- StringLength(input) > 50:
    That's too long. Please keep it under 50 characters.
    -> ask_name
- else:
    ~ player_name = input
    -> confirm_name
}

Pattern matching:

ink
~ temp email = GetPlayerInput()

{StringMatchesPattern(email, "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"):
    Email saved: {email}
    -> END
- else:
    That doesn't look like a valid email address. Please try again.
    -> ask_email
}

Image Attachment

Basic Image Attachment

Structure:

ink
* [Continue]
  # attach_image:Prompt text here
  ~ variable_name = GetAttachedGalleryItem()
  -> next_knot

Example - Identity Verification:

ink
EXTERNAL GetAttachedGalleryItem()

VAR attached_item = ""

-> start

== start ==
Can you send me a photo for identity verification? #typing:2s
-> request_photo

== request_photo ==
* [Continue]
  # attach_image:Select a photo for identity verification
  ~ attached_item = GetAttachedGalleryItem()
  -> verify_photo

== verify_photo ==
Thank you. I'll review this photo. #delay:2s
-> END

How It Works

  1. Display the prompt - The message before the choice asks for the image
  2. Show gallery picker - The #attach_image: tag opens the gallery
  3. Player selects - Player chooses an image from their gallery
  4. Retrieve ID - GetAttachedGalleryItem() gets the gallery item ID
  5. Store in variable - Save the ID for later use
  6. Process or validate - Check which image was selected

Important Requirements

Must use with a single-option choice:

ink
* [Continue]  // Single choice only
  # attach_image:Your prompt

Declare external function:

ink
EXTERNAL GetAttachedGalleryItem()

Declare variable to store item ID:

ink
VAR attached_item = ""

Complete Example - Photo Evidence Submission

Based on the detective.ink example:

ink
EXTERNAL GetAttachedGalleryItem()
EXTERNAL GetStoryToolsStatus()
EXTERNAL DetectObjectInGalleryImage(galleryItemId, objectName)

VAR attached_item = ""
VAR tools_enabled = 0
VAR face_confidence = 0

-> start

== start ==
Before we continue, I need to verify your identity for security purposes. #delay:3s
Can you send me a photo that clearly shows your face? #delay:2s
-> request_face_photo

== request_face_photo ==
* [Continue]
  # attach_image:Select a photo for identity verification
  ~ attached_item = GetAttachedGalleryItem()
  -> verify_face_photo

== verify_face_photo ==
~ tools_enabled = GetStoryToolsStatus()

{tools_enabled == 1:
  Let me run this through our facial recognition system... #delay:2s #typing:3s
  ~ face_confidence = DetectObjectInGalleryImage(attached_item, "face")
  
  {face_confidence > 0.7:
    Perfect. I can clearly see a face in this photo. Identity verified. #delay:2s
  - face_confidence > 0.3:
    Hmm, the image quality isn't great, but I can make out a face. That'll do. #delay:2s
  - else:
    I'm having trouble detecting a face in this photo. #delay:2s
    But we'll proceed anyway - time is of the essence. #delay:2s
  }
- else:
  Thank you. I'll review this manually later. #delay:2s
}
-> END

Checking Which Image Was Selected

Match specific gallery items:

ink
~ attached_item = GetAttachedGalleryItem()

{attached_item == "crime-scene-photo":
  This is the crime scene photo. Excellent. #delay:2s
- attached_item == "witness-photo":
  Ah, the witness photo. This could be useful. #delay:2s
- attached_item == "sarah-selfie":
  This is Sarah's selfie from that night. #delay:2s
- else:
  Thank you for sending this. We'll analyze it. #delay:2s
}

Example - Multiple Photo Options:

ink
EXTERNAL GetAttachedGalleryItem()

VAR attached_item = ""

== ask_for_photo ==
I need you to send me a photo of where you found the phone. #delay:2s
-> receive_photo

== receive_photo ==
* [Continue]
  # attach_image:Select a photo from your gallery
  ~ attached_item = GetAttachedGalleryItem()
  -> process_photo

== process_photo ==
{attached_item == "found-phone":
  This is excellent. This photo shows the exact location. #delay:2s #typing:4s
  I can see some important details in the background. #delay:3s
  We'll send a forensics team there right away. #delay:2s
- attached_item == "sample-photo":
  Interesting choice. This might be relevant. #delay:2s
- attached_item == "sample-video":
  Actually, this appears to be a video file, not a photo. #delay:2s #typing:3s
  But it might still be useful. Let me review it. #delay:2s
- else:
  Thank you for sending this. We'll analyze it for any clues. #delay:2s #typing:3s
}
-> END

AI-Powered Image Analysis

Object Detection

Use the DetectObjectInGalleryImage() function to detect objects in player-submitted images.

Setup:

ink
EXTERNAL GetStoryToolsStatus()
EXTERNAL DetectObjectInGalleryImage(galleryItemId, objectName)

VAR tools_enabled = 0
VAR confidence = 0

Check if tools are available:

ink
~ tools_enabled = GetStoryToolsStatus()

{tools_enabled == 1:
  Our AI tools are online. #delay:2s
- else:
  Our systems are offline. We'll proceed manually. #delay:2s
}

Detect objects:

ink
~ confidence = DetectObjectInGalleryImage(attached_item, "face")

{confidence > 0.7:
  I can clearly see a face in this photo. #delay:2s
- confidence > 0.3:
  There might be a face, but the quality is poor. #delay:2s
- else:
  I don't see a face in this photo. #delay:2s
}

Common Objects to Detect

People:

  • "face" - Human faces
  • "person" - Full person

Vehicles:

  • "car" - Cars
  • "truck" - Trucks
  • "motorcycle" - Motorcycles

Objects:

  • "phone" - Mobile phones
  • "weapon" - Weapons
  • "bag" - Bags or backpacks

Example - Multi-Object Detection:

ink
EXTERNAL GetAttachedGalleryItem()
EXTERNAL DetectObjectInGalleryImage(galleryItemId, objectName)

VAR attached_item = ""
VAR has_face = 0
VAR has_car = 0
VAR has_weapon = 0

== analyze_evidence_photo ==
* [Continue]
  # attach_image:Send the evidence photo
  ~ attached_item = GetAttachedGalleryItem()
  -> process_evidence

== process_evidence ==
Let me analyze this photo... #delay:2s #typing:3s

~ has_face = DetectObjectInGalleryImage(attached_item, "face")
~ has_car = DetectObjectInGalleryImage(attached_item, "car")
~ has_weapon = DetectObjectInGalleryImage(attached_item, "weapon")

{has_face > 0.5:
  I can see a person in this photo. #delay:2s
}

{has_car > 0.5:
  There's a vehicle visible. #delay:2s
}

{has_weapon > 0.5:
  Wait... is that a weapon? This is serious. #delay:2s
}

{has_face < 0.3 && has_car < 0.3 && has_weapon < 0.3:
  I'm not detecting anything significant in this photo. #delay:2s
}
-> END

Combining Text and Image Input

Sequential Input

Example - Complete Witness Statement:

ink
EXTERNAL GetPlayerInput()
EXTERNAL GetAttachedGalleryItem()

VAR witness_name = ""
VAR witness_location = ""
VAR evidence_photo = ""

-> start

== start ==
I need to take your statement. First, what's your name? #typing:2s
-> ask_name

== ask_name ==
* [Continue]
  # text_input:Enter your name
  ~ witness_name = GetPlayerInput()
  -> ask_location

== ask_location ==
Thank you, {witness_name}. Where exactly did you see the suspect? #delay:2s #typing:3s
* [Continue]
  # text_input:Describe the location
  ~ witness_location = GetPlayerInput()
  -> ask_photo

== ask_photo ==
You said you saw them at {witness_location}. #delay:2s
Did you take any photos? #typing:2s
* [Yes, I have a photo]
  -> request_photo
* [No, I didn't take any photos]
  -> no_photo
-> END

== request_photo ==
Please send me the photo. #delay:1s
* [Continue]
  # attach_image:Select the photo you took
  ~ evidence_photo = GetAttachedGalleryItem()
  -> process_statement

== no_photo ==
That's okay, {witness_name}. Your description is still helpful. #delay:2s
-> process_statement

== process_statement ==
{evidence_photo != "":
  Thank you, {witness_name}. We have your statement and the photo from {witness_location}. #delay:2s #typing:4s
- else:
  Thank you, {witness_name}. We have your statement about {witness_location}. #delay:2s #typing:3s
}
This information will be very helpful to the investigation. #delay:2s
-> END

Best Practices

Text Input

Clear prompts:

ink
# text_input:Enter your full name
# text_input:Where did you find the phone?
# text_input:Describe what you saw

Avoid vague prompts:

ink
# text_input:Enter something  // ❌ Too vague
# text_input:Type here        // ❌ Not helpful

Validate input:

ink
~ temp input = GetPlayerInput()
~ temp cleaned = StringTrim(input)

{StringIsEmpty(cleaned):
    Please enter something.
    -> ask_again
}

Provide context:

ink
I need your full legal name for the official report. #typing:2s
* [Continue]
  # text_input:Enter your full name (First and Last)

Image Attachment

Specific prompts:

ink
# attach_image:Select a photo for identity verification
# attach_image:Send the crime scene photo
# attach_image:Choose the photo you took at the location

Explain why:

ink
I need to verify your identity for security purposes. #delay:2s
Can you send me a photo that clearly shows your face? #typing:2s
* [Continue]
  # attach_image:Select a photo for identity verification

Handle different selections:

ink
{attached_item == "expected-photo":
  Perfect, this is exactly what I needed. #delay:2s
- else:
  Hmm, this isn't quite what I was expecting, but it might still be useful. #delay:2s
}

User Experience

Don't overwhelm:

ink
// ❌ Too many inputs at once
-> ask_name
-> ask_age
-> ask_location
-> ask_photo
-> ask_email

// ✅ Space them out
-> ask_name
// ... story progression ...
-> ask_location
// ... more story ...
-> ask_photo

Acknowledge input:

ink
~ player_name = GetPlayerInput()

Thank you, {player_name}. #delay:1s
// Always acknowledge what they entered

Make it feel natural:

ink
// ❌ Robotic
Enter your name.
* [Continue]
  # text_input:Name

// ✅ Natural
Before we continue, I need to know who I'm speaking with. #typing:2s
What's your name? #delay:1s
* [Continue]
  # text_input:Enter your name

Troubleshooting

Input not working:

  • Ensure you have a single-option choice: * [Continue]
  • Check external function is declared: EXTERNAL GetPlayerInput()
  • Verify variable is declared: VAR player_name = ""

Empty input:

  • Use StringTrim() to remove whitespace
  • Check with StringIsEmpty() before proceeding
  • Provide validation and ask again if needed

Image not recognized:

  • Verify the gallery item ID matches exactly
  • Check the item exists in src/gallery/
  • Remember: ID is the filename without .json

AI detection not working:

  • Check GetStoryToolsStatus() returns 1
  • Ensure API is configured and running
  • Provide fallback for when tools are offline

Complete Example - Police Interview

Here's a complete, realistic example combining everything:

ink
EXTERNAL GetPlayerInput()
EXTERNAL GetAttachedGalleryItem()
EXTERNAL GetPlayerName()
EXTERNAL GetPlayerFirstName()
EXTERNAL GetStoryToolsStatus()
EXTERNAL DetectObjectInGalleryImage(galleryItemId, objectName)

VAR player_name = ""
VAR witness_location = ""
VAR attached_item = ""
VAR tools_enabled = 0
VAR face_confidence = 0

-> start

== start ==
This is Detective Morgan from the Metropolitan Police. #initial
I'm investigating a missing person case. #initial
I need to ask you a few questions. #initial
First, can you tell me your name? #delay:2s
-> ask_name

== ask_name ==
* [Continue]
  # text_input:Enter your name
  ~ player_name = GetPlayerInput()
  -> confirm_name

== confirm_name ==
Thank you, {player_name}. I appreciate your cooperation. #delay:2s #typing:3s

{player_name != GetPlayerName():
  Wait... I have your details here. Your real name is {GetPlayerFirstName()}, isn't it? #delay:2s
  No worries, let's continue. #delay:2s
}

Let me check our system status... #delay:2s
~ tools_enabled = GetStoryToolsStatus()

{tools_enabled == 1:
  Our advanced investigation tools are online. #delay:2s
- else:
  Our systems are currently offline, but we'll manage. #delay:2s
}

Before we continue, I need to verify your identity for security purposes. #delay:3s
Can you send me a photo that clearly shows your face? #delay:2s
-> request_face_photo

== request_face_photo ==
* [Continue]
  # attach_image:Select a photo for identity verification
  ~ attached_item = GetAttachedGalleryItem()
  -> verify_face_photo

== verify_face_photo ==
{tools_enabled == 1:
  Let me run this through our facial recognition system... #delay:2s #typing:3s
  ~ face_confidence = DetectObjectInGalleryImage(attached_item, "face")
  
  {face_confidence > 0.7:
    Perfect. I can clearly see a face in this photo. Identity verified. #delay:2s
  - face_confidence > 0.3:
    Hmm, the image quality isn't great, but I can make out a face. That'll do. #delay:2s
  - else:
    I'm having trouble detecting a face in this photo. #delay:2s
    But we'll proceed anyway - time is of the essence. #delay:2s
  }
- else:
  Thank you. I'll review this manually later. #delay:2s
}

Now, you mentioned you found Sarah's phone. #delay:3s
Can you tell me exactly where you found it? #typing:2s
-> ask_location

== ask_location ==
* [Continue]
  # text_input:Where did you find the phone?
  ~ witness_location = GetPlayerInput()
  -> confirm_location

== confirm_location ==
I see. You found it at {witness_location}. That's very helpful information. #delay:2s #typing:4s
We'll need to send a forensics team to that location immediately. #delay:3s

Thank you for your cooperation, {player_name}. #delay:2s
If you remember anything else, please contact me immediately. #typing:3s
-> END

Next Steps

Released under the MIT License.