Monday, 7 April 2014

"Infinite Scrolling" Using Unity3d and NGUI's UIScrollview Part 2

Previously I posted a demo of an early version of NGUI UIScrollview that uses a pool of objects to display large amount of data. e.g. contacts list, achievements...etc

Since then I updated the component with more features such as:
  • Better scroll handling which allowed me to use spring physics without issues so far
  • Sections support (not as sophisticated as the indexpath in iOS but did the job for now)
  • Basic scroll indicator (i.e. not interactive)
  • Made for on item per row for now...(uses UITable... should work with UIGrid)
  • Only fixed row height is supported.
 Here is a demo of the result:

How does the infinite scrolling work?
1. Each visible item will notify the main controller when it becomes invisible using NGUI's panel function... here is an example of how I've done it:
private bool isVisible;
void CheckVisibilty() 
{
 bool currentVisibilty = panel.IsVisible(backgroundSprite);
 if(currentVisibilty != isVisible)
 {
  isVisible = currentVisibilty;
  thisCollider.enabled = isVisible;
   
  if(!isVisible)
  {
   // notify main controller. itemNumber is the index of the pool item (not the data)
   StartCoroutine(listPopulator.ItemIsInvisible(itemNumber));
  }
 }
}
2. On receiving this notification the main controller do the following:
  • Retrieve the data index from the pool item
  • Check the visiblity of the next or previous item to determine scroll direction. (I didn't use the currentMomentum of UIScrollview... )
  • Based on the direction we start moving items of the pool from the top to the bottom (or vice versa) and change it to an item or section...
public IEnumerator ItemIsInvisible(int itemNumber)
{
    if(isUpdatingList) yield return null;
    isUpdatingList = true;
    if(dataList.Count > poolSize)// we need to do something "smart"... 
    {
 Transform item = itemsPool[itemNumber];
 int itemDataIndex = 0;
 if(item.tag.Equals(listItemTag))
            itemDataIndex = item.GetComponent().itemDataIndex;
 if(item.tag.Equals(listSectionTag))
     itemDataIndex = item.GetComponent().itemDataIndex;

 int indexToCheck=0;
 InfiniteItemBehavior infItem = null;
 InfiniteSectionBehavior infSection = null;
 if(dataTracker.ContainsKey(itemDataIndex+1))
 {
     infItem = itemsPool[(int)(dataTracker[itemDataIndex+1])].GetComponent();
     infSection = itemsPool[(int)(dataTracker[itemDataIndex+1])].GetComponent();

     if((infItem != null && infItem.verifyVisibility()) || (infSection != null && infSection.verifyVisibility()))
     {
  // dragging upwards (scrolling down)
  indexToCheck = itemDataIndex -(extraBuffer/2);
  if(dataTracker.ContainsKey(indexToCheck))
  {
      //do we have an extra item(s) as well?
      for(int i = indexToCheck; i>=0; i--)
      {
          if(dataTracker.ContainsKey(i))
   {
       infItem = itemsPool[(int)(dataTracker[i])].GetComponent();
       infSection = itemsPool[(int)(dataTracker[i])].GetComponent();
       if((infItem != null && !infItem.verifyVisibility()) || (infSection != null && !infSection.verifyVisibility()))
       {
    item = itemsPool[(int)(dataTracker[i])];
    if((i)+poolSize < dataList.Count && i>-1)
    {
        // is it a section index?
        if(sectionsIndices.Contains(i+poolSize))
        {
     // change item to section
     ChangeItemToSection(item,i+poolSize,i);
        }
        else if(item.tag.Equals(listSectionTag))
        {
     // change section to item
     ChangeSectionToItem(item,i+poolSize,i);
        }
        else
        {
     PrepareListItemWithIndex(item,i+poolSize,i);
        }
    }
       }
   }
   else
   {
     scrollCursor = itemDataIndex-1;
     break;
   }
      }
        }
           }
       }
       if(dataTracker.ContainsKey(itemDataIndex-1))
       {
    infItem = itemsPool[(int)(dataTracker[itemDataIndex-1])].GetComponent();
    infSection = itemsPool[(int)(dataTracker[itemDataIndex-1])].GetComponent();

    if((infItem != null && infItem.verifyVisibility()) || (infSection != null && infSection.verifyVisibility()))
    {
        //dragging downwards check the item below
        indexToCheck = itemDataIndex +(extraBuffer/2);
    
        if(dataTracker.ContainsKey(indexToCheck))
        {
            // if we have an extra item
     for(int i = indexToCheck; i();
      infSection = itemsPool[(int)(dataTracker[i])].GetComponent();
      if((infItem != null && !infItem.verifyVisibility()) || (infSection != null && !infSection.verifyVisibility()))
      {
          item = itemsPool[(int)(dataTracker[i])];
          if((i)-poolSize > -1 && (i) < dataList.Count)
          {
       // is it a section index?
       if(sectionsIndices.Contains(i-poolSize))
       {
           // change item to section
           ChangeItemToSection(item,i-poolSize,i);
       }
       else if(item.tag.Equals(listSectionTag))
       {
           // change section to item
           ChangeSectionToItem(item,i-poolSize,i);
       }
       else
       {
           PrepareListItemWithIndex(item,i-poolSize,i);
       }
          }
             }
                }
                else
                {
             scrollCursor = itemDataIndex+1;
             break;
                }
            }
               }
           }
        }
     }
     isUpdatingList = false;
}
The full source with sample and documentation is available on Github under MIT License :

