No announcement yet.

Auto-scaling chat bubbles (incl AS2.0 and UnrealScript)

  • Filter
  • Time
  • Show
Clear All
new posts

    Auto-scaling chat bubbles (incl AS2.0 and UnrealScript)

    It took me a day of fighting to make this work, so hopefully I can save someone else the time...

    The goal of this system was to be able to specify a max size for an on-screen chat bubble, and for that chat bubble to then auto-scale down if the bubble was larger than necessary.

    Pretty simple system, and critical for an RPG. The alternative would be to hand-place all line breaks in your localization file, and to make sure those made sense if the user increased the text size in the options menu, and to hard-code the appropriate bubble size for each, and... just... ugh, yeah. I did that for my last project. This is better, trust me.

    Step #1: The ActionScript

    This is the trickiest bit, hence it being in the UI forum.

    var next_bubble_id:Number = 0;
    // Creates a dialog bubble, and returns its ID
    function create_dialog_bubble(bubble_x:Number, bubble_y:Number, bubble_width:Number, bubble_height:Number, bubble_dialog:String):Number {	
    	var bubble:MovieClip;
    	var bubble_txt:MovieClip;
    	// Track the unique bubble ID.
    	// (In the unlikely event that we spawn 60,000 dialog bubbles, pretty sure we can safely loop around.)
    	if (bubble_id > 60000)
    		bubble_id = 0;
    	// Create the bubble and the txt element for the dialog bubble
    	// (we create them separately since, if the txt is inside the bubble and inherits its width/height, it stretches
    	// rather than properly resizing the text area - Flash gets it right, but ScaleForm does not)
    	bubble = _root.attachMovie("dialog_bubble", "dialog_bubble" + next_bubble_id, this.getNextHighestDepth()); 
    	bubble_txt = _root.attachMovie("dialog_bubble_txt", "dialog_bubble" + next_bubble_id + "_txt", this.getNextHighestDepth()); 
    	// Figure out the buffer size defined between the bubble and text edges - we'll need
    	// it later, to adjust calcs.
    	var width_diff:Number = bubble._width - bubble_txt.dialog._width;
    	var height_diff:Number = bubble._height - bubble_txt.dialog._height;
    	// Give the bubble its maximum possible size
    	bubble._width = bubble_width;
    	bubble._height = bubble_width;
    	bubble_txt.dialog._width = bubble._width - width_diff;
    	bubble_txt.dialog._height = bubble._height - height_diff;
    	// Next, set out the dialog for this bubble, because...
    	bubble_txt.dialog.text = bubble_dialog;
    	// Now we're going to enable auto-sizing on the text window.  This will shrink
    	// it down to the minimum possible size.
    	bubble_txt.dialog.autoSize = TextFieldAutoSize.LEFT;
    	// ... and since that takes effect immediately, we can now read out the change
    	// in dimenion - and apply that to the bubble as well.
    	bubble._height = bubble_txt.dialog._height + height_diff;
    	// Super trick - now we turn of word-wrapping, to catch the case where resizing
    	// makes the window less wide
    	var old_width:Number = bubble_txt.dialog._width;
    	bubble_txt.dialog.wordWrap = false;
    	if (bubble_txt.dialog._width < old_width)
    		// If it's smaller now, ok, just re-enable word-wrap
    		bubble_txt.dialog.wordWrap = true;
    		bubble_txt.dialog.autoSize = TextFieldAutoSize.NONE;
    		// Otherwise, re-enable wrap and immediately correct the width change back
    		bubble_txt.dialog.wordWrap = true;
    		bubble_txt.dialog.autoSize = TextFieldAutoSize.NONE;
    		bubble_txt.dialog._width = old_width;
    	// Update the bubble width to match whatever change took place in the text width
    	bubble._width = bubble_txt.dialog._width + width_diff;
    	// Finally, set the centered the position as appropriate for the new width
    	bubble._x = bubble_x - (bubble._width / 2.0);
    	bubble._y = bubble_y - bubble._height;
    	bubble_txt._x = bubble._x;
    	bubble_txt._y = bubble._y;
    	// Then we're done!
    	return next_bubble_id;
    // Destroys a dialog bubble by ID
    function destroy_dialog_bubble(bubble_id:Number)
    ... you'll notice I use autoSize in there. This has a quirk: that code won't work in vanilla Flash. Flash normally seems to evaluate autoSize during some update cycle, rather than immediately. In ScaleForm though, that calc is done immediately - so this'll work in your ScaleForm debug view, and in UDK proper.

    I use autoSize rather than trying to calculate the true width of the text field myself because... well just look at this: - Those are the rules for true line length. To get the true minimum textField dimensions, I would have to run those calculations on every single line, determine the longest, fix the inevitable bugs, etc. My brain melted about an hour in. Just Don't Do It (tm), the autoSize works fine for us in this case.

    Other quirk - I separate the 'text field' part of the bubble from the background. Why? Because while Flash understands what to do with a textfield in a 9-sliced movieclip (ie. adjust the text field size, so that the text size stays the same while the image gets bigger, as opposed to scaling the text), ScaleForm does not. So you need to separate the textfield from the background. You also need both to have a zero point in their upper lefthand corners, so that when you adjust the width and the height of the textfield, that extra spaces goes in the right direction (it'll always get tacked onto the right/bottom).

    When you're doing the assets, start by making your 9-slice dialog bubble backdrop (and turn it into a movieclip). Now, draw into that where you want the textfield to be (and turn that textfield into a movieclip - and delete it from the 9-slice). Then, into 9-slice, temporarily place your textfield movieclip, with position 0,0. Now edit the textfield movieclip and nudge the contained textfield around until it lines up how you want in the 9-slice backdrop movieclip. Then delete the temp textfield movieclip from the 9-slice movieclip. To do a final test, place both the 9-slice backdrop and the textfield movieclip into your scene somewhere, and give them equivalent positions. If they line up properly - success!

    Ok! That's the hard part, done!

    Step #2: The UnrealScript

    This is easy, you just need a way of calling the AS from UDK-side. In my approach, I have my own HUDGfx class, and these go in it, which I then call whenever I want to throw up a bubble:

    /** This is the wrapper for the Flash-side bubble that actually makes the chat bubble, and returns its ID **/
    function int FlashCreateDialogBubble(float bubble_x, float bubble_y, float bubble_width, float bubble_height, string bubble_dialog)
    	return ActionScriptInt("_root.create_dialog_bubble");
    /** This is the wrapper for the Flash-side bubble that actually destroys the chat bubble associated with the given ID **/
    function FlashDestroyDialogBubble(int bubble_id)
    ... but, there are other ways of handling the UnrealScript/Kismet->ScaleForm tie, and it's up to you which you want to use.

    Here's some mostly programmer art of how it looks in our game (our system attaches them to the projected coordinates of in-world actors, so that they stay with whoever is doing the talking):

    That's basically it. Have fun!

    (I'm working on a platforming RPG called The Savage Garden: - we're entered into the IGF, and you'll see this system and some others in a trailer... very soon)

    wow nice work.


    have a great day