Thursday 3 May 2012

Multi-Line Names

We still have a problem with long names - they come out too small.
If we show less of it, it defeats the point of having a long name to start with.

The stone axe could be addressed by putting axe on a second line and is quite easy to do as it has a space in the middle.
function ReplaceSubString( thisString$ , thisOld$ , thisNew$ )
   thisLen = len( thisOld$ ) 
   thisStart = FindSubString( thisString$ , thisOld$ , 1 )
   while thisStart > 0
      thisString$ = left( thisString$ , thisStart - 1 ) + thisNew$ + right( thisString$ , len( thisString$ ) - (thisStart + thisLen - 1) )
      thisStart = FindSubString( thisString$ , thisOld$ , 1 )
   endwhile
endfunction thisString$
This utility function replaces all occurences of one string with another. The logic is fairly straight forward.
  • It uses FindSubString() to check if the old string exists.
  • If so, the main string is split into three, with the old string being the middle piece.
  • The string is re-built with the left piece, the new string and the right piece.
  • It repeats until the old string is no longer found.
By using it on the name string, we can look for a space and replace it with a line break.

In the SetPanelSprite() function, replace the following code.
setTextString( thisNameText , ObjectName( thisObject ) )
setTextString( thisShadText , ObjectName( thisObject ) )
With this new version.
thisString$ = ReplaceSubString( ObjectName( thisObject ) , " " , chr(10) )
setTextString( thisNameText , thisString$ )
setTextString( thisShadText , thisString$ )
Which passes the object name through the new ReplaceSubString() function, swapping any space for character code 10 - which is a line-feed.
Now any name containing a space will be split into multiple lines.

The stone axe text looks a good size now, but we don't know what size that is.  To find out, we can add a bit of code to hijack the routine, putting the text size above the name temporarily.

Before the first setTextY() line, add the following code.
// Add Text Size to Name temporarily
thisString$ = str(getTextSize( thisNameText ),2) + chr(10) + thisString$
setTextString( thisNameText , thisString$ )
setTextString( thisShadText , thisString$ )
// End of Additional Code
Which now gives us values for each piece of text indicating it's size, any less than 24.00 have been scaled down.
Since this code comes after the resize code, the values do go outside the icon area.

Looking at the results, I think a size of 18 would probably produce a better start size.
thisSize# = IconSize() * 0.28125
What do you think?
We can improve visibility on this in a number of ways.
  • Adjust the panel background
  • Adjust the name and shadow colours
  • design the icons with a standard background behind the name.
But these can be fine tuned when we have more icons to test the impact.

Panel Icon Text Size

There are a number of things not quite right with the text so far.
  • stone axe simply doesn't fit
  • resource doesn't fit either
  • stone is too low 
These are three issues all caused by the size of the text.

What might sound strange is that to address this, the first thing I'm going to do is make the text larger.  By changing the text size from 15 to 25 in the SetPanelSprite() function.
At this size, "tree" (4 characters) just about fills the icon width and "stone" (5 characters) is too wide, this has effectively set a maximum for the text size.

What I'm actually looking at here, though is how readable the text is.  If it's no good at it's maximum size, there is no point looking at other sizes.

Though not great, it is readable.  Darkening the panel will improve this. So initially, the text size will 24 - because its an even number and will make "stone" slightly smaller.

