Having a text Description follow the mouse around the screen

Started by bx83, Mon 10/05/2021 01:57:58

Previous topic - Next topic

bx83

This function currently takes the current location at mouse.x/mouse.y, and in repeatedly_execute_always(), put's the Game.GetLocationName (ie. the editor's 'Description') at a predetermined location on the screen.
This is the editor's 'Description' for Objects, Hotspots etc.
It almost works perfectly, but I can't get it to follow and constantly update it's position on mouse.x,mouse.y

GlobalScript.asc
Code: ags
function SetFloatingText(String floatingText)
{
	if (ConversationIsOn) {
			DrawingSurface* dsFloatingText_2 = sprtFloatingText.GetDrawingSurface();
			dsFloatingText_2.Clear(COLOR_TRANSPARENT);
			dsFloatingText_2.Release();
	} else {
		if (textContent == null)
			textContent = "";
		
		if(sprtFloatingText == null)
			sprtFloatingText = DynamicSprite.Create(FLOATING_TEXT_GUI.Width, FLOATING_TEXT_GUI.Height, true);
	
		if (floatingText != textContent) {
			textContent = floatingText;
			
			DrawingSurface* dsFloatingText = sprtFloatingText.GetDrawingSurface();
			dsFloatingText.Clear(COLOR_TRANSPARENT);
			
			if(!String.IsNullOrEmpty(textContent)) {
				dsFloatingText.DrawingColor = FLOATING_TEXT_COLOR;
				dsFloatingText.drawStringWrappedOutline(mouse.x,
											mouse.y
											FLOATING_TEXT_GUI.Width - 2*FLOATING_TEXT_OUTLINE_WIDTH,
											eTextOutlineRounded,																			
											FLOATING_TEXT_FONT,																				
											eAlignLeft,																							
											textContent,																							
											0,																												
											FLOATING_TEXT_OUTLINE_COLOR,														
											FLOATING_TEXT_OUTLINE_WIDTH);
			}
			
			dsFloatingText.Release();
			
			FLOATING_TEXT_GUI.BackgroundGraphic = sprtFloatingText.Graphic;
		}
	}
}

...

function repeatedly_execute_always()
{  
...	 
	if (Game.GetLocationName(mouse.x, mouse.y)!=null) {
		SetFloatingText(Game.GetLocationName(mouse.x, mouse.y));
	}
...
}



GlobalScript.ash
Code: ags
//for gFloatingText 'description of what mouse is over'
#define FLOATING_TEXT_GUI gFloatingText
#define FLOATING_TEXT_FONT eFontSpeech
#define FLOATING_TEXT_OUTLINE_WIDTH 2
#define FLOATING_TEXT_COLOR 2047
#define FLOATING_TEXT_OUTLINE_COLOR 0


gFloatingText:
Width: 1366 (game screen width)
Height: 768 (game screen height)
Left, Top: 0
Clickable: False
Visible: True
ZOrder: 999
no controls, thats it.

What's supposed to happen: Description/Name text is supposed to follow the mouse around over an area with a Description/Name (floatingText)
What actually happens: When the mouse enters a Named area, it displays the text at those coords, but doesn't follow the mouse around.

How do I make it float around at mouse.x,mouse.y, constantly updating as it changes location?

Bonus super-advanced question: How do I get the floatingText background to have a white rectangle on it - shaped on Description words width/height - rather than translucency?

Gilbert

Too lazy to check the code at the moment, but this was precisely what my old OverHot module did, but as described there, was superseded by the Description module.

As both modules are quite old I cannot guarantee they're still working out of the box with newer version of AGS but you may try them first.

bx83


Snarky

Note that the code here rawdraws the text (.drawStringWrappedOutline()) in order to make use of special outline styling (taken from the SpeechBubble module). So any module would likely have to be modified in order to achieve the same result.

Given the care you've obviously taken to pose the question clearly, I feel bound to try to answer, but I haven't time to look into it until this evening. From a quick scan, nothing jumps out at me as obviously wrong (though I would solve it a little differently myself). Maybe someone else will help in the mean time.

bx83

Here's something that seems to work so far....

old_mouse_x and _y are global variables, int's set to 0 initially.


Code: ags
function SetFloatingText(String floatingText)
{
	if (textContent == null)
		textContent = "";
	
	if(sprtFloatingText == null)
		sprtFloatingText = DynamicSprite.Create(FLOATING_TEXT_GUI.Width, FLOATING_TEXT_GUI.Height, true);

	if (mouse.x!=old_mouse_x || mouse.y!=old_mouse_y) {
		textContent = floatingText;
		
		DrawingSurface* dsFloatingText = sprtFloatingText.GetDrawingSurface();
		dsFloatingText.Clear(COLOR_TRANSPARENT);
		
		if(!String.IsNullOrEmpty(textContent)) {
			dsFloatingText.DrawingColor = FLOATING_TEXT_COLOR;
			dsFloatingText.drawStringWrappedOutline(mouse.x+30,
								mouse.y-30,
								FLOATING_TEXT_GUI.Width - 2*FLOATING_TEXT_OUTLINE_WIDTH,
								eTextOutlineRounded,
								FLOATING_TEXT_FONT,
								eAlignLeft,
								textContent,				
								0,						
								FLOATING_TEXT_OUTLINE_COLOR,
								FLOATING_TEXT_OUTLINE_WIDTH);
		}
		
		dsFloatingText.Release();
		
		FLOATING_TEXT_GUI.BackgroundGraphic = sprtFloatingText.Graphic;
     
		old_mouse_x=mouse.x;
		old_mouse_y=mouse.y;
	}
}

Crimson Wizard

#5
I also don't have much spare time at the moment, but would like to point out one thing that stood out for me:

Quote from: bx83 on Mon 10/05/2021 01:57:58
gFloatingText:
Width: 1366 (game screen width)
Height: 768 (game screen height)

If I understand correctly, you have a fullscreen gui which is fully transparent, and on which you redraw the floating text in a new position each game update?
This is an inefficient approach, because engine will have to convert a much larger than necessary bitmap into texture for redrawing the GUI. It likely won't slow your game down too much (unless you have several similar guis), nevertheless the optimal approach is to:

* calculate text size,
* resize sprtFloatingText if necessary (only if it's smaller),
* draw on sprite from 0,0
* resize gui to the text's size,
* reposition gui.

bx83

I realised this, but unless you have a screen-sized GUI on which to display the text, it will cut off the GUI as the mouse moves beyond it's border.
Eg. you have a 100x200 area, put the text on to that, as soon as your mouse reaches 150x220 absolute the text will be cut off and eventually disappear. I'm probably misunderstanding something - I definitely am - but this was what lead to make the GUI screen-sized.
As it is, it's not too inefficient, but there's a definite 200ms lag as the description catches up to the mouse. Then again, I'm using a Mac Pro 2010, so..... :P

Crimson Wizard

#7
Quote from: bx83 on Mon 10/05/2021 13:48:57
I realised this, but unless you have a screen-sized GUI on which to display the text, it will cut off the GUI as the mouse moves beyond it's border.

Cannot same happen with the text drawn on full gui? You will have to provide some solution for this case anyway.

E.g. with GUI you may clamp gui position to screen.

if (gui.X + gui.Width > System.ScreenWidth) {
    gui.X = System.ScreenWidth - gui.Width;
}

same for height.


For better visual results though you may align GUI to mouse not by it's left-top corner, but move it further to the left, so that it is aligned with a right-top or right-bottom corner for example.

Snarky

OK, I've had a look at it. It took a second to recognize that this was my code in the first place. (wtf)

Like CW, one thing that I would do differently is to not have a screen-size GUI and manually position the text as you draw its sprite, but rather use a text-size GUI that you just reposition (for one thing, it means you don't have to regenerate the text sprite from scratch every time the mouse moves a pixel). The other is to be more careful about the code structure.

So, my first step is to break this task up into multiple sub-tasks:

1. Determine what text we want to display (if any)
2. Format and draw the text we want the way we want it
3. Place the text where we want it on the screen

We want to keep these steps separate, because it keeps the code as simple as possible, and allows us flexibility to change one part without breaking another.

The code you originally got from me, SetFloatingText(), dealt with step 2, but you've added in part of step 1 (hiding the text if somebody is speaking). I have taken that back out; it does not belong in the function.

The only thing we really need to change from the original code is to fit the drawing canvas and the GUI it sits on snugly around the text. (This is actually a bit problematic in AGS, at least before 3.5.0, because of some quirks/bugs in the font logic; the GetTextWidth/Height bounding box will often not fit the text perfectly, and sometimes text may go outside the canvas, depending on the font you're using. I've added padding and a "baseline" value to shift the text vertically in order to address this.) I've also added in a background color option:

Code: ags
#define FLOATING_TEXT_GUI gFloatingText
#define FLOATING_TEXT_FONT eFontfntSpeech
#define FLOATING_TEXT_OUTLINE_WIDTH 6
#define FLOATING_TEXT_COLOR 11
#define FLOATING_TEXT_OUTLINE_COLOR 0
#define FLOATING_TEXT_BACKGROUND_COLOR 15
#define FLOATING_TEXT_H_PADDING 4
#define FLOATING_TEXT_V_PADDING 2
#define FLOATING_TEXT_BASELINE 4

String textContent;
DynamicSprite* sprtFloatingText;

void SetFloatingText(String floatingText)
{
  if(textContent == null)
    textContent = "";
  if(floatingText != textContent)
  {
    textContent = floatingText;
    if(String.IsNullOrEmpty(textContent))
    {
      if(sprtFloatingText != null)
      {
        DrawingSurface* dsFloatingText = sprtFloatingText.GetDrawingSurface();
        dsFloatingText.Clear(COLOR_TRANSPARENT);
        dsFloatingText.Release();
        FLOATING_TEXT_GUI.BackgroundGraphic = sprtFloatingText.Graphic;
        FLOATING_TEXT_GUI.Width = 1;
        FLOATING_TEXT_GUI.Height = 1;
      }
    }
    else
    {
      int w = GetTextWidth(floatingText, FLOATING_TEXT_FONT) + 2*FLOATING_TEXT_OUTLINE_WIDTH + 2*FLOATING_TEXT_H_PADDING;
      int h = GetTextHeight(floatingText, FLOATING_TEXT_FONT, w+1) + 2*FLOATING_TEXT_OUTLINE_WIDTH + 2*FLOATING_TEXT_V_PADDING;

      sprtFloatingText = DynamicSprite.Create(w, h, true);
      DrawingSurface* dsFloatingText = sprtFloatingText.GetDrawingSurface();

      dsFloatingText.Clear(FLOATING_TEXT_BACKGROUND_COLOR);
      dsFloatingText.DrawingColor = FLOATING_TEXT_COLOR;
      dsFloatingText.drawStringWrappedOutline(FLOATING_TEXT_OUTLINE_WIDTH+FLOATING_TEXT_H_PADDING,
                                              FLOATING_TEXT_OUTLINE_WIDTH+FLOATING_TEXT_V_PADDING-FLOATING_TEXT_BASELINE,
                                              w - 2*FLOATING_TEXT_OUTLINE_WIDTH - 2*FLOATING_TEXT_H_PADDING + 1,
                                              eTextOutlineRounded,
                                              FLOATING_TEXT_FONT,
                                              eAlignLeft,
                                              textContent,
                                              0,
                                              FLOATING_TEXT_OUTLINE_COLOR,
                                              FLOATING_TEXT_OUTLINE_WIDTH);
      dsFloatingText.Release();
      FLOATING_TEXT_GUI.Width = w;
      FLOATING_TEXT_GUI.Height = h;
      FLOATING_TEXT_GUI.BackgroundGraphic = sprtFloatingText.Graphic;
      
    }
  }
}


Now for the other logic:

Code: ags

function repeatedly_execute_always()
{ 
...	
    if(ConversationIsOn) // We move this check here so it's part of the rest of the "what text to display" logic
    {
      gFloatingText.Visible = false; // Much easier than clearing the canvas
    }
    else
    {
      gFloatingText.Visible = true;
      SetFloatingText(Game.GetLocationName(mouse.x, mouse.y));  // SetFloatingText() handles null, so we don't need to test for it

      // Position the floating text
      gFloatingText.X = mouse.x;
      gFloatingText.Y = mouse.y;
    }
...
}


That's a very simple version of the "follows mouse cursor" logic. You would probably want to add some of the things Crimson Wizard suggests so the text doesn't get covered up by the cursor (just add a simple x,y offset to the coordinates), and to ensure it doesn't go outside the screen edges. (Personally I prefer it to flip to the left side/above the cursor if necessary, rather than just stop when it gets to the edge, but do it however you like.)

Snarky

BTW, the TwoClick template has a slightly fancier version of the Game.GetLocationName() logic that you could copy:

Code: ags

	// Action Text
	// We always display the name of what is under the mouse, with one exception:
	// IF the player has an inventory item selected and hovers over the same inventory item, 
	// we display nothing to indicate that an item can not be used on itself
	if (player.ActiveInventory == null)
		SetFloatingText(Game.GetLocationName(mouse.x, mouse.y));
	else
	{
		InventoryItem *i = InventoryItem.GetAtScreenXY(mouse.x, mouse.y);
		if (i != null && (i.ID == player.ActiveInventory.ID))
			SetFloatingText("");
		else
			SetFloatingText(Game.GetLocationName(mouse.x, mouse.y));
	}

bx83

It's my edited, awful code, but it now works perfectly:

GlobalScript.ash
Code: ags
...
//for gFloatingText 'description of what mouse is over'
#define FLOATING_TEXT_GUI gFloatingText
#define FLOATING_TEXT_FONT eFontSpeech
#define FLOATING_TEXT_OUTLINE_WIDTH 2
#define FLOATING_TEXT_COLOR 11
#define FLOATING_TEXT_OUTLINE_COLOR 0
#define FLOATING_TEXT_BACKGROUND_COLOR 15
#define FLOATING_TEXT_H_PADDING 20
#define FLOATING_TEXT_V_PADDING 40
#define FLOATING_TEXT_BASELINE 4
...


GlobalScript.asc
Code: ags
function SetFloatingText(String floatingText)
{ 
  if (Intro) return;

  if(textContent == null)
    textContent = "";
  
  if(old_mouse_x!=mouse.x || old_mouse_y!=mouse.y) {
  
    textContent = floatingText;
    if(String.IsNullOrEmpty(textContent)) {
      if(sprtFloatingText != null && (mouse.y>120)) {   //a bit of a kludge for mouse.y for when conversation is on, to wipe the sprite when it's on the lower half of the screen, away from conversation icons at the top
        DrawingSurface* dsFloatingText = sprtFloatingText.GetDrawingSurface();
        dsFloatingText.Clear(COLOR_TRANSPARENT);
        dsFloatingText.Release();
        FLOATING_TEXT_GUI.BackgroundGraphic = sprtFloatingText.Graphic;
        FLOATING_TEXT_GUI.Width = 1;
        FLOATING_TEXT_GUI.Height = 1;
      }
    } else {
      
      int w;
      int h;
      
      if (ConversationIsOn) {
        w = 1366;   //width of conversation icons
        h = 150;   //height+a bit of conversation icons
      } else {
        w = GetTextWidth(floatingText, FLOATING_TEXT_FONT) + 2*FLOATING_TEXT_OUTLINE_WIDTH + 2*FLOATING_TEXT_H_PADDING;
        h = GetTextHeight(floatingText, FLOATING_TEXT_FONT, w+1) + 2*FLOATING_TEXT_OUTLINE_WIDTH + 2*FLOATING_TEXT_V_PADDING;        
      }
      
      sprtFloatingText = DynamicSprite.Create(w, h, true);
      
      DrawingSurface* dsFloatingText = sprtFloatingText.GetDrawingSurface();
 
      dsFloatingText.Clear(COLOR_TRANSPARENT);   //TRANSPARENT because otherwise was coming up as a black square
      dsFloatingText.DrawingColor = FLOATING_TEXT_COLOR;
      
      if (ConversationIsOn) {
        dsFloatingText.drawStringWrappedOutline((FLOATING_TEXT_OUTLINE_WIDTH+FLOATING_TEXT_H_PADDING),
                                                120,
                                                w - 2*FLOATING_TEXT_OUTLINE_WIDTH - 2*FLOATING_TEXT_H_PADDING + 1,
                                                eTextOutlineRounded,
                                                FLOATING_TEXT_FONT,
                                                eAlignCentre,
                                                textContent,
                                                0,
                                                FLOATING_TEXT_OUTLINE_COLOR,
                                                FLOATING_TEXT_OUTLINE_WIDTH);
      } else {
        dsFloatingText.drawStringWrappedOutline((FLOATING_TEXT_OUTLINE_WIDTH+FLOATING_TEXT_H_PADDING)+15,
                                                (FLOATING_TEXT_OUTLINE_WIDTH+FLOATING_TEXT_V_PADDING-FLOATING_TEXT_BASELINE)-35,
                                                w - 2*FLOATING_TEXT_OUTLINE_WIDTH - 2*FLOATING_TEXT_H_PADDING + 1,
                                                eTextOutlineRounded,
                                                FLOATING_TEXT_FONT,
                                                eAlignLeft,
                                                textContent,
                                                0,
                                                FLOATING_TEXT_OUTLINE_COLOR,
                                                FLOATING_TEXT_OUTLINE_WIDTH);
      }
      
      dsFloatingText.Release();
      FLOATING_TEXT_GUI.Width = w;
      FLOATING_TEXT_GUI.Height = h;
      FLOATING_TEXT_GUI.BackgroundGraphic = sprtFloatingText.Graphic;
    }
  }
}



function repeatedly_execute_always()
{  
...
  //floating text description
if (Game.GetLocationName(mouse.x, mouse.y)!=null) {
	  gFloatingText.Visible = true;
    SetFloatingText(Game.GetLocationName(mouse.x, mouse.y));  // SetFloatingText() handles null, so we don't need to test for it
  }
  
  // Position the floating text
  if (ConversationIsOn) {
    gFloatingText.X = 0;
    gFloatingText.Y = 0;
  } else {
    gFloatingText.X = mouse.x;
    gFloatingText.Y = mouse.y-17;  //will take the corner of the description sprite up above the y-coord of the cursor, otherwise it's sits forever too far down
    
  }
...
}


Incidentally, here is my custom dialog system, which calls SetFloatingText() in dialog_options_repexec():
LangTrialHappening is a switch which is only on during a conversation with the pirates, which has custom icon size, custom background etc. etc. - please ignore, LangTrialHappenig=false for this.
The question text in a dDialog is of the form:
1234|An Apple
or
1234
1234 is a sprite number; |Text is what displays below the icon.

GlobalScript.asc
Code: ags
//----------------------------------------------------------------------------------------------------------------------
// Dialogue Options Get Dimensions
//----------------------------------------------------------------------------------------------------------------------

function dialog_options_get_dimensions(DialogOptionsRenderingInfo* info)
{
	ConversationIsOn=true;
	beartrap=false;
  
  
	if (LangTrialHappening) {
    ConvoIconSize=110;        //redundant?
    dialog_left=507;
    info.Height =110+1;
  } else {
    ConvoIconSize=96;
    dialog_left=11;
    info.Height = 96+14;
  }

  info.X = 0;
  info.Y = 0;
  info.Width = 1366;
  
	
  gIconbar.Visible=false; //turn off top-bar
    
	MyDynamicSpriteForTheFakeGUI = DynamicSprite.Create(info.Width, info.Height, true);
  DrawingSurface* ds = MyDynamicSpriteForTheFakeGUI.GetDrawingSurface();
	if (InPirateBay) {
    ds.DrawImage(0, 0, 2639); //draw wood-panel dialog box over the top of what was there at 0,0 
  } else {
    ds.DrawImage(0, 0, 2604); //draw alternate dialog box over the top of what was there at 0,0
  }
	ds.Release();
	
	gFakeDialogOptions.BackgroundGraphic = MyDynamicSpriteForTheFakeGUI.Graphic;
  gFakeDialogOptions.Visible = true;
}

//----------------------------------------------------------------------------------------------------------------------
// Draw Dialog Options
//----------------------------------------------------------------------------------------------------------------------

function DrawDialogOptions(DrawingSurface* ds, DialogOptionsRenderingInfo* info)
{
	int i = 1, ypos = 7, xpos = dialog_left;  
  if (LangTrialHappening) ypos=1;
  
  while (i <= info.DialogToRender.OptionCount) {
    if (info.DialogToRender.GetOptionState(i) == eOptionOn) {
			gFakeDialogOptions.BackgroundGraphic = MyDynamicSpriteForTheFakeGUI.Graphic;
			gFakeDialogOptions.Visible = true;
			
      String str = info.DialogToRender.GetOptionText(i);  //get glyph number from option text
      
      int strposdiv=str.IndexOf("|");
      int cur_img=0;
      String displayText="";
      
      if (strposdiv != -1) {     //has | in it...
        String temp=str.Substring(0, strposdiv);         //get glyph as sprite number
        cur_img=temp.AsInt;
        
      } else {      
        cur_img = str.AsInt;                                      //get glyph as sprite number
      }
      
      ds.DrawImage(xpos, ypos, cur_img);                          //draw this glyph
      
      if (LangTrialHappening) {
        xpos += PirateIconSize;
      } else {
        xpos += ConvoIconSize;																		//xpos+=width of glyph
      }
      
    }
    
    i++;
  }
}

//----------------------------------------------------------------------------------------------------------------------
// Dialog Options Render
//----------------------------------------------------------------------------------------------------------------------

function dialog_options_render(DialogOptionsRenderingInfo* info)
{
  DrawDialogOptions(info.Surface, info);
}

//----------------------------------------------------------------------------------------------------------------------
// Dialog Options Repeat Exec
//----------------------------------------------------------------------------------------------------------------------

function dialog_options_repexec(DialogOptionsRenderingInfo* info)
{
	gFakeDialogOptions.BackgroundGraphic = MyDynamicSpriteForTheFakeGUI.Graphic;
  gFakeDialogOptions.Visible = true;
	
	int i = 1, xpos = dialog_left;
  while (i <= info.DialogToRender.OptionCount) {
    if (info.DialogToRender.GetOptionState(i) == eOptionOn) {
      if (LangTrialHappening) {  
        
        if (	 mouse.y >= info.Y
            && mouse.y <= info.Y+PirateIconSize
            && mouse.x >= xpos
            && mouse.x <= xpos+PirateIconSize
            && mouse.x <= 837 )
        {
          info.ActiveOptionID = i;
          
          DrawingSurface* ds = MyDynamicSpriteForTheFakeGUI.GetDrawingSurface();
          
          DrawDialogOptions(ds, info);
          ds.Release();
          
          return;
        }
        
      } else {
        
        if (	 mouse.y >= info.Y
            && mouse.y <= info.Y+ConvoIconSize+10
            && mouse.x >= xpos
            && mouse.x <= xpos+ConvoIconSize)
        {
          info.ActiveOptionID = i;
          
          DrawingSurface* ds = MyDynamicSpriteForTheFakeGUI.GetDrawingSurface();
          
          DrawDialogOptions(ds, info);
          ds.Release();
          
          String str = info.DialogToRender.GetOptionText(i);  //get glyph number from option text
          
          int strposdiv = str.IndexOf("|");
          if (strposdiv!=-1) {
            ConvoDisplayText = str.Substring(strposdiv+1, str.Length-1);  //get associated text
          }
          
          SetFloatingText(ConvoDisplayText);  //set it to floatingText
          
          return;
        }
      }
    
      if (LangTrialHappening) {
        xpos += PirateIconSize;
      } else {
        xpos += ConvoIconSize;																		//xpos+=width of glyph
      }
    }
    i++;
  }
}

//----------------------------------------------------------------------------------------------------------------------
// Dialog Options Mouse Click
//----------------------------------------------------------------------------------------------------------------------

function dialog_options_mouse_click(DialogOptionsRenderingInfo* info, MouseButton button)
{
  if (info.ActiveOptionID > 0)
  {
    DrawingSurface* ds = MyDynamicSpriteForTheFakeGUI.GetDrawingSurface();
		DrawDialogOptions(ds, info);
    ds.Release();

    gFakeDialogOptions.BackgroundGraphic = MyDynamicSpriteForTheFakeGUI.Graphic;
    gFakeDialogOptions.Visible = true;
    
    if (LangTrialHappening) {
      if (mouse.x>507 && mouse.x<837) {
        info.RunActiveOption();
      }
    } else {
      info.RunActiveOption();
    }
    
  }
}


some pictures:
</br>
</br>
</br>
</br>
</br>
</br>
</br>

SMF spam blocked by CleanTalk