Free Android app called Avatar Messenger is using this component for the phone book list: https://play.google.com/store/apps/details?id=com.orange.labs.avachat

Monday, 31 March 2014

Avatar Messenger: Free App For Android

One of the apps I've been developing for Orange Labs UK. An updated version was released last week

Demo:


The app allows users to create their avatar and see their friends' ones as well as sending animated messages.

The app is made with Unity3D and utilizes the Infinite scrolling component I made for the phone book screen. (I am working on making the scripts of this component available as open source)

If anyone would like to try the app its available for free on Google Play

This version of the app uses SMS for messaging. Next step is to make an IM version for both iOS and Android.


Sunday, 15 December 2013

UnityPlayer as Subview with Transparent Background (iOS)

In a previous post I was experimenting with Android & Unity to be able to use Unity as subview overlaying a native UI made with Android... now I tried to do the same but with iOS.

Since Unity 4.2 it became easier to override unity ios view to do your own custom one and combine it with native views inside iOS application.

In this post I will outline the steps to have a simple unity scene with native controls using Unity 4.2 (basic license).

Unity Scene
  1. For simplicity I created a scene with a rotating cube and for the camera I set the color to (0,0,0,0) 
  2. Inside one of my "RotatingCube" gameobject's scripts I added a listener function that simply toggle the direction everytime it is called:
  3. void OnTogglePressed(string message)
    {
         Debug.Log("Toggle direction");
         direction *= -1;
    } 
    
  4. Once all done save the scene and build for iOS to generate XCode project


The generated XCode project
  1. First thing to do is to support transparent background in unity view. To do that simply do a minor change to CreateSystemRenderingSurface method inside GlesHelper.mm to have:
    surface->layer.opaque = NO;
  2. Create UIViewController (you can also subclass UnityViewControllerBase if you wish) with some native controls: In my case I simply added a UILabel and UIButton (which I will use to toggle rotation direction) 
  3. Create a subclass for UnityAppController (e.g. MyUnityAppController) where we will override some methods:
  4.  
    // the header file
    #import "UnityView.h"
    #import "UnityAppController.h"
    @interface MyUnityAppController : UnityAppController
    @end
    
    // the implementation file
    #import "MyUnityAppController.h"
    #import "MyNativeViewController.h"
    @interface MyUnityAppController ()
    @end
    @implementation MyUnityAppController
    // I simply removed the user interaction from unityview
    - (UnityView*)initUnityViewImpl
    {
        UnityView *unityView = [[UnityView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        unityView.userInteractionEnabled = NO;
        return unityView;
    }
    // here I am using my UIViewController as _rootController
    - (void)createViewHierarchyImpl
    {
     _rootView = _unityView;
     _rootController = [[MyNativeViewController alloc] init];
    }
    // I had to override this method (although unity doesn't recommend it) to change one line 
    // instead of assigning _rootView as _rootViewController.view I've added it as a subview
    - (void)createViewHierarchy
    {
     [self createViewHierarchyImpl];
     NSAssert(_rootView != nil, @"createViewHierarchyImpl must assign _rootView");
     NSAssert(_rootController != nil, @"createViewHierarchyImpl must assign _rootController");
        
     [_rootController.view addSubview:_rootView];
     if([_rootController isKindOfClass: [UnityViewControllerBase class]])
      [(UnityViewControllerBase*)_rootController assignUnityView:_unityView];
    }
    @end
    
  5. Inside my UIViewController I set the UIButton to call "OnTogglePressed" method mentioned earlier when building the scene using UnitySendMessage method:
  6.  
    -(IBAction)togglePressed
    {
        UnitySendMessage("RotatingCube", "OnTogglePressed", "");
    }
    
  7. Last thing to do is to change the appcontroller name inside main.mm from UnityAppController to MyUnityAppController then you should be able to see the cube scene with the label and the button:
Video of it in action:



There are few things which need to be investigated: for example whether we can have only a part of the screen for Unity Scene (similar to what I've done with Android). Setting unityview frame didn't give me expected results so I might need to dig deeper.
For now this seems to work using full screen.

If anyone have good ideas or did something similar please share! :-)