But if we change the number of columns later, the icon size will change, so we have to do it in a way which will also change the text.
thisIconMax# = IconSize() - 4.0
thisSize# = IconSize() * 0.375
setTextSize( thisNameText , thisSize# )
Local variable thisIconMax# is the maximum width of the text - it allows for a 4 pixel gap between names.

The current icon size is 64 pixels and we want a text size of 24, the value we multiply the icon size by ( 0.375) is worked out from 24 / 64.

Once the text size has been set, we can measure the resulting width of the text object using getTextTotalWidth().
thisScale# = thisIconMax# / getTextTotalWidth( thisNameText )
By dividing thisIconMax# by the width of the text object, we get a scale value.
  • higher than 1.0 indicates thisIconMax# is bigger than the text.
  • exactly 1.0 indicates they are the same width
  • lower than 1.0 indicates thisIconMax# is smaller than the text.
In the third case, the text needs to be shrunk.  The new size is the original value ( 0.375 x the icon size ), multiplied by the scale value.
if thisScale# < 1.0 then setTextSize( thisNameText , thisSize# * thisScale# )
Then we use the size of one object to set the size of the other.
setTextSize( thisShadText , getTextSize( thisNameText ) )
Lets look at an example.

If a text size of 24 produced text which was 80 pixels wide, then the scale value would be (64.0 - 4.0) / 80.0 or 60.0 / 80.0 = 0.75

As this is less than 1.0, the the new text size would be 24 * 0.75 = 18

This would produce a width of 80 * 0.75 or 60

Since the height of the text has been changed, the Y position of the text needs to be reset to align it with the bottom of the icon.
setTextY( thisShadText , getSpriteY( thisSprite ) + IconSize() - getTextTotalHeight( thisNameText ) )
setTextY( thisNameText , getTextY( thisShadText ) - 1 )
Now the text will shrink if necessary to fit. This uses getTextTotalHeight() to allow for multi-line names, which is the next step.

Panel Icon Text

The depth and position of the sprite is set in the PositionPanelIcons() function, so the depth and position of the text should also be set there.
setSpritePosition( panelIconSprite( icon ) , x , y )
setSpriteDepth( panelIconSprite( icon ) , iconDepth )
thisX# = x + IconSize() / 2.0
thisY# = y + IconSize() - getTextTotalHeight( panelIconNameText( icon ) )
setTextPosition( panelIconShadText( icon ) , thisX# , thisY# )
setTextPosition( panelIconNameText( icon ) , thisX# - 1 , thisY# - 1 )
setTextDepth( panelIconShadText( icon ) , iconDepth - 1 )
setTextDepth( panelIconNameText( icon ) , iconDepth - 2 )
The text is centrally aligned so the X position is middle of the icon. The Y position is the bottom of the icon less the height of the text object.  The shadow is set to the resulting position and the name is positioned 1 pixel up and left.

The depth of the text items needs to be above the icon image with the shadow first, then the text on top.

The "drawing" of the icon images is done in the SetPanelSprite() function, this is where we will set the text for the icons too. Since this is called by both the group and object routines, this will display text for both.

This (with comments added) is what we have so far.
function SetPanelSprite( thisPos , thisFrame )
   thisSprite = panelIconSprite( thisPos )
   if thisFrame = 0
      // No Icon - Clear Image and Reset Animation
      setSpriteImage( thisSprite , 0 )
      clearSpriteAnimationFrames( thisSprite )
      // Clear Text

   else
      // Icon - Set Image and Animation Frame
      setSpriteImage( thisSprite , IconImage() )
      setSpriteAnimation( thisSprite , 64 , 64 , 252 )
      setSpriteFrame( thisSprite , thisFrame )
      // Set Text

   endif
endfunction
The // Clear Text and // Set Text comments show where the text changes go.

At the start of the function, where we get the sprite reference, we also need a few more bits of information.
thisNameText = panelIconNameText( thisPos )
thisShadText = panelIconShadText( thisPos )
This gets the references for both text items.

The code to clear the text is simply.
// Clear Text
setTextString( thisNameText , "" )
setTextString( thisShadText , "" )
Since a null string won't show, we don't need to do anything extra here.

If there is a frame, then we need to find the object it belongs to.
// Set Text
thisObject = FindPanelObjectByFrame( thisFrame )
This looks like a new function, and it is in a way, but we only need a slight change from what we have in the FindObjectByPanelSprite() function, which starts by getting the frame of the sprite. If we could jump in here - as we have the frame - we could use the existing code.
function FindObjectByPanelSprite( thisSprite )
   thisObject = 0
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisString$ = GameGroups()
   if PanelGroup() > 0 then thisString$ = thisString$ + ObjectDetail( PanelGroup() )
...
To do this we split the function at the point we want to enter from - after the frame has been obtained - and put our new function name in there.

Above it we will add some code to link the old call to the new one.

In practical - less editing - terms, we insert a block of code immediately after the old function first line.
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisObject = FindPanelObjectByFrame( thisFrame )
endfunction thisObject 

function FindPanelObjectByFrame( thisFrame )
This creates a smaller version of the FindObjectByPanelSprite() function and starts a new function FindPanelObjectByFrame() to pick up the old code.

Delete the now duplicate line from the old code.
thisFrame = getSpriteCurrentFrame( thisSprite )
and the result should look like this.
function FindObjectByPanelSprite( thisSprite )
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisObject = FindPanelObjectByFrame( thisFrame )
endfunction thisObject

function FindPanelObjectByFrame( thisFrame )
   thisObject = 0
   thisString$ = GameGroups()
   if PanelGroup() > 0 then thisString$ = thisString$ + ObjectDetail( PanelGroup() )
...
This may all be changed back when the finished routine is optimised.

Back at the SetPanelSprite() function, we now use the name of the retrieved object to set the text of the icon.
// Set Text
thisObject = FindPanelObjectByFrame( thisFrame )
setTextString( thisNameText , ObjectName( thisObject ) )
setTextString( thisShadText , ObjectName( thisObject ) )
This just dumps it in as is for now.

Finally we set the size of the text to an arbitrary value of 15 to give an idea of how it looks.
setTextSize( thisNameText , 15 )
setTextSize( thisShadText , 15 )
Now a test run gives us our first look at the icon text.
A few issues to deal with, but nothing unexpected.

Panel Icon UDT

All the object loaded have names, but these are not currently used anywhere.

To make the objects in the panel more easily identified, we can add the name to the bottom of each icon.

Since we don't know in advance the colour of the icon, we need a way to make the text visible over any colour.

This is done by putting something behind the text that is a contrasting colour, either a box or - in this case - a shadow.

If we make the text a bright colour such as yellow, and put a black shadow behind it, it should be easy against any icon.  If this does not work, we will try something else.

To create the text, we need two text objects - one for the text, the other for it's shadow.  These will be used with the sprite currently stored in the panelIconSprite[] array.

Rather than have more arrays, the best option is to move from a dedicated sprite array to a UDT array for the icon.
type panelIconType
   sprite
   nameText
   shadText
endtype
Now we can create a new array called panelIcon[] in the Initialise() function to replace panelIconSprite[].

The initialise code is changed from.
dim panelIconSprite[ panelIconMax() ]
   for icon = 1 to panelIconMax()
      panelIconSprite[ icon ] = createSprite( 0 )
      setSpriteSize( panelIconSprite[ icon ] , IconSize() - 4, IconSize() - 4)
      setSpriteColor( panelIconSprite[ icon ] , 255 , 255 , 255 , 127 )
   next icon
To this.
dim PanelIcon[ panelIconMax() ] as panelIconType
   for icon = 1 to panelIconMax()
      panelIcon[ icon ].sprite = createSprite( 0 )
      setSpriteSize( panelIconSprite( icon ) , IconSize() - 4, IconSize() - 4)
      setSpriteColor( panelIconSprite( icon ) , 255 , 255 , 255 , 127 )
      panelIcon[ icon ].nameText = createText( "" )
      panelIcon[ icon ].shadText = createText( "" )
      setTextAlignment( PanelIconNameText( icon ) , 1 )
      setTextAlignment( PanelIconShadText( icon ) , 1 )
      setTextColor( PanelIconNameText( icon ) , 255 , 255 , 128 , 255 )
      setTextColor( PanelIconShadText( icon ) , 0 , 0 , 0 , 255 )
   next icon
Which now uses individual fields for all the values needed for this icon. The text alignment and colour is also set here.

A series of return functions are created.
// Return functions - PanelIcon Array

function PanelIconIsValid( thisIcon )
   thisBool = ( thisIcon > 0 and thisIcon <= PanelIconMax() )
endfunction thisBool

function PanelIconSprite( thisIcon )
   if PanelIconIsValid( thisIcon )
      thisSprite = panelIcon[ thisIcon ].sprite
   else
      thisSprite = -1
   endif
endfunction thisSprite

function PanelIconNameText( thisIcon )
   if PanelIconIsValid( thisIcon )
      thisText = panelIcon[ thisIcon ].nameText
   else
      thisText = -1
   endif
endfunction thisText

function PanelIconShadText( thisIcon )
   if PanelIconIsValid( thisIcon )
      thisText = panelIcon[ thisIcon ].shadText
   else
      thisText = -1
   endif
endfunction thisText
Now to keep the old functions working, every use of the panelIconSprite[] array needs to be changed.  There should be.
  • Two in PositionPanelIcons().
  • Three in SetPanelIcons().
  • One in SetPanelSprite().
Where the value is put into the array, this needs changing to panelIcon[].sprite.

Where a value is read from and array (which will be most of them), it is changed to the function call PanelIconSprite().

In both cases, the variables used as the parameter (the thing in square brackets) is kept the same.  See the  initialise code for examples.

Perhaps now it's apparent why so often we copy the sprite references from the arrays into local variables like thisSprite.  It is to reduce changes like this.

Panel Group Selection

Now we have the panel displaying some icons, it's time to get back to the functions that deal with pointer selection, so we can change the displayed group.

Game input is handled by the checkGamePointer() function, let's review what it does as a reminder.

An outer structure checks the state of the pointer, Just Pressed, Just Released or Held.

Within the first part - getPointerPressed() - the Y position of the pointer is checked against two sets of conditions.
  • If it's over the panel text object, the panel state is changed between opened and closed.
  • If Y is less than this, the work area is processed.
We now want to add the third possibility - that is it over the panel - using the else command.  With a few comments added, this makes the start of that function look like this.
function checkGamePointer()
   if getPointerPressed() > 0
      if PointDY() >= getTextY( panelText() ) and PointDY() < getSpriteY( panelSprite() )
         // Pointer is over Panel Control Text
         ToggleTrayOpen()
      elseif PointDY() < getTextY( panelText() )
         // Pointer is over Work Area
         touch.spritePicked = getSpriteHit( PointWX() , pointWY() )
         if SpritePicked() > 0
            touch.objectPicked = FindWorkObjectBySprite( SpritePicked() )
            if ObjectPicked() > 0
               touch.pickOffsetX# = PointWX() - getSpriteXByOffset( SpritePicked() )
               touch.pickOffsetY# = PointWY() - getSpriteYByOffset( SpritePicked() )
            endif
         endif
      else
         // Pointer is over Panel

      endif
   elseif getPointerReleased() > 0
We will now add the code to the new block indicated by // Pointer is over Panel.

The code almost the same as the work area code, but will be dealing with display coordinates rather than world coordinates.
// Pointer is over Panel
touch.spritePicked = getSpriteHit( PointDX() , pointDY() )
if SpritePicked() > 0
   touch.objectPicked = FindObjectByPanelSprite( SpritePicked() )
   if ObjectPicked() > 0
      if ObjectisGroup( ObjectPicked() ) = 0
         // Object is not a group object (so can be dragged)
         touch.pickOffsetX# = PointDX() - getSpriteXByOffset( SpritePicked() )
         touch.pickOffsetY# = PointDY() - getSpriteYByOffset( SpritePicked() )
         // Create work object for dragging

      else
         // Object is a group object
         if ObjectPicked() <> PanelGroup()
            panel.group = ObjectPicked()
            SetPanelIcons()
         endif
         touch.objectPicked = 0
      endif
   endif
endif
As before, the .objectPicked field of the touch global is used to store the object and .pickOffsetX# and .pickOffsetY#, the offset of the pointer from the middle of that object.  However in this case, we check if the object is a group object before storing these values.

This also calls a new function FindObjectByPanelSprite(), where the work area used FindWorkObjectBySprite(), because panel icons are not work objects and so a different routine is needed.
function FindObjectByPanelSprite( thisSprite )
   thisObject = 0
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisString$ = GameGroups()
   if PanelGroup() > 0 then thisString$ = thisString$ + ObjectDetail( PanelGroup() )
   for i = 1 to len( thisString$ ) / 2
      thisCode$ = mid( thisString$ , i * 2 - 1 , 2 )
      thisObject = Base96Decode( thisCode$ )
      if thisObject > 0
         if ObjectIcon( thisObject ) = thisFrame then exit
         thisObject = 0
      endif
   next i
endfunction thisObject
This uses the getSpriteCurrentFrame() command to get the current frame of the sprite provided.  This corresponds to the .icon field of the object array so can be used to locate the correct object.

We don't need to check the full list of objects, just the ones currently shown.

Next, it creates a string of Base96 codes by combining the string of groups with members - GameGroups(), with the codes for the member objects - ObjectDetail(), of the currently selected group - PanelGroup().

The resulting string contains Base96 codes for all the icons currently shown in the panel.

This list is processed in the normal manner, extracting two characters, decoding them and then checking to see if the icon for that object matches the frame we are looking for.

Back with the pointer code, once the object is identified as a group object, then it is checked to make sure it is different from the currently selected group.

If so, the current group is changed and the panel redrawn.

Finally, the selected object is reset to zero so the user cant drag the group icon by keeping the pointer pressed.

Now the two groups can be selected on the panel and the correct icons will be shown.

Debug Log File

When there is too much going on to show everything on screen, details of what is going on can be sent to a log file that can be checked later.

This can be done with a very simple function.
function DebugOut( thisString$ )
   thisFile = opentowrite( "debug.txt" , 1 )
   writeline( thisFile , thisString$ )
   closefile( thisFile )
endfunction
This opens a file called debug.txt in append mode (the 1 parameter after the name), writes a string of text to the file, then closes the file.

At the start of the program - just before the call to Initialise(), we now check for and remove any old debug log.
// *** DIAGNOSTIC LOGGING ***
if getFileExists( "debug.txt" ) then deleteFile( "debug.txt" )
DebugOut( "Calling Initialise()" )
// *** DIAGNOSTIC LOGGING *** 
This ensures that each run of the program clears the file and starts again.  It then uses the new function to log the fact that it is calling the Initialise() function.

The comment lines before and after are to draw attention to the presence of this code to make it easier to find and remove later.

The program will run as before, but now a file will be created called debug.txt which will look like this.
Calling Initialise()
AGK uses a designated write area, which varies from system to system.  On Windows this is in an AGK folder within your My Documents folder, with a sub folder for the project and it's media.

On my system, the file is in.

S:\Users\Marl\Documents\AGK\Build-It\media

With the file being created ok, we can now set about locating the problem by putting diagnostic logging calls at key places in the LoadObjectData() function.

At the very start, we add lines which indicate the start of the Load function and the parameter passed in quotes.
function LoadObjectData( thisName$ )
   // Pass the name without extension

// *** DIAGNOSTIC LOGGING ***
DebugOut( "LoadObjectData() called with " + chr(34) + thisName$ + chr(34) )
// *** DIAGNOSTIC LOGGING ***
After the closefile() command, we indicate how many lines were read.
// *** DIAGNOSTIC LOGGING ***
DebugOut( "Lines read : " + str( thisArraySize ) )
// *** DIAGNOSTIC LOGGING ***
At the end of the for i=1 to topObject() loop, just before the last endif, we output the object details stored in the array.
// *** DIAGNOSTIC LOGGING ***
DebugOut( "Object : "+str(i)+" = "+chr(34)+ObjectName(i)+chr(34)+", Icon: "+str(ObjectIcon(i))+", Detail: "+chr(34)+ObjectDetail(i)+chr(34)) 
// *** DIAGNOSTIC LOGGING ***

         endif
      next i
      // Recipes Go Here
This shows the initial values set for the name, icon and detail strings of each object.

At the start of the group membership section, we log the fact.
      // Group Memberships

// *** DIAGNOSTIC LOGGING ***
DebugOut( "Group Memberships") 
// *** DIAGNOSTIC LOGGING ***

And within the group loop, we log if a group is found with members.
      for i=1 to topObject()
         if ObjectIsGroup( i ) > 0 and ObjectDetail( i ) <> ""

// *** DIAGNOSTIC LOGGING ***
DebugOut("Group "+str(i)+" has members "+chr(34)+ObjectDetail( i )+chr(34))
// *** DIAGNOSTIC LOGGING ***

            game.groupObject$ = game.groupObject$ + Base96Encode( i )
Following the FindObjectByName() call, we log the used name and resulting object reference.
               thisObject$ = mid( thisString$ , thisStart , thisComma - thisStart )
               // convert to base96
               thisObject = FindObjectByName( thisObject$ )

// *** DIAGNOSTIC LOGGING ***
DebugOut("..Name "+chr(34)+thisObject$+chr(34)+" is object "+str( thisObject ))
// *** DIAGNOSTIC LOGGING ***

               if thisObject > 0 then thisCoded$ = thisCoded$ + Base96Encode( thisObject )
This covers the main stages of the conversion of the group memberships and should give a better idea of the problem.

When we run the program, and then check debug.txt, we see this.
Calling Initialise()
LoadObjectData() called with "standard"
Lines read : 14
Object : 1 = "tool", Icon: 1, Detail: "stone axe"
Object : 2 = "resource", Icon: 2, Detail: "tree,stone"
Object : 3 = "device", Icon: 3, Detail: ""
Object : 4 = "job", Icon: 4, Detail: ""
Object : 5 = "process", Icon: 5, Detail: ""
Object : 6 = "storage", Icon: 6, Detail: ""
Object : 7 = "tree", Icon: 7, Detail: "Home to Birds and Monkeys"
Object : 8 = "stone", Icon: 8, Detail: "Small hard piece of planet"
Object : 9 = "stone axe", Icon: 9, Detail: "Primative chopping device"
Object : 10 = "log", Icon: 10, Detail: "Rough tree part, might burn well"
Object : 11 = "kindling", Icon: 11, Detail: "Basis of a good fire"
Object : 12 = "spark", Icon: 12, Detail: "Warning - fire Hazard"
Object : 13 = "flame", Icon: 13, Detail: "Fire's younger sibling"
Object : 14 = "fire", Icon: 14, Detail: "Good source of heat and light"
Group Memberships
Group 1 has members "stone axe"
..Name "stone axe" is object 0
Group 2 has members "tree,stone"
..Name "tree" is object 0
..Name "stone" is object 0
Which shows the problem is that all object names are returning object value zero. The problem is in the FindObjectByName() function.

Within that function, we log the fact that the function has been called, the parameter passed and the result.
function FindObjectByName( thisName$ )
   thisName$ = upper( thisName$ )
   for thisObject = 1 to TopObject()
      if upper( ObjectName( thisObject )) = thisName$ then exit
   next thisObject

// *** DIAGNOSTIC LOGGING ***
DebugOut("FindObjectByName() Called with "+chr( 34 )+thisName$+chr(34)+", Result: "+str(thisObject))
// *** DIAGNOSTIC LOGGING ***

   if thisObject > TopObject() then thisObject = 0
endfunction thisObject
Which now changes the bottom part of the log file to
Group Memberships
Group 1 has members "stone axe"
FindObjectByName() Called with "stone axe", Result: 15
..Name "stone axe" is object 0
Group 2 has members "tree,stone"
FindObjectByName() Called with "tree", Result: 15
..Name "tree" is object 0
FindObjectByName() Called with "stone", Result: 15
..Name "stone" is object 0
What is interesting here is that the logged output shows the parameter passed in each case as lower case, yet the first call of the function changes this to upper case using the Upper() command.

Testing the Upper() command with some test code such as
testString$ = "Hello World"
print( chr(34) + upper( testString$ ) + chr(34) )
which returns "", shows that it is this command which is the problem.

This was logged as issue 282 on the AGK reporting board ( code.google.com ).

The workaround for this - to get the project running - is to change the strings to lower case, rather than upper case.  The effect is the same, we simply want the case to be the same in the comparison.
This involves changing all references to upper() to lower().
function FindObjectByName( thisName$ )
   thisName$ = lower( thisName$ )
   for thisObject = 1 to TopObject()
      if lower( ObjectName( thisObject )) = thisName$ then exit
   next thisObject
With this change, the app is back to its working state.
Now the detail fields for objects 1 and 2 are populated correctly and the stone axe is once again displayed.

The debug.txt file also shows everything as it should be.
Calling Initialise()
LoadObjectData() called with "standard"
Lines read : 14
Object : 1 = "tool", Icon: 1, Detail: "stone axe"
Object : 2 = "resource", Icon: 2, Detail: "tree,stone"
Object : 3 = "device", Icon: 3, Detail: ""
Object : 4 = "job", Icon: 4, Detail: ""
Object : 5 = "process", Icon: 5, Detail: ""
Object : 6 = "storage", Icon: 6, Detail: ""
Object : 7 = "tree", Icon: 7, Detail: "Home to Birds and Monkeys"
Object : 8 = "stone", Icon: 8, Detail: "Small hard piece of planet"
Object : 9 = "stone axe", Icon: 9, Detail: "Primative chopping device"
Object : 10 = "log", Icon: 10, Detail: "Rough tree part, might burn well"
Object : 11 = "kindling", Icon: 11, Detail: "Basis of a good fire"
Object : 12 = "spark", Icon: 12, Detail: "Warning - fire Hazard"
Object : 13 = "flame", Icon: 13, Detail: "Fire's younger sibling"
Object : 14 = "fire", Icon: 14, Detail: "Good source of heat and light"
Group Memberships
Group 1 has members "stone axe"
FindObjectByName() Called with "stone axe", Result: 9
..Name "stone axe" is object 9
Group 2 has members "tree,stone"
FindObjectByName() Called with "tree", Result: 7
..Name "tree" is object 7
FindObjectByName() Called with "stone", Result: 8
..Name "stone" is object 8
With that confirmed as working, we can now go through and remove all the diagnostic logging code and the block added to print the details in the main loop.

Though I'm sure we'll be seeing it again soon so the DebugOut() function can stay.

Tracking down this issue was made so much easier by the fact the program is modular, and this also meant that the change to rectify it was limited to just one place.

AGK V1070 Bug Hunt

A new version of AGK was released on Tuesday ( 1st May 2012 ), Version 107.0.

This has been promised for some time, it adds a number of new commands and fixes a number of issues with the last release (which was version 106.5).

After removing the old version and installing the new one, the Build-It project was recompiled and tested to make sure it still works.

While most of the app appears to work normally, now the panel no longer displays the object icon as it did before.
It appears that the new version has introduced a "bug".

There are two approaches to this.
  • Revert back to the old version and wait for a fix
  • Find out why it no longer works and report the bug.
The problem with the first option is that the developers of AGK may not yet be aware of the bug, they will have tested the new commands and fixes for previous bugs, but this may not be part of that code.

If everyone simply reverts back to the old version, without looking into the cause, it may be some time before the bug is found - let alone fixed.

As this project worked fine before the upgrade, we know for certain it's the change to AGK which has caused it, we don't know specifically what is not working, but we have an idea where it will be found.

It is something to do with the objects being displayed.

The fact that some icons are visible, that these are in the right place and show the right icon and the fact that only the groups with members are shown indicates that the code involved in this is probably still OK.

This eliminates the following from possible causes.
  • Object and Image Load - as some objects are correct and all are loaded the same way.
  • Base96 Encoding / decoding - as this is needed to show group icons.
  • Panel update routines - as the same code is used for objects and groups.
The problem appears to be with showing group members, which are stored in the .detail field of the object array.

Adding the following code to the main loop will show the current contents of certain key variables.
print( "PanelGroup() : " + chr(34) + str( PanelGroup() ) + chr(34) )
for i=1 to topObject()
   print(str(i)+" : " + chr(34) + ObjectDetail(i) + chr(34) )
next i
This prints the currently selected group icon from PanelGroup(), to verify the correct group is being used.  The quotes were not actually needed here, but were simply left over from the code copied to make this line.

Then it loops through objects loaded displaying the .detail field for each array entry.
This should contain the encoded string for group members and descriptions for objects. It appears the problem is with the former.

Since only groups which have members are currently shown in the panel - and these ARE being shown, the group membership codes are getting lost after this fact is checked.  Somewhere in the LoadObjectData() function.  Probably as the member objects are converted to codes.

While dumping information to the screen has worked well for us so far, we have reached the limit of what we can find out with this method, to track this issue down, we are going to need more advanced diagnostics.

Tuesday 1 May 2012

Panel Icon Optimising

There is a duplicated block of code now which will fit into it's own function.
function SetPanelSprite( thisPos , thisFrame )
   thisSprite = panelIconSprite[ thisPos ]
   if thisFrame = 0
      setSpriteImage( thisSprite , 0 )
      clearSpriteAnimationFrames( thisSprite )
   else
      setSpriteImage( thisSprite , IconImage() )
      setSpriteAnimation( thisSprite , 64 , 64 , 252 )
      setSpriteFrame( thisSprite , thisFrame )
   endif
endfunction
This was lifted straight out of the group loop, so can simply be replaced with the call
SetPanelSprite( thisPos , thisFrame )
thisSprite = panelIconSprite[ thisPos ]
In the members loop for the objects,
thisSprite = panelIconSprite[ nextIcon ]
setSpriteImage( thisSprite , IconImage() )
setSpriteAnimation( thisSprite , 64 , 64 , 252 )
setSpriteFrame( thisSprite , thisFrame )
Can be replaced with,
SetPanelSprite( nextIcon , thisFrame )
thisSprite = panelIconSprite[ nextIcon ]
Finally, in the remainder loop.
thisSprite = panelIconSprite[ i ]
setSpriteImage( thisSprite , 0 )
clearSpriteAnimationFrames( thisSprite )
Is replaced with,
SetPanelSprite( i , 0 )
thisSprite = panelIconSprite[ i ]
So the function now looks like this.
function SetPanelIcons()
   // Process Groups
   thisGroups = len(GameGroups()) / 2.0
   firstGroupIcon = panelIconMax() - thisGroups + 1
   for i=0 to thisGroups-1
      thisCode$ = mid( GameGroups() , i * 2 + 1 , 2 )
      thisObject = Base96Decode( thisCode$ )
      if thisObject = 0
         thisFrame = 0
      else
         thisFrame = ObjectIcon( thisObject )
      endif
      thisPos = firstGroupIcon + i
      SetPanelSprite( thisPos , thisFrame )
      thisSprite = panelIconSprite[ thisPos ]
      if thisObject = PanelGroup()
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
      else
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
      endif
   next i
   // Process Objects
   if PanelGroup() > 0
      thisString$ = ObjectDetail( PanelGroup() )
      thisMembers = len(thisString$) / 2.0
      nextIcon = 1
      for i=0 to thisMembers - 1
         thisCode$ = mid( thisString$ , i * 2 + 1 , 2 )
         thisObject = Base96Decode( thisCode$ )
//         if ObjectIsKnown( thisObject ) > 0
            thisFrame = ObjectIcon( thisObject )
            SetPanelSprite( nextIcon , thisFrame )
            thisSprite = panelIconSprite[ nextIcon ]
            setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
            inc nextIcon , 1
            if nextIcon >= firstGroupIcon then exit
//         endif
      next i

      // Reset Remainder
      for i = nextIcon to firstGroupIcon - 1
         SetPanelSprite( i , 0 )
         thisSprite = panelIconSprite[ nextIcon ]
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
      next i
   endif
endfunction

More Panel Icons

With all the group icons visible at once, there has to be a way to tell which group the objects belong to.

Highlighting the current group works in a similar way as highlighting menu items, but as there is no way to know in advance which colours will be used for icons, it's safer to modify it's transparency

As with the menu, these use a set of constants to define the alpha levels both normal and highlighted icons.
#constant PGROUP_ALPHA   127
#constant PGROUP_ALPHAHI 255
The initial colours should use a yellow tint for highlighted icons and a darker grey colour for others.

Following on from the last code in the group portion of the SetPanelIcons() function, goes the code to set the colour.
// Change Icon Colour
if thisObject = PanelGroup()
   setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
else
   setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
endif
This checks to see if the code for the object currently being processed matches the code for the currently selected group and uses the appropriate alpha value.
This is all that is needed for the group icons.

The object icons are done in the same way as the group icons, so a lot of the code can be reused.
This.
   // Process Objects
   if PanelGroup() > 0
      thisString$ = ObjectDetail( PanelGroup() )
      thisMembers = len(thisString$) / 2.0
      nextIcon = 1
      for i=0 to thisMembers - 1
         thisCode$ = mid( thisString$ , i * 2 + 1 , 2 )
         thisObject = Base96Decode( thisCode$ )
//         if ObjectIsKnown( thisObject ) > 0
            thisFrame = ObjectIcon( thisObject )
            thisSprite = panelIconSprite[ nextIcon ]
            setSpriteImage( thisSprite , IconImage() )
            setSpriteAnimation( thisSprite , 64 , 64 , 252 )
            setSpriteFrame( thisSprite , thisFrame )
            setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
            inc nextIcon , 1
            if nextIcon >= firstGroupIcon then exit
//         endif
      next i
      // Reset Remainder
This routine works from a member list, so if there is no active group, then there must be no member list and this routine cannot run. This is the purpose of the first check.

The list is copied to a local variable and its length used to find the number of members.

Now not every object is known about, so as we go through the list, any unknowns are ignored.  Only known objects will be displayed.

Because of this, we can't use the loop counter to get the panel position - as was done with the groups - and so a local variable nextIcon is defined to indicate this.

Now it loops through the list members as before, getting the coded ID and decoding it.

Here, there is a commented out line which checks if an object is known.  As this is an if command, the corresponding endif is also commented out later.

This will be how the unknown objects will be ignored later.  Since we have not yet set which objects are known, for now, this is commented out so all objects will be shown.

There are changes to increment and check the nextIcon variable, and the remainder of the code sets the sprite as was done for the groups.

At the end of this routine, nextIcon points to the first panel position not used by objects and firstGroupIcon points to the position after the last panel position available for objects.

All that remains is to reset the sprites in the gap.
      // Reset Remainder
      for i = nextIcon to firstGroupIcon - 1
         thisSprite = panelIconSprite[ i ]
         setSpriteImage( thisSprite , 0 )
         clearSpriteAnimationFrames( thisSprite )
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
      next i
   endif
endfunction
Now the panel shows an object icon for the axe which corresponding to the highlighted tool group.

Panel Icons

To put the icons into the panel will require a new function,
function SetPanelIcons()

endfunction
which will be called each time the icons are changed - particularly when a different group is selected.

First, we get the number of groups with members and store the value.
thisGroups = len(GameGroups()) / 2.0
Since each group is encoded in Base96 which uses 2 chars per object, we divide the length of the encoded list by 2 to get the number of groups.

We determine which panel icon will be the first used by a group.
firstGroupIcon = panelIconMax() - thisGroups + 1
If there are no groups, the following loop will not run.
for i=0 to thisGroups-1
otherwise, this loop processes each one.
thisCode$ = mid( GameGroups() , i * 2 + 1 , 2 )
thisObject = Base96Decode( thisCode$ )
if thisObject > 0
   thisFrame = ObjectIcon( thisObject )
else
   thisFrame = 0
endif
This extracts the coded object index from the group list and decodes it to obtain the object number.

If the object number is greater than zero then the icon field for that object is used for the frame, otherwise a value of zero is used.

The frame number is checked by the next piece of code.
thisPos = firstGroupIcon + i
thisSprite = panelIconSprite[ thisPos ]
if thisFrame = 0
   setSpriteImage( thisSprite , 0 )
   clearSpriteAnimationFrames( thisSprite )
else
   setSpriteImage( thisSprite , IconImage() )
   setSpriteAnimation( thisSprite , 64 , 64 , 252 )
   setSpriteFrame( thisSprite , thisFrame )
endif
If it is zero, indicating for some reason that we don't have an image for this object, then the panel sprite is set back to it's default.

If there is a frame number, then the icon sheet image is assigned to the sprite, animation is enabled for 64 x 64 frames and the correct frame selected.

The loop is closed and the function so far, should look like this.
function SetPanelIcons()
   // Process Groups
   thisGroups = len(GameGroups()) / 2.0
   firstGroupIcon = panelIconMax() - thisGroups + 1
   for i=0 to thisGroups-1
      thisCode$ = mid( GameGroups() , i * 2 + 1 , 2 )
      thisObject = Base96Decode( thisCode$ )
      if thisObject = 0
         thisFrame = 0
      else
         thisFrame = ObjectIcon( thisObject )
      endif
      thisPos = firstGroupIcon + i
      thisSprite = panelIconSprite[ thisPos ]
      if thisFrame = 0
         setSpriteImage( thisSprite , 0 )
         clearSpriteAnimationFrames( thisSprite )
      else
         setSpriteImage( thisSprite , IconImage() )
         setSpriteAnimation( thisSprite , 64 , 64 , 252 )
         setSpriteFrame( thisSprite , thisFrame )
      endif
      // Change Icon Colour

   next i
   // Process Objects

endfunction
Add a call to change the icons at the start of the case STATE_RUNNING block of code
case STATE_RUNNING
   // Running Code
   if getTextVisible( PanelText() ) = 0
      // Game starting - Activate Transition
      setTextVisible( panelText() , 1 )
      setTextColor( panelText() , 127 , 255 , 127 , 0 )
      PositionPanelIcons()
      setPanelIcons()
      StartTransition( TRANS_FADEIN , MENU_FADE )
   elseif (Transition() = TRANS_FADEIN) or (Transition() = TRANS_FADEOUT)
And the first icons can be tested.

When I try this, the whole thing crashes.  So begins an hour of isolating code and printing variables until the problem can be tracked down.

In this case, it is caused by a bug in AGK, where data is getting corrupted on access.

Since there is a new release imminent with a number of bug fixes, I am hoping this will be picked up.

In the meantime, a workaround (for me at least) is to change the following function.
function GameGroups()
endfunction game.groupObject$
Into this.
function GameGroups()
thisString$ = game.groupObject$
endfunction thisString$
Which does exactly the same thing but with an extra step.

That step seems like enough to prevent the problem.
And now if we increase the columns to 5.
#constant PANEL_COLS   5
It becomes.

Panel Layout

At the moment, this size of the slide out icon panel is set by the resolution of the screen - specifically the width - and the number of icons specified in the constants PANEL_ROWS and PANEL_COLS.

To recap,
  • The panel width is the full width of the display
  • The icon width (in pixels) is the width of the display in pixels divided by the number of columns
  • The icon height is the same as the width
  • The panel height is the icon height multiplied by the number of rows
It was done this way so that the icons are square and evenly distributed on the panel, irrespective of the resolution of the device being used.

So on my phone, for example, in portrait (vertical orientation) the display is 480 pixels wide by 800 high.  Based on 4 columns and 3 rows, this gives;
  • The panel width is 480 pixels
  • The icon width is 120 pixels ( 480 / 4 )
  • The icon height is 120 pixels
  • The panel height is 360 pixels (120 x 3 )
So on my phone, the panel occupies 360 / 800 or 45% of the height of the screen.

On a baseline android phone, with a resolution of 320 x 480 (portrait), the icons would be 80 x 80 pixels and the panel would be 320 x 240 or 50% of the height of the screen.

In both cases, the icons are being scaled up from their 64 x 64 pixel source and the panel is taking up about half of the screen space when open.

Increasing the number of icons across - the number of columns - the icon size will be smaller and so the panel height will be smaller.  This will also allow more icons in the panel.
Making the icons closer to their original size (as stored in the image) will improve the quality of the icons by reducing the amount of up-scaling needed.

In the example images above - for a 320x480 display - the middle option ( 5 Columns) would show the icons as they were drawn with no scaling at all.

The icons within the panel represent two type of things; Objects and Groups.
Ideally, objects should be closer to the work area, so start in the top left corner.

Putting the group icons at the end, separates the two types and the empty icons between, provide a buffer between them.

The objects shown in the panel, will be members of the currently selected group.  This requires another value to be stored, in a field called group, which will be part of the panelType UDT.
type panelType
   height#
   speed#
   isOpen
   image
   sprite
   text
   sound
   iconMax
   group
endtype
As always, it has a return function.
function PanelGroup()
endfunction panel.group
And is set in the Initialise() function.
panel.group = 0
and at the end of the function, by changing the line.
game.groupObject$ = Base96Sort( GameGroups() )
into
if len( GameGroups() ) > 0
   game.groupObject$ = Base96Sort( GameGroups() )
   panel.group = Base96Decode( left( GameGroups() , 2) )
else
   panel.group = 0
endif
Which now decodes the first group ID ( if any exist ) and stores it.