Testing Moai's Positioning

Today a bit of clarification about the way Moai handles the positions of objects. My motivation is two fold. First, I'm working on the UI for character conversation in Dawn, and so I wanted to test my assumptions about the way MOAI works. Second, I wanted to follow up on a misleading statement I made in my first post about connected objects and their joints.


What follows is just a short series of step-by-step tests I wrote regarding MOAITransform2D "setLoc" and "setParent". I'll show you how they work, and, at the end of the post, I'll show you when they don't work. ( Which is either a bug, or simply surprising behavior. )

For these small snippets, I'll leave out the redundant steps ( complete sample at end of post ). The important things to know are: there's a window 128 pixels wide by 128 pixels high. The viewport is setup so that 0,0 is at the upper right corner of the screen. And, we have a textured quad 32x32 pixels with it's origin at it's upper right corner.

Location, Location

To position the quad so that it appears in the middle of the screen, all we need to say is:

local sw,sh= 128,128
-- viewport and windowing left out (see below)

local prop= MOAIProp2D.new()
-- deck, texture setup left out ( see below )

local pw,ph= prop:getDims()
prop:setLoc( sw/2-pw/2, sh/2-ph/2 )
Now, the point of making one object another's parent is so that the parent's position ( its whole transform in fact: position, and rotation ) determines the child's. Move one, move the other.

Here, instead of setting the prop's position, we'll leave it at its default location and, instead we'll parent it to another prop, and set the parent's position to the center of the screen.
-- a new object
local root = MOAI2DProp2d.new()
-- with the same position as prop had
root:setLoc( sw/2-pw/2, sh/2-ph/2 )

-- prop
local prop= MOAIProp2D.new()
-- but we'll leave it at 0,0
prop:setParent( root )
It looks the same, which is exactly what we'd expect. The parent dragged the object along with it over to the center of the screen. ( To keep the visuals simple, the root itself doesn't have an appearance, and it wasn't added to the scene. )

Relative Positioning

That's all well and good, but what this doesn't tell us is how Moai will behave if we alter the position of the prop after it has been parented.  There are multiple (fairly) reasonable interpretations for setLoc(). It could treat the specified location as:
  1. Irrelevant. After parenting, you would be responsible for updating the "joint" between the two objects in order to affect the child's positions.
  2. An offset from the parent. Internally, Moai would update some "final transform" of the child before screen clipping, rendering, hit tests, etc.
  3. An absolute position in world space. Moai would update an internal, per prop relative transform, based on the current position of the prop's parent.
The documentation says only "sets the transform's location". Which probably indicates #1 or #2. In my first post I thought #1, because that's how the last system I used worked, and also because other documentation on attributes seemed confirm my conclusions. In fact, it turns out to be #2. setLoc() sets the object's position relative to whatever its parented to.

local prop = MOAI2dProp2D.new()
local root = MOAI2DProp2D.new()
prop:setParent( root )

function duringSimulation()
   prop:setLoc( 32, 32 )
end

local cr= MOAICoroutine.new()
cr:run( duringSimulation )
You'll note, rather than just set the position immediately after parenting,  the code uses a coroutine. I did this just to make sure that "first pass", wasn't special in anyway. Not in this case, but later the simulation step will... yield... some questionable behavior.

As long as we're here, it's also worthwhile to test what happens if we set the position before we parent the object.

-- create prop first, and set its position
local prop = MOAI2dProp2D.new()
prop:setLoc( sw/2-pw/2, sh/2-ph/2 )

-- now create root and parent prop to it.
local root = MOAI2DProp2D.new()
prop:setParent( root )

We get the same result regardless of order, this is good.

Reporting

In terms of reporting, its worth asking getLoc() the prop's location to see what happens.  Is the value we put in, is the value we get out? The answer is yes, with a caveat. If we want to know the world space position, we have to transform its point origin using "modelToWorld". And, that's where the simulation step becomes important.
local root = MOAI2DProp2D.new()
root:setLoc( sw/2-pw/2, sh/2-ph/2 )

