Tracking Typewriter Text State

0 favourites
  • 6 posts
From the Asset Store
State Machine
$10.99 USD
State Machine is a great tool for managing the state of anything in your game.
  • Using the typewriterText(str, duration) method on ITextInstance, is there a way to track added characters or know when typing ends? The instance's text property always reflects the value of the str argument, not the current text state. Here's a simplified code example:

    runOnStartup(async runtime => {
    	runtime.addEventListener('beforeprojectstart', () => onBeforeProjectStart(runtime));
    });
    
    async function onBeforeProjectStart(runtime: IRuntime) {
    	const str = 'Hello, World! This is a typewriter effect.';
    	const duration = str.length / 30;
    	const textInst = runtime.objects.Text.getFirstInstance()!;
    	textInst.typewriterText(str, duration);
    
    	runtime.addEventListener('tick', () => {
    		console.log(textInst.text === str); // Always true
    	});
    }
    
  • If anyone needs a workaround, here's my quick implementation (in TypeScript). There's certainly room for improvement, but it solves my issue for now:

    Example Usage

    import Typewriter from './Typewriter.js';
    
    // ...
    
    // Create a new Typewriter instance.
    const textInst = runtime.objects.Text.getFirstInstance()!;
    const typewriter = new Typewriter(textInst);
    
    // Callback executed when a character is added.
    typewriter.onTypeHandler = (char, _) => {
    	console.log(`Typed character: ${char}`);
    }
    
    // Callback executed when typing finishes.
    typewriter.onFinishedHandler = text => {
    	console.log(`Finished typing text: ${text}`);
    }
    
    // Start the effect with the specified arguments.
    const text = 'Hello, World! This is a typewriter effect with callbacks.';
    const charsPerSecond = text.length / 30;
    typewriter.start(text, charsPerSecond);
    

    Typewriter.ts

    export default class Typewriter {
    	readonly textInstance: ITextInstance;
    
    	onTypeHandler: ((char: string, text: string) => void) | null = null;
    	onFinishedHandler: ((text: string) => void) | null = null;
    
    	get isTyping() { return this.charIndex > -1; }
    
    	private text = '';
    	private charIndex = -1;
    	private charsPerSecond = 0;
    	private timeSinceLastType = 0;
    
    	constructor(textInstance: ITextInstance) {
    		this.textInstance = textInstance;
    		this.onTick = this.onTick.bind(this);
    		this.textInstance.runtime.addEventListener('tick', this.onTick);
    	}
    
    	destroy() {
    		this.textInstance.runtime.removeEventListener('tick', this.onTick);
    	}
    
    	start(text: string, duration: number) {
    		if (this.isTyping) this.finish();
    
    		this.text = text;
    
    		if (text.length > 0 && duration > 0) {
    			this.charIndex = 0;
    			this.charsPerSecond = text.length / duration;
    			this.timeSinceLastType = 0;
    			this.textInstance.text = '';
    		} else {
    			this.finish();
    		}
    	}
    
    	finish() {
    		this.charIndex = -1;
    		this.textInstance.text = this.text;
    		this.onFinishedHandler?.(this.text);
    	}
    
    	private onTick() {
    		if (!this.isTyping) return;
    
    		while (this.isTyping && this.timeSinceLastType >= 1 / this.charsPerSecond) {
    			this.type();
    			this.timeSinceLastType -= 1 / this.charsPerSecond;
    		}
    
    		this.timeSinceLastType += this.textInstance.runtime.dt;
    	}
    
    	private type() {
    		if (this.charIndex === this.text.length - 1) {
    			this.finish();
    			return;
    		}
    
    		// Type the next character.
    		const char = this.text.charAt(this.charIndex);
    		this.textInstance.text += char;
    		this.onTypeHandler?.(char, this.textInstance.text);
    		this.charIndex++;
    
    		// Add a line break if the next word would exceed the text width.
    		if (char === ' ' && this.textInstance.wordWrapMode === 'word' && this.nextWordWillWrap()) {
    			this.textInstance.text =
    				this.textInstance.text.substring(0, this.charIndex - 1) + '\n' +
    				this.textInstance.text.substring(this.charIndex);
    		}
    	}
    
    	private nextWordWillWrap(): boolean {
    		const lines = this.textInstance.text.split('\n');
    		const currentLine = lines[lines.length - 1] || '';
    		const nextSpaceCharIndex = this.text.indexOf(' ', this.charIndex);
    		const nextWordEndIndex = (nextSpaceCharIndex === -1) ? undefined : nextSpaceCharIndex;
    		const nextWord = this.text.substring(this.charIndex, nextWordEndIndex);
    		const lineWidth = this.getLineWidth(currentLine + nextWord);
    
    		return lineWidth > this.textInstance.width;
    	}
    
    	private getLineWidth(text: string): number {
    		// Cache the text instance's state.
    		const originalText = this.textInstance.text;
    		const originalWidth = this.textInstance.width;
    
    		// Measure the full width of the line.
    		this.textInstance.text = text;
    		this.textInstance.width = Infinity;
    		const lineWidth = this.textInstance.textWidth;
    
    		// Restore the text instance's state.
    		this.textInstance.text = originalText;
    		this.textInstance.width = originalWidth;
    
    		return lineWidth;
    	}
    }
    
  • Overall looking pretty good!

    That does seem to be the best way of handling a typewriter effect if you need to track each letter

    The biggest boost to performance I can see here would be to when you start the typing, parse the string for the whitespace, split out into the word char arrays, then you don't need to test every single character typed to see if you are gonna word break, or find the edge of the word each time!

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • Thanks for the feedback! That's definitely worth considering.

    With that said, unless the current character is a whitespace, I don't believe line 69 tests if the next word will wrap (since the logical AND expression is a short-circuit operator).

  • Yeah it SHOULD do, so it wont test EVERY character, only if its a space and if you are word wrapping

    Its probably a personal preference, I would keep track of more states like Current Line and Line Length than calculating as you go

    But as I say, overall, does the job!

  • Gotcha. Yeah, those would be nice performance optimizations to make. Thanks again for the suggestions.

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)