local prop = MOAI2dProp2D.new()
prop:setParent( root )

function duringSimulation() 
  print( prop:getLoc() ) -- 0,0
  print( prop:modelToWorld( prop:getLoc() ) ) -- 0,0
  print( prop:modelToWorld( 0,0 ) ) -- 0,0
  coroutine.yield()
  print( prop:getLoc() ) -- 0,0
  print( prop:modelToWorld( prop:getLoc() ) ) -- 48,48
  print( prop:modelToWorld( 0,0 ) ) -- 48,48
end
The yield lets MOAI run its simulation for a frame and get back to us. You can see: before its had a chance to run, the object's transform has not been updated: it's still at its default position. After the simulation, the prop winds up at 48,48.

[ EDIT-Aug1/12: prop:modelToWorld() incorporates the prop's own transformation. The correct way to determine a prop's world space position is to transform the point 0,0. The sample above has been fixed. ]

Multiple Levels of Hierarchy

Now here's where things get interesting. After confirming the above, and setting up my UI, things still weren't right. UI elements were overlapping, when they should have been separated. Even more interesting my test stubs ( which emulate the interfaces and positioning in MOAI without any actual graphics ) reported everything was fine.

Here I reduced the problem to three colored squares. The setup code isn't going to be incredibly enlightening, because it's using my custom ui code, but basically it's making three colored boxes, the white and blue boxes are separated by 20 pixels, and are parented to a transform positioned 10 pixels below a  red box.
not what was expected!
local box= mk.vbox{ width=200, height=50, gap= 10, mk.blip { width=20, height=20, color="red", }, mk.hbox { gap= 20, width= 200,height= 20, mk.blip { width=10, height=10, color="white", }, mk.blip { width=10, height=10, color="blue", }, }, }
Except, as you can see from the picture that's not what I got at all. What I suspected was that Moai wasn't resolving the dependency graph correctly.   And so I simply forced Moai to re-evaluate the transform every frame.
ahh... much better.
-- same coroutine setup as the initial tests
function duringSimulation() 
  while true do
    -- setting box, the root of this ui, to 0,0
    box:setLoc(0,0)

    -- and letting the simulation get back to us.
    coroutine.yield()
  end
end
Sure enough. That fixed things.  Parenting an object two levels deep wasn't enough to make MOAI notice it needed a position update. Only by setting the transform of the root object -- even if just to it's current, default position -- was needed to kick off the evaluation of the world space positions.

I'm not ready to call this a bug yet. I need rewrite it with just pure Moai objects, and setup a couple of new tests before reporting it to the Moai folks.  That said, it feels like something to keep an eye out for.  If you're nesting a bunch of objects, and they aren't all appearing in the right place, trying setting the position of the root object as well and see if that doesn't trigger the layout you need.

....................

The Positioning Test Code

-- window for display
local sw,sh=128,128
MOAISim.openWindow( "test", sw, sh )

-- viewport initialized so 0,0 is at upper-left corner of screen
local viewport= MOAIViewport.new()
viewport:setSize(sw,sh)
viewport:setScale(sw,-sh)
viewpot:setOffset(-1, 1)

-- layer as container for props
local layer = MOAILayer2D.new()
layer:setViewport( viewport )
MOAISim.pushRenderPass( layer )

-- textured quad 32x32 as measured from it's upper left corner
local gfxQuad = MOAIGfxQuad2D.new()
gfxQuad:setTexture( "moai.png" )
gfxQuad:setRect( 0,0,,32,32 )
gfxQuad:setUVRect(0,0,1,1)

-- prop to display the quad 
prop:setDec( gfxQuad )
layer:insert( prop )

-- set prop's center to the screen's center
local pw,ph= prop:getDims()
prop:setLoc( sw/2-pw/2, sh/2-ph/2 )

0 comments: