New Deferred Rendering System on WaveEngine 2.0

For WaveEngine 2.0 we decided to change our render to allow users to have more control on lighting. The big challenge here was how to offer a better lighting system and still support old mobile devices with OpenGL 2.0.

The render system in WaveEngine 1.4.2 is known in computer graphics as Forward Rendering. It is fast, simple but limited since you cannot use for example more than 4 to 8 lights per object (there are some techniques that try to simulate more lights but do not give good quality), and you need to write a large amount of shaders to support some combinations efficiently.

So we spent a period studying others options like:

  • Forward Rendering Multipass
  • Deferred Rendering
  • Light-Pre Pass Deferred Rendering
  • Light Indexed Deferred Rendering

Multipass is too heavy and Deferred Techniques seem really the future because they separate the simple pass of Forward Rendering into two passes, one for lighting and another one for shading and the results using this technique are awesome, but the original implementation uses MRT which is not supported on the most common mobile devices currently.
After this research we decided that Light-Pre Pass Deferred Rendering was the best option for mobiles right now, but the original implementation is not supported on a lot the old mobile devices. We wanted our render to run on:

  • Windows, Linux, MacOS (DepthBuffer supported, MRT supported)
  • Windows Store DirectX 9.1 (DepthBuffer unsupported and MRT unsupported)
  • Windows Phone DirectX 9.3 (DepthBuffer supported)
  • iOS OpenGL ES 2.0 (DepthTexture extension)
  • Android OpenGL ES 2.0 (DepthTexture extension)
  • Android Tegra GPU (No DepthTexture extension, MRT supported)

So we needed to develop a custom Light-Pre pass Deferred Rendering technique to support all these old mobile versions, because a great amount of the current worldwide mobile base has theses specs.

Here you can see screenshots of how this technique works through these images:

Pass_GBufferPass_LightingPass_Forward

We have this temple demo with more than 30 dynamic lights running on old devices.

This new system also allows us to introduce more types of light source SpotLights, PointLights, Directional Lights and more types are coming: area lights, 2D light sources, occluders and projectors.

We will continue revealing more WaveEngine 2.0 features in the upcomming days.

Stay tuned at @waveengineteam and waveengine.net.

WaveEngine 1.4 (Dolphin) is out!

We are glad to announce that WaveEngine 1.4 (Dolphin) is out! This is probably our biggest release until now, with a lot of new features.

New Demo

Alongside with the 1.4 Release of Wave Engine, we have published in our GitHub repository a new sample to show all the features included in this new version.
In this sample you play Yurei, a little ghost character that slides through a dense forest and a haunted house.

Some key features:

  • The new Camera 2D is crucial to follow the little ghost across the way.
  • Parallax scrolling effect done automatically with the Camera2D perspective projection.
  • Animated 2D model using Spine model with FFD transforms.
  • Image Effects to change the look and feel of the scene to make it scarier.

The source code of this sample is available in our GitHub sample repository

Binary version for Windows PC, Here.

 

Camera 2D

One of the major improvements in 2D games using Wave Engine is the new Camera 2D feature.

With a Camera 2D, you can pan, zoom and rotate the display area of the 2D world. So from now on making a 2D game with a large scene is straightforward.
Additionally, you can change the Camera 2D projection:

  • Orthogonal projection. Camera will render objects 2D uniformly, with no sense of perspective. That was the most common projection used.
  • Perspective projection. Camera will render objects 2D with sense of perspective.

Now it’s easy to make a Parallax Scrolling effect using Perspective projection in Camera 2D, you only need to properly set the DrawOrder property to specify the entity depth value between the background and the foreground.

More info about this.

 

Image Effects library

This new release comes with and extesion library called WaveEngine.ImageEffects, this allows users an easy mode (one line of code) to add multiple postprocessing effects to their games.
The first version of this library has more than 20 image effects to improve the visual quality of each development.
All these image effects have been optimized to work in real time on Mobile devices.
Custom image effects are also allowed and all image effects in the current library are published as OpenSource.

More info about image effects library.

 

Skeletal 2D animation

In this new release we have improved the integration with Spine Skeletal 2D animation tool to support Free-Form deformation (FFD).
This new feature allows you to move individual mesh vertices to deform the image.

Read more.

 

Transform3D & Transform3D with real hierarchy

One of the most requested features by our users was the implementation of a real Parent / Child transform relationship.
Now, when an Entity is a Parent of another Entity, the Child Entity will move, rotate, and scale in the same way as its Parent does. Child Entities can also have children, conforming an Entity hierarchy.
Transform2D and Transform3D components now have new properties to deal with entity hierarchy:

  • LocalPosition, LocalRotation and LocalScale properties are used to specify the transform values relative to its parent.
  • Position, Rotation and Scale properties are used now to set global transform values.

Transform2D inherited from Transform3D component, so you can deal with 3D transform properties in 2D entities.

More info about this.

 

Multiplatform tools

We have been working on rewrite all our tools using GTK# to get these tools available on Windows, MacOS and Linux.
We want to offer the same development experience for all our developer regardless their OS.

More info about this.

 

Workflow improved

Within this WaveEngine version we have also improved the developer workflow, because one of the most tedious tasks when working in multiplatform games is the assets management.
So with this new workflow all these tasks will be performed automatically and transparently to developers, we obtaining amazing benefits:

  • Reduce development time and increased productivity
  • Improved the process of porting to other platforms
  • Isolated the developer from managing WPK files

All our samples and quickstarter have been updated to this new workflow, Github repository.

More about new workflow.

 

Scene Editor in progress

After many developers requests we have started to create a Scene Editor tool, We are excited to announce that we are already working on it.
It will be a multiplatform tool so developers will be able to use this tool from either Windows, MacOs, or Linux.

Community thread about this future tool

 

Digital Boss Monster powered by WaveEngine

Outstanding  success of the Boss Monster card game for iOS & Android kickstarter, which will be developed using WaveEngine in the next months.

If you want to see a cool video of the prototype here is the link.

Don’t miss the chance to be part of this kickstarter, only a few hours left (link)

 

More Open Source

We keep on publishing source code of some extensions for WaveEngine:

Image Effects Library: WaveEngine image library with more than 20 lenses (published)
Analytics Library: http://www.localytics.com/ (published)
Spine Integration: http://esotericsoftware.com/ (published)
TileMap Integration: http://www.mapeditor.org/ (coming soon)
Vuforia Integration: https://www.vuforia.com/ (coming soon)
Kinect V2 Integration: http://www.microsoft.com/en-us/kinectforwindows/ (coming soon)
Leap Motion Integration: https://www.leapmotion.com/ (coming soon)
Occulus Rift Integration: http://www.oculusvr.com/ (coming soon)

Complete code in our github repository.

 

Using Wave Engine in your applications built on Windows Forms, GTKSharp and WPF

Within this new version we want to help every developer using Wave Engine on their Windows Desktop Applications, like game teams that need to 
build their own game level editor, or University research groups that need to integrate research technologies with a Wave Engine render and show tridimensional
results. Right now, located at our Wave Engine GitHub Samples repository, you can find some demo projects that show how to integrate Wave Engine with Windows Forms, 
GtkSharp or Windows Presentation Foundation technologies.

Complete code in our github repository.

Better Visual Studio integration

Current supported editions:

Visual Studio Express 2012 for Windows Desktop
Visual Studio Express 2012 for Web
Visual Studio Professional 2012
Visual Studio Premium 2012
Visual Studio Ultimate 2012
Visual Studio Express 2013 for Windows Desktop
Visual Studio Express 2013 for Web
Visual Studio Professional 2013
Visual Studio Premium 2013
Visual Studio Ultimate 2013

 

We help you port wave engine project from 1.3.5 version to new 1.4 version

Within this new version there are some importants changes so we want to help every wave engine developers port theirs game projects
To the new 1.4 version.

More info about this.

 

Complete Changelog of WaveEngine 1.4 (Dolphin), Here.

Download WaveEngine Now (Windows, MacOS, Linux)

How to hide some parts of your API in C#

I have spent my last 3 years working on Plainconcepts in an amazing project called WaveEngine, this is a 2D/3D multiplatform game engine. And I learn a lot of things by working on this project, because I think it is very different develop apps using some libraries or frameworks than to develop a library or a framework for others developers who will later create apps using it. The first thing is that your customer are developer intead of final user and developer are very demanding with the libraries that they use (representative names of methods, intellisense documentation, clean API, consistency are some examples).

 

So when you work on a library, you sometimes need to hide some parts of your API like methods or properties which you don’t want the final developers to have access to or to change directly, or simply because you want to offer a clean API to these final developers. I would like to show you some techniques to get this behavior.

 

Using visibility operators like internal or protected internal

We need to have a Game class with a Draw method, which will be called from the main loop of our graphics framework. If our graphics framework needs to synchronize some methods or needs to prepare properties before calling the Draw method, to make it easier others task for our users we could make this method invisible in our API. Using internal this method will be accessible only from classes in the same assembly.

public class Game 

{

    ...

 

    internal void Draw()

    {

        // Render

    }

 

    ...

}

Draw method will be “public” for us from our assembly but invisible for our users.

 

Another possibility is if our users need to extend a framework class and override one of its methods, and we need to be able to call this new method from another class but we don’t want the final developer to call this method from another class.

public class Component

{

    internal protected abstract void ResolveDependencies();

}

 

public class UserComponent : Component

{

    protected internal override void ResolveDependencies()

    {

    }

}

 

We can call the ResolveDependencies method written for our user from other classes inside of our framework assembly, but for the final developer this method is only protected, so they can’t call this method from other classes in their code.

Our view:

public Manager()

{

    foreach (Component component in components)

    {

        component.ResolveDependencies();

    }

}

 

User view:

static void Main(string[] args)

{

    UserComponent component = new UserComponent();

 

    // Error: UserComponent.ResolveDependencies() cannot change access modifiers 

    // when overriding 'protected' inherited member Component.ResolveDependencies()

    component.ResolveDependencies();

}

Using explicit interfaces implementation

Explicit interfaces implementation is a great technique in C# that allows us to clean our final API. This is the most common technique used by Microsoft in all their APIs. The situation is, imagine that you need some extra method to simplify the final API for users, but all this aux method blurred the final API.

So if you make an explicit implementation for these aux methods, those methods will only be visible if you do a casting to the specific type.

public interface InternalMethods

{

    void Resume();

    void Active();

    void Execute();

}

 

public class GameObject : InternalMethods

{

    public void Play();

 

    public void InternalMethods.Resume()

    {

    }

 

    public void InternalMethods.Active()

    {

    }

 

    public void InternalMethods.Execute()

    {

    }

}

 

How to call each method:

class Program

{

    static void Main(string[] args)

    {

        GameObject obj = new GameObject();

        obj.Play();

 

        (obj as InternalMethods).Resume();

        (obj as InternalMethods).Active();

        (obj as InternalMethods).Execute();

    }

}

 

When our user uses intellisense on the GameObject instance, they will only see the Play method (clean API), and if he does a casting to the interface then he could access all the methods from the specific interface.

 

Using new to override methods

Imagine this situation, we want to draw 3D and 2D elements, to do that we need to call the low level API in DirectX so that all is drawn using 3D vectors (a 2D sprite is a 3D plane oriented to the camera any Z plane). But the user wants a 2D simplified API that only shows them 2D vectors.

We are going to create a base class Transform3D where we have all the properties needed to draw 3d models, plus we will have a Transform2D class with some helper methods and with some extra properties like zoom or flip options.

Internally you need all transform3D properties to be able to render the 2D elements, but your users want to see a simple API with only 2D vector properties. To solve this situation we will use the new keyword to override some methods and properties.

public class Transform3D

{

    protected Vector3 position;

 

    public Vector3 LocalPosition

    {

        get

        {

            return this.position;

        }

 

        set

        {

            this.position = value;

        }

    }

}

 

public class Transform2D : Transform3D

{

    public new Vector2 LocalPosition

    {

        get

        {

            return new Vector2(this.position.X, this.position.Y);

        }

 

        set

        {

            this.position.X = value.X;

            this.position.Y = value.Y;

        }

    }

}

 

Using this technique our user will only see the 2D LocalPosition property version from Transform2D and to access to the 3D LocalPosition property version they will need to do a casting to Transform3D like this:

Transform3D t1 = new Transform3D();

t1.LocalPosition = new Vector3(10);

 

Console.WriteLine(t1.LocalPosition.ToString());

 

Transform2D t2 = new Transform2D();

t2.LocalPosition = new Vector2(20);

Console.WriteLine(t2.LocalPosition.ToString());

 

(t2 as Transform3D).LocalPosition = new Vector3(30);

Console.WriteLine((t2 as Transform3D).LocalPosition.ToString());

image

 

It is important to remember that the purpose of all these techniques are only simplify the final API for the developers who use your libraries or frameworks.

 

I hope you enjoyed reading this post XD

Desarrollo multiplataforma con .NET ( Xamarin )

Objetivo

Conocer lo interesante del mercado de aplicaciones móviles, repasar los pros y contras de algunas tecnologías muy conocidas para el desarrollo de aplicaciones multiplataforma móvil, y conocer la alternativa perfecta para desarrolladores .NET a la creación de aplicaciones nativas para iOS, Android y Windows Phone.

Introducción

El desarrollo de aplicaciones móviles es un negocio emergente, por ello la mayoría de las ofertas de empleo para ingenieros actualmente está relacionado con ello. Esto es debido a que cada día somos más las personas conectadas entre sí a través de un teléfono móvil. Para hacernos una idea de cómo está evolucionando el mundo móvil echémosle un vistazo a la siguiente gráfica:

image

Como dato interesante tenemos que saber que la población mundial se estima en 7.000 millones de personas, y en esta gráfica podemos ver el número de dispositivos nuevos que se fabrican por año. Es sorprendente comprobar como solo este año 2014 se van a fabricar más de 1.000 millones de teléfonos. La estimación para el año 2014 es que en el mundo habrá más de 34.000 millones de smartphones acumulados, una auténtica barbaridad si recordamos la cifra de 7.000 millones de habitantes, por ello empresas como Google o Facebook tienen el foco puesto en formas de llevar la conectividad a aquellas zonas del planeta en las que aún no llega internet ni la telefonía móvil. También es interesante comprobar como este año 2014 será el primer año donde el número de tabletas fabricadas supera al número de ordenadores desktop  y laptops, estos datos ya no son desconocidos por nadie, por ello cualquier empresa a nivel mundial debe saber por pequeña que sea, que si crea una aplicación móvil y la coloca en los marketplaces podrá situarse en el bolsillo de muchos usuarios potenciales por un coste ínfimo.

Desde el punto de vista de los desarrolladores cada vez es más complicado abarcar todas las tecnologías necesarias para hacer desarrollos en la mayoría de los sistemas operativos móviles, ya que las empresas demandan a ser posible que sus aplicaciones estén en todos los móviles y desconocen las dificultades técnicas que esto implica.

Algunas de las plataformas móviles más conocidas actualmente son:

  • Android
  • iOS
  • Windows Phone
  • Symbian
  • RIM
  • FirefoxOS
  • UbuntuOS

Cada vez más empresas intentan sumarse al carro ya que saben que será el futuro estar ahí. Además de estos sistemas operativos con tecnologías diferentes luego existen muchos marketplaces diferentes, por ejemplo Nokia acaba de estrenar su NokiaX con un nuevo marketplaces para aplicaciones Android, o Amazon tiene su KindleFire con otro marketplace diferente.

Todo esto hace prácticamente imposible que pequeñas empresas de desarrollo de software puedan crear aplicaciones móviles para todos los móviles, ya que es necesario tener diferentes equipos especializados en diferentes tecnologías y lenguajes de programación, pero la demanda sigue estando ahí. Además muchas empresas grandes que quieren crear su primera aplicación no entienden que el coste de desarrollar su app móvil para que funcione en cualquier teléfono tenga un coste muy elevado, ajenos a todos los aspectos tecnológicos.

Como he dicho antes, la demanda es tan grande que hace necesario no solo que las grandes desarrolladoras de software puedan crear estas aplicaciones, sino que incluso desarrolladores freelance puedan hacerlo, por ello han aparecido una serie de tecnologías para permitir desarrollar aplicaciones “multiplataforma”, por ejemplo:

 

image image image image

Existen muchas más, pero estas son algunas de las más conocidas. Estas tecnologías se basan en crear aplicaciones nativas del dispositivo y colocar a pantalla completa un navegador web (similar a si el usuario entrara en el navegador de su teléfono y visitara una web), de esta forma se intenta simplificar y en vez de desarrollar una aplicación móvil, pues desarrollar una web que se vea bien en todos los navegadores móviles. Intenta aprovechar el hecho de que actualmente existen muchos más desarrolladores web que desarrolladores móviles que pueden ser fácilmente reconvertidos a desarrolladores de aplicaciones usando alguna de estas tecnologías.

Hasta aquí todo parece muy bonito, pero poco a poco van apareciendo más inconvenientes con estas tecnologías, algunas de ellas son:

  • El rendimiento es bastante malo incluso en dispositivos modernos, ya que se suelen utilizar lenguajes interpretados como javascript y dependen de las implementaciones del los navegadores en cada sistema operativo.
  • PhoneGap como el más conocido en este punto, es un proyecto cedido a la comunidad por lo que le cuesta estar actualizado.
  • El bajo rendimiento hace imposible crear aplicaciones con animaciones o transiciones fluidas, por lo que comparado con aplicaciones nativas a nivel de vistosidad de interfaz hay muchas limitaciones.
  • En Android por ejemplo, existen más de 14 versiones del SO en la calle, esto conlleva a que hay más de 14 versiones del navegador, y por lo tanto cada vez es más difícil garantizar que una web se ve exactamente igual en todos los Android del mercado, limitando el número de librarías JS o etiquetas que puedes utilizar.
  • A nivel de interfaz todo queda resuelto con el motor de dibujado del navegador, pero a nivel de lógica de la aplicación, debes hacer uso de Plugins escritos en los lenguajes nativos de la plataforma, para los cuales suele cumplirse la ley de Murphy (están todos menos el que buscas, están todos actualizados menos el que buscas, etc) y por tanto toca escribirte un plugin en el lenguaje nativo.
  • No se utilizan los controles nativos de cada plataforma.
  • Las aplicaciones no suelen diseñarse para verse en un dispositivo concreto, a una resolución concreta con un aspect ratio concreto, por lo que el resultado son controles estirados o desaprovechamiento del espacio.

Bueno aquí alguno de los inconvenientes, los cuales muchos son subjetivos (para gustos 32bits de color como diría un amigo mío) y muchos defensores acérrimos de estas tecnologías podrán argumentar que el rendimiento no es tan malo, o que a nivel de interfaz de usuario se puede hacer cualquier cosa igual al nativo.

Según mi experiencia personal, la conclusión es: (Spoiler Alert)

Crear una única aplicación que funcione bien en todas las plataformas móviles –> !!Es un mito!!

¿Por qué?

Pues por el Look & Feel, los usuarios esperan que las aplicaciones de su dispositivo móvil se comporten todas similares. Si en las aplicaciones hay un botón pues que este botón no solo se parezca al del resto de las apps de mi teléfono si no que también se comporte igual (solo posible si se utilizan los controles nativos de cada plataforma), el usuario espera que si su teléfono tiene un botón hardware para hacer back pues que la app no lo tenga, tienen ya interiorizada la navegación. Por ejemplo, los usuarios de Windows Phone esperan un control pivot o panorama en sus aplicaciones, esperan una estética, comportamiento y transiciones similares a las del sistema operativo, una organización de la arquitectura de la información en sus aplicaciones muy concreta al estilo Windows Phone, sino pues está el caso extremo de que los usuarios sean incapaces de manejar la aplicación y terminen borrándola. Si los dispositivos a nivel hardware son tan similares, es más hay modelos en Samsung por ejemplo con Android y Windows Phone, ¿qué diferencia tener un dispositivo de otro? pues el sistema operativo y las apps, los usuarios que se compran el último modelo con su sistema operativo favorito quieren que las apps aprovechen su teléfono y sentir que estas han sido desarrolladas para él.

image

Y ¿esto es así porque sí? ¿hay alguien que haya hecho un estudio sobre ello? Pues concretamente tenemos el caso de Facebook, empresa mundialmente conocida la cual desarrolló su aplicación móvil usando HTML. Para esta tarea seguramente puso a grandes profesionales y el resultado podemos analizarlo de muchas formas, pero una de ellas fue (para mi la importante), si a los usuarios de Facebook les gustó o no dicha app en sus móviles. Una forma de ver esto es observar sus reviews en el store:

image

La mayoría de los usuarios puntuaron esta aplicación con una sola estrella. Tras esto Facebook hizo la prueba de re implementar sus aplicaciones de forma nativa y ¿cuál ha sido la opinión de los usuarios en este caso?

image 

Pues que la mayoría de estos ahora han valorado esta aplicación con 5 estrellas, esto posiblemente demuestra que los usuarios esperan grandes experiencias de sus aplicaciones, que estas aprovechen el teléfono de última generación que tienen y les molesta mucho comprarse un teléfono nuevo y que una aplicación vaya lenta, no se parezca al resto de apps que tienen, o no sepan usarla porque tiene una navegación más al estilo de otro sistema operativo móvil.

Si estamos de acuerdo con todo esto, lo que nos queda es desarrollar nuestras apps de forma nativa. Llegados a este punto es cuando nos interesará la tecnología de Xamarin, la cual asumiendo que el desarrollo nativo es lo que los usuarios quieren nos ayuda a reducir tiempos de desarrollo y costes. Xamarin es el nombre de una empresa joven y a muchos les asusta esto de confiar en una empresa joven, por lo que repasaremos un poco la trayectoria del equipo de Xamarin.

Todo empezó en octubre de 1999 cuando Miguel de Icaza y Nat Friedman fundaron la empresa Ximian y esta era una proveedora de software libre para Linux e Unix. En Julio de 2001 tras el lanzamiento de la plataforma .NET, Miguel de Icaza estuvo trabajando en una implementación libre (para plataformas no Microsoft) del framework de .NET y así nació el proyecto Mono, que permitía usar lenguajes como C# para crear aplicaciones en Linux. En Julio de 2003 Ximian fue absorbida por Novell, durante este periodo continuaron mejorando el proyecto Mono y sacando nuevos proyectos basados en él. Por ejemplo, ellos fueron los que desarrollaron la tecnología Moonlight que fue una implementación para plataformas no Microsoft de Silverlight. Además de esto en su periodo en Novell, empezaron a desarrollar dos tecnologías llamadas Monodroid y Monotouch, las cuales hacía posible crear aplicaciones para Android y iOS usando mono. En 2011 Novell decide prescindir del equipo de Mono (unos 25 desarrolladores) y Miguel de Icaza vuelve a ponerse en contacto con Nat Friedman para fundar esta vez Xamarin y continuar con el proyecto Mono y los desarrollos alrededor de este.

Bien, ahora que estamos situados y conocemos un poco mejor a los que están detrás de este proyecto, estudiemos qué nos proponen con sus tecnologías Xamarin.iOS y Xamarin.Android. Xamarin nos da una muy buena alternativa para el desarrollo nativo en móviles, apoyándonos sobre la tecnología .NET (a través de mono) para mejorar la productividad.

image

Su propuesta de desarrollo para las plataformas de desarrollo más importantes (Windows Phone, iOS y Android) es la siguiente: desarrollar una interfaz de usuario nativa con los controles de cada plataforma, consiguiendo de esta forma la gran experiencia nativa que estábamos buscando, y a nivel de lógica de la aplicación en vez de desarrollar cada app con lenguajes diferentes como c++, java u objectiveC, desarrollar usando C# para todas, compartiendo por tanto la lógica de negocio de las aplicaciones para las tres plataformas móviles. Gracias a la productividad del lenguaje C# y a las PCLs ahora compatibles con Xamarin (Portable Class Library), esto hace que podamos compilar una librería solo una vez con unos mismos parámetros y dicha librería pueda ser enlazada desde la aplicación de Windows Phone, iOS e Android.

 

Xamarin.iOS

Empecemos analizando Xamarin.IOS:

  • ¿Qué es?
    • C# en iOS (iPhone, iPad)
  • ¿Qué no es?
    • Silverlight en iOS
    • Compact Framework para iOS
    • Desarrollo de apps en iOS sin necesitar un Mac (aunque existe la posibilidad de máquinas virtuales locales o en cloud como las que ofrece http://www.macincloud.com/)

¿Cómo sería el espacio de trabajo de un desarrollador que utilice Xamarin.iOS? Como dicen que una imagen vale más que mil palabras, aquí dejo un ejemplo práctico mío en casa:

image

Desarrollando en C# desde Visual Studio una aplicación para iOS, puedes compilar en Windows pero no puedes ejecutar, para ello necesitas tener emparejado un Mac por wifi por ejemplo a través de una app que viene con Xamarin. De esa forma cuando le das a desplegar “play” en Visual Studio, el código es enviado al Mac y este tras compilar a nativo es capaz de desplegar en un dispositivo con iOS. Lo interesante de todo esto es que puedes depurar,  y si pones un breakpoint en Visual Studio cuando tu app llega a esa línea en el dispositivo físico, el Visual Studio se para y puedes leer el valor de todas las variables independientemente de todo lo que está pasando por debajo. Con ello consigues desarrollar con apps desde Visual Studio aprovechando todas las herramientas de este gran IDE de desarrollo.

Otra opción es desarrollar directamente desde MacOS utilizando Xamarin.Studio, una versión del proyecto Monodevelop específica para desarrollar con Xamarin. También puedes desplegar sobre el emulador que viene con el SDK de Apple junto al Xcode:

 image

Algo muy interesante de las versiones actuales de Xamarin.iOS que no estaba en las versiones iniciales es que para crear la interfaz de usuario nativa (ficheros xib), podemos usar directamente XCode o un nuevo editor visual en Xamarin Studio:

image designer_new

 

¿Pero cómo es posible que una aplicación en C# usando mono corra en iOS, si Apple eliminó hace años la posibilidad de que las aplicaciones en su plataforma pudieran modificar su código en tiempo real? Algo que afectó a las aplicaciones que se desarrollaban con flash por ejemplo, y algo necesario para que el CLR de mono vaya Jiteando código en tiempo real.

Pues la solución que dio el equipo de Xamarin fue usar AOT (Ahead Of Time o precompilación a binario), apoyándose en que el hardware de los dispositivos iOS es bastante similar. Sino esto implicaría pues que dependiendo de la arquitectura hardware la aplicación final precompilada funcionaría o no. Esto no es nuevo, Microsoft tiene desde hace tiempo una herramienta llamada NGen, el problema es que al ser compilado a binario al igual que cuando compilas una aplicación en C++ para desktop pues necesitas una versión para x86 y otra para x64. Por suerte esto no ocurre en iOS ahora mismo, y si ocurriera la solución sería similar a la que está planteando Microsoft ahora con .NET native para aplicaciones en su store, la idea es compilarlas a binario de la máquina para conseguir un rendimiento prácticamente igual al desarrollo en C++ (ya que se utiliza directamente el compilador de C++ con todas sus optimizaciones) para cada arquitectura y subir todo esto al store. Cuando un usuario vaya a descargar una app este enviará al servicio su arquitectura y por tanto se descargará el binario para su arquitectura exacta, siendo por tanto transparente para el usuario.

 

Xamarin.Android

Analicemos ahora Xamarin.Android, qué es lo que nos ofrece:

  • No necesitamos un Mac
  • Completo soporte desde Visual Studio
  • Xamarin Studio como IDE de desarrollo gratis en Windows y Mac
  • Compilación embebiendo un runtime de mono en la aplicación (JIT)

A nivel de arquitectura sobre todo para temas avanzados es necesario conocer que internamente tenemos:

image

El sistema operativo Android, nuestra aplicación corriendo sobre el mono runtime embebido ejecutando nuestro código y como para ciertos bindings (acceso a controles o servicios del SO) internamente hay comunicación contra las APIs en java pues Dalvik está corriendo, esto solo hay que tenerlo en cuenta para ciertos temas avanzados. Por ejemplo en un caso muy concreto en el que estábamos trabajando, estábamos implementando un binding nativo para el acelerómetro y los valores vienen dados por una API en java, y (X,Y,Z) venían en forma de array. Al copiar estos valores era necesario disposear manualmente el array de java si no queríamos tener un memory leak. Por suerte ya existen múltiples bindings hechos para evitar que usuarios de Xamarin.Android tengan que preocuparse de estas cosas, y puedan centrarse en el desarrollo de sus apps.

La integración en Visual Studio de Xamarin.Android es mejor, la compilación es más rápida al no requerir enviar nada a otro pc, la depuración también es buena en las últimas versiones de Xamarin.Android e incluso tenemos un diseñador de interfaz para generar los ficheros nativos “axml” desde Visual Studio:

image

Para probar nuestras apps podemos enchufar directamente nuestro dispositivo Android a nuestro PC con Windows y desplegar desde Visual Studio directamente de forma bastante cómoda, si queréis probar en un emulador, os recomiendo que no uséis el oficial de Google, su rendimiento es vergonzoso (lo iba a poner más suave pero es que se lo merece y con mayúsculas). Mucho mejor usar los emuladores de Genymotion que corren sobre x86 y VirtualBox, y dan un gran rendimiento. El problema es que no podéis tener HyperV activado en vuestras máquinas, por lo que habrá que elegir.

Resumen de la forma de desarrollo:

  • Crea la interfaz de usuario nativa de tus aplicaciones, para conseguir la experiencia que los usuarios quieren, para ello xaml en Windows Phone, axml en Android y xib en iOS, lo puedes hacer con los editores nativos o con los propuestos aquí.
  • Comparte todo el código de lógica de tus apps usando C# + PCL, de esta forma puedes tener servicios en Azure por ejemplo, que utilicen exactamente el mismo código en Android, iOS, Windows, si hay cambios del servicio o errores, los corriges una vez en código y se corrigen en todas las plataformas. Facilitando mucho el mantenimiento de estas.

Las tecnologías de Xamarin, son potentes y está bien actualizadas y por ello habrá que “pasar por caja” si quieres usarlas.

 

ShowCase

¿Qué aplicaciones se han hecho con esta tecnología y qué resultados se han obtenido?

Xamarin Mobile World Congress App

image

Esta aplicación fue desarrollada por el equipo de Xamarin, los resultados que presentaron fueron:

  • iPhone + iPad (2.476 LOC / 2 apps) – 57% reutilizado
  • Android (1095 LOC)- 60%
  • WP7 (896 LOC) – 65%

Rdio

image

Los desarrolladores de este servicio de música stream vía internet destacaron:

  • Que podían sacar updates al mismo tiempo, si tener que esperar por tener 3 equipos distintos unos a otros para lanzar una actualización con un cambio en el servicio de streaming.

 

iCircuit

image

Los desarrolladores de este diseñador de circuitos, destacaron tras su desarrollo con Xamarin:

  • La productividad del lenguaje C# comparado con desarrollos nativos en C++.
  • Y según ellos reutilizaron para su versión Windows Phone y iOS entre el 70% y el 80% del código.

TouchDraw

image

Los desarrolladores de este diseñador de diagramas, destacaron:

  • Algo que antes en nativo habían empleado 3 semanas ahora lo habían hecho en 1.
  • Compartir el código de lógica reducía mucho el Bug Fixing (mantenimiento).

Sage Murano, Sage Despachos y Sage Contaplus

image

Personalmente me ha tocado gestionar este proyecto en Plainconcepts, y estos son los datos de nuestra propia experiencia:

  • Teníamos que desarrollar 12 aplicaciones nativas, (3 aplicaciones para las versiones Phone y Tablet en Android e iOS)
  • Nos hemos aprovechado del uso de Universal Apps para tener una misma solución en Phone y Tablet con diferentes ficheros de layout e interfaz.
  • El proyecto lo hemos desarrollando usando Visual Studio 2013 y gestionado como un proyecto de Git a través de TFS 2013 (Xamarin Studio tiene integración con Git por lo que es más fácil compartir código entre Mac y Windows).
  • Utilizando los emuladores de iOS nativos o del SDK y los de Genymotion para Android que también se integran directamente con Visual Studio.
  • Utilizando MVVMCross para la navegación por ViewModels.
  • Hemos reutilizado entre el 50% y el 60% del nuestra base de código entre las 12 apps.
  • 12 apps nativas desarrolladas por 3 desarrolladores en 3 meses y medio.

 

Este es el aspecto del proyecto en Visual Studio, para Android e iOS:

image image

 

Extra

Para terminar algunas cosas también muy interesantes alrededor de este proyecto, son:

La existencia de un marketplace de controles cross-platform (Xamarin components), algunos gratis y otros de pago, interesante tanto para desarrolladores de apps como para empresas que se dediquen a la creación de controles para terceros:

image

Ahí existen algunos controles o librerías interesante como Xamarin.Mobile, que nos ofrece una API unificada (Windows Phone, iOS, Android) para acceder a diferentes servicios muy comunes de las plataformas, directamente desde nuestro código C#:

image

y por último el servicio de testCloud de Xamarin, el cual aún no está abierto para todo el mundo y para poder usarlo hay que hacerlo previo contacto con ellos. Ofrece una forma fácil de evitar a los desarrolladores tener que comprar múltiples dispositivos Android para probar sus aplicaciones en ellos, (este problema de fragmentación en Android supone un coste muy elevado de compra de dispositivos para muchas empresas). Pues bien este servicio ofrece la posibilidad de realizar pruebas sobre emuladores o dispositivos físicos que son grabados por cámaras y comprobar si nuestra app funciona bien en la mayoría de estos dispositivos:

image

Algo muy interesante además es que cada dispositivo muestra la información de porcentaje a nivel mundial de ese tipo de dispositivo que existe, de manera que si tu aplicación funciona en la mayoría de los dispositivos Android menos en uno y este tiene un porcentaje muy pequeño, por ejemplo el 1% a nivel mundial, pues ya puedes tener argumentos tú o tu cliente para decidir si es asumible que en dichos dispositivos la aplicación no se vea perfectamente.

 

Bueno pues nada más, espero que todo esto aclare qué es y qué no es Xamarin, y esto os ayude a decidir cuándo me interesa usar Xamarin y cuándo no, lo cual depende de muchísimos aspectos, como capacidades del equipo, costes, tiempos, etc.

Optimizando un motor de videojuegos en C#

Hacia tiempo que no escribía un post y por fin he encontrado un tema interesante (al menos para mi). Desde hace 3 años trabajo en Plainconcepts desarrollando un motor multiplataforma de videojuegos llamada WaveEngine.

Todo el código que escribimos en WaveEngine debe en medida de lo posible ser lo más eficiente posible, ya que para que un videojuego corra a 60fps, todas las operaciones de IA, física, update y render de nuestro juego deben ejecutarse en como máximo 16 milliseconds. Como me decía Ignacio Castaño (una persona de la que aprendí muchísimo mientras estuve colaborando con NVidia) “lo difícil no es hacer las cosas (dentro del mundo de los videojuegos, efectos, graphics technique,…) lo difícil es conseguir que vayan rápidas”, lo cual suele marcar la diferencia entre desarrollos profesionales y desarrollos amateur.

En un graphics engine siempre hay partes que se pueden optimizar y un pequeño cambio a bajo nivel puede provocar grandes resultado a alto nivel, es importante destacar aquí que cuanto menos milliseconds consuma el engine más milliseconds quedarán libres para el usuario de dicho engine.

Gran parte de WaveEngine ha sido escrito en C# por lo que esto te lleva a un escenario concreto, en el que lo interesante es analizar cuales son las formas más eficientes de escribir código C# para este tipo de software en determinados aspectos. En un graphics engine se utilizan muchísimas estructuras de datos como vectors, matrix, quaternions, colors, estructuras que se crean y se destruyen muy rápido, por este motivo se suelen usar structs en vez de class, para intentar en medida de lo posible que el GC esté saltando continuamente, ya que gran parte de las veces estas struct se crean directamente en el stack en vez de en el heap.

WaveEngine al ser un motor multiplataforma por debajo puede usar cualquier API de dibujado a bajo nivel, como DirectX u OpenGL, esto significa que ciertas estructuras de datos deberán ser transformadas antes de poder ser enviadas al driver de la GPU. Ya escribí anteriormente cómo podemos hacer un casting entre estructuras similares usando código unsafe para evitar tener que hacer new de una estructura nueva en dichas microoperaciones que se realizan durante el render.

 

Tras aquellas optimizaciones, que fueron útiles para los adaptadores que usaban directX, ahora la idea era intentar realizar una optimización similar pero para OpenGLES, en este caso las funciones de la API de OpenTK lo que necesita como parámetro no son struct propias sino array de floats. Por ejemplo, una instancia de la estructura WaveEngine.Common.Math.Matrix son 16 floats (64bytes), y lo que necesita la API de OpenTK es que le pasemos dicha información en forma de float[] matrix = new float[16]. Como el motor es multiplataforma no podemos cambiar las estructuras básicas que se usan, debemos hacer la conversión de tipos en los adaptadores, es decir cada adaptador es el encargado de transformar los datos a lo que necesite.

¿Cuál sería la primera implementación que se nos ocurre hacer para convertir Matrices 4×4?, pues crear un método extensor que realice la transformación.

public static float[] ToAdapterMatrix(this WaveEngine.Common.Math.Matrix mat)

{

    return new float[]

    {

        mat.M11,

        mat.M12,

        mat.M13,

        mat.M14,

        mat.M21,

        mat.M22,

        mat.M23,

        mat.M24,

        mat.M31,

        mat.M32,

        mat.M33,

        mat.M34,

        mat.M41,

        mat.M42,

        mat.M43,

        mat.M44

    };

}

 

A diferencia de las struct la ventaja aquí es que los arrays no se pasan como copia, la desventaja es que los arrays suelen crearse en el heap, por lo que el GC tendrá que trabajar mucho si estamos continuamente creando y destruyendo arrays.

Por este motivo la primera mejora que se nos puede ocurrir aquí, es intentar tener una serie de arrays cacheados desde la clase que use este método extensor, que podamos reutilizar una y otra vez, consiguiendo de esa forma eliminar que se haga un new de una array para cada llamada.

public static void ToAdapterMatrix1(this WaveEngine.Common.Math.Matrix mat, float[] result)

{

    result[0] = mat.M11;

    result[1] = mat.M12;

    result[2] = mat.M13;

    result[3] = mat.M14;

    result[4] = mat.M21;

    result[5] = mat.M22;

    result[6] = mat.M23;

    result[7] = mat.M24;

    result[8] = mat.M31;

    result[9] = mat.M32;

    result[10] = mat.M33;

    result[11] = mat.M34;

    result[12] = mat.M41;

    result[13] = mat.M42;

    result[14] = mat.M43;

    result[15] = mat.M44;

}

Y la pregunta es, ¿podemos optimizarlo más?, la respuesta no es simple (si o no) ya que como veremos más adelante esto depende de la plataforma y del CLR del que dispongamos en ella.

Intentemos hacer algo similar al articulo anterior sobre conversiones de tipo entre struct usando código unsafe. Haremos una prueba a ver si es posible, el objetivo es mapear los valores de una struct en un array. Podemos por ejemplo obtener el puntero a un array y usar un for para ir avanzando el puntero e ir copiando cada valor.

 

Hagamos un pequeño ejemplo:

float[] array = new float[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

Matrix m = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);

 

unsafe

{

    Matrix* pmatrix = &m;

    float* pmf = (float*)(Matrix*)&m;

 

    for (int i = 0; i < 16; i++)

    {

        float v = (float)*(pmf + i);

        Console.WriteLine(v + " - " + array[i]);

    }

 

    fixed (float* p = array)

    {

        for (int i = 0; i < 16; i++)

        {

            *(p + i) = *(float*)(pmf + i);

        }

 

 

        for (int i = 0; i < 16; i++)

        {

            float v = (float)*(pmf + i);

            Console.WriteLine(v + " - " + array[i]);

        }

    }

}

 

Es importante destacar aquí el significado de la palabra fixed, la cual indica al GC que hemos obtenido un puntero al array y por lo tanto dentro del bloque fixed queremos que se evite la posibilidad de que el GC tras una pasada mueva dicho array a otra posición. Internamente el GC en las pasadas no solo tiene que compactar y liberar espacio sino también debe actualizar las referencias a las zonas de memoria donde hayan cambiado los objetos, arrays, etc.

Si ejecutamos dicho código podemos ver como el contenido de la matrix es volcado sobre los elementos del array.

image

 

Pero claro, realmente esto es lo mismo que teníamos en el método anterior, vamos copiando float a float desde la struct al array, lo ideal ya que estamos trabajando con punteros sería poder copiar el contenido completo en una sola instrucción, pero a la hora de asignar los valores el puntero que tenemos tiene 4 bytes (float*), necesitaríamos un puntero de 64Bytes de tamaño.

Pues sinceramente no sabía que esto se podía hacer hasta que no me surgió la necesidad de hacerlo XD:

float[] array = new float[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

Matrix m = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);

 

unsafe

{

    Matrix* pmatrix = &m;

 

    fixed (float* p = array)

    {

        Matrix* marray = (Matrix*)p;

 

        *marray = *pmatrix;

    }

}

 

Ahora ya no necesitamos el bucle que copia los valores de float en float, directamente estamos moviendo los 64bytes de un sitio a otro y con una sola instrucción. Esto ha sido posible haciendo un doble casting entre punteros, es decir Matrix* marray = (Matrix*)(float*)array.

En el código anterior estamos usando dos punteros aunque solo uno esta fijado (fixed), el otro si es struct que se encuentra en el stack no sería necesario, pero para evitar posibles problemas podamos quitar dicho puntero y usar directamente m. Veamos como metemos todas las ideas dentro de un método extensor:

public static unsafe void ToAdapter(this WaveEngine.Common.Math.Matrix matrix, float[] result)

{

    fixed (float* p = result)

    {

        Matrix* marray = (Matrix*)p;

        *marray = matrix;

    }

}

 

Lo curioso aquí es ver cómo estamos realizando una asignación directa entre una struct y un array.

 

Después de todo este esfuerzo mental XD, la pregunta ahora es, ¿esto es más eficiente que lo anterior?, la lógica inicialmente nos dice si, pero con eso no nos vale, hay que hacer pruebas. Vamos a realizar un test simple de rendimiento, en el cual probemos varias opciones, por ejemplo:

 

  1. Versión inicial en la que se hacía un new en cada llamada.
    public static float[] ToAdapterMatrix(this WaveEngine.Common.Math.Matrix mat)

    {

        return new float[]

        {

            mat.M11,

            mat.M12,

            mat.M13,

            mat.M14,

            mat.M21,

            mat.M22,

            mat.M23,

            mat.M24,

            mat.M31,

            mat.M32,

            mat.M33,

            mat.M34,

            mat.M41,

            mat.M42,

            mat.M43,

            mat.M44

        };

    }

     
  2. Asignación de valores.
    public static void ToAdapterMatrix1(this WaveEngine.Common.Math.Matrix mat, float[] result)

    {

        result[0] = mat.M11;

        result[1] = mat.M12;

        result[2] = mat.M13;

        result[3] = mat.M14;

        result[4] = mat.M21;

        result[5] = mat.M22;

        result[6] = mat.M23;

        result[7] = mat.M24;

        result[8] = mat.M31;

        result[9] = mat.M32;

        result[10] = mat.M33;

        result[11] = mat.M34;

        result[12] = mat.M41;

        result[13] = mat.M42;

        result[14] = mat.M43;

        result[15] = mat.M44;

    }

  3. Asignación de valores pero le añadimos al método el atributo [MethodImpl(MethodImplOptions.AggressiveInlining)] para intentar que se haga Inline de dicho método (al estilo de C++).
    [MethodImpl(MethodImplOptions.AggressiveInlining)]

    public static void ToAdapterMatrix2(this WaveEngine.Common.Math.Matrix mat, float[] result)

    {

        result[0] = mat.M11;

        result[1] = mat.M12;

        result[2] = mat.M13;

        result[3] = mat.M14;

        result[4] = mat.M21;

        result[5] = mat.M22;

        result[6] = mat.M23;

        result[7] = mat.M24;

        result[8] = mat.M31;

        result[9] = mat.M32;

        result[10] = mat.M33;

        result[11] = mat.M34;

        result[12] = mat.M41;

        result[13] = mat.M42;

        result[14] = mat.M43;

        result[15] = mat.M44;

    }

  4. Versión usando código Unsafe.
    public static unsafe void ToAdapter(this WaveEngine.Common.Math.Matrix matrix, float[] result)

    {

        fixed (float* p = result)

        {

            Matrix* marray = (Matrix*)p;

            *marray = matrix;

        }

    }

  5. Código unsafe + inline.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]

    public static unsafe void ToAdapter1(this WaveEngine.Common.Math.Matrix matrix, float[] result)

    {

        fixed (float* p = result)

        {

            Matrix* marray = (Matrix*)p;

     

            *marray = matrix;

        }

    }

 

Ya tenemos la casuística a probar, faltan los tests que lancen miles de veces estas operaciones para sacar valores en ms con los que podamos evaluar el rendimiento de cada estrategia.

La siguiente tabla la he podido crear con la ayuda de David Ávila (un compañero del equipo de Wave). Hemos medido el rendimiento de esto no solo en Windows, sino también en WindowsPhone, iOS y Android, estos dos últimos utilizando los compiladores de Xamarin.

results

 

Windows

En esta plataforma los resultados son algo sorprendentes. Lo más eficiente es hacer la asignación simple, por lo que podemos pensar que usar código unsafe no sale gratis, pero también es sorprendente ver como el atributo [MethodImpl(MethodImplOptions.AggressiveInlining)] no le sienta nada bien. También puede ser que la palabra fixed requiera una sincronización con el thread del GC y que en esto se nos vaya gran parte del tiempo.

 

iOS

Esta plataforma es posiblemente la más beneficiada tras esta investigación, el rendimiento usando código unsafe es casi 10 veces mejor. Es importante destacar aquí que el compilador en iOS de Xamarin utiliza AOT para transformar directamente a binario de la plataforma, motivo por el cual posiblemente pueda tomar ventaja cuando activamos unsafe.

 

Android

En esta plataforma el código unsafe tampoco ayuda demasiado. Quizás también se deba a que a diferencia de iOS, aquí si que se usa un CLR de mono.

 

Windows Phone

La primera pregunta que mucha gente podéis haceros es, pero ¿es posible usar código unsafe en Windows Phone 8?, ya que al crear un proyecto la opción para habilitar el código unsafe está deshabilitada y no puede ser modificada. Lo curioso es que si que nos permite añadir esto pero manualmente, modificando los csproj y añadiendo en cada plataforma de compilación <AllowUnsafeBlocks>true</AllowUnsafeBlocks>.

Parece que el CLR para ARM que ha hecho Microsoft en Windows Phone 8 tiene un comportamiento similar al de x86 a diferencia de las versiones anteriores, y por lo tanto la asignación simple es también la mejor opción.

 

 

Bueno pues esto ha sido todo, espero que todo esto os haya gustado tanto como a mi XD, cualquier comentario, opinión o interpretación distinta de los resultado siempre será bienvenida (nunca es mal día para aprender algo nuevo).

Wave Engine ya está disponible

Este jueves por fin lanzamos WaveEngine (waveengine.net) tras más de dos años de desarrollo. Creo que es un proyecto muy interesante para desarrolladores C# por lo que os animo a todos a probarlo y darnos feedback para continuar mejorándolo.

A continuación para motivaros a probarlo os voy a contar rápidamente algunas API highlights del proyecto, las cuales desde mi punto de vista son bastantes atractivas e interesantes para desarrollados en C#.

WaveEngine ha sido diseñado bajo los principios de Component Based Game Engine (el primer artículo que yo leí sobre esta arquitectura fue este): existe el concepto de scene, donde se registran las Entities y estás a su vez están formadas por Components, puede observarse esto en la inicialización de una entidad, lo cual hace muy fácil la integración de dicha API con un editor.

Entity primitive = new Entity("Primitive")

    .AddComponent(new Spinner() { AxisTotalIncreases = new Vector3(1f, 2f, 1f) })

    .AddComponent(new Transform3D())

    .AddComponent(Model.CreateSphere())

    .AddComponent(new BoxCollider())

    .AddComponent(new ChangeModel())

    .AddComponent(new MaterialsMap())

    .AddComponent(new ModelRenderer());

 

EntityManager.Add(primitive);

 

Además puede apreciarse la aplicación del concepto Fluent Interface por toda la API para facilitar la legibilidad y productividad del código del usuario.

Una vez construida una entidad se ha aprovechado el uso de Generics para facilitar el acceso a alguna de las partes de una entidad:

var boxCollider = primitive.FindComponent<BoxCollider>();

 

Sin embargo para entidades prefabricadas (Camaras, Controles de UI, etc) en el motor, esta arquitectura presenta serios problemas para el usuario, ya que es prácticamente necesario conocer los componentes que conforma una entidad para poder editar cualquier característica de ellos.

primitive.FindComponent<Spinner>().IncreaseY = 0;

 

Por ello descartamos el uso de factorías que construían las entidades prefabricadas fácilmente pero seguía manteniendo el problema para el usuario de tener que conocer su construcción interna para poder modificar cualquier cosa.

La solución llegó tras la introducción del Decorator Pattern, el cual permite mantener los principios de la arquitectura (Component Based Game Engine) y aportar una API común para las entidades prefabricadas o de terceros, algunos ejemplos:

Button b_reset = new Button()

{

    Text = "Reset",

    Margin = new Thickness(10, 0, 0, 20),

    VerticalAlignment = VerticalAlignment.Bottom,

    Foreground = Color.White,

    BackgroundColor = lightColor,

};

 

b_reset.Click += (s, o) =>

{

    radio4.IsChecked = true;

    toggleTexture.IsOn = true;

    sliderScale.Value = 50;

    sliderRotX.Value = 0;

    sliderRotY.Value = 0;

};

 

El decorador permite que el usuario no tenga porqué conocer la construcción interna de un botón de UI:

this.entity = new Entity(name)

.AddComponent(new Transform2D())

     .AddComponent(new RectangleCollider())

       .AddComponent(new TouchGestures())

      .AddComponent(new ButtonBehavior())

       .AddComponent(new PanelControl(DefaultWidth, DefaultHeight))

       .AddComponent(new PanelControlRenderer())

       .AddComponent(new BorderRenderer())

       .AddChild(new Entity("TextEntity")

           .AddComponent(new Transform2D()

           {

               DrawOrder = 0.4f

           })

           .AddComponent(new AnimationUI())

           .AddComponent(new TextControl()

           {

                   Text = "Button",

                   Margin = this.defaultMargin,

                   HorizontalAlignment = HorizontalAlignment.Center,

                   VerticalAlignment = VerticalAlignment.Center

           })

           .AddComponent(new TextControlRenderer()));

 

A la vez que le ofrece una API sencilla a la hora de modificar cualquiera de sus propiedades.

Por otra parte internamente estos componentes son elementos de software aislados respetando así una de las máximas de los principios SOLID, bajo acoplamiento y alta cohesión.

Aunque en ellos se permite el cacheo de otros componentes (por motivos de eficiencia) de la misma o de otras entidades las cuales necesitan para funcionar, por ejemplo el ModelRenderer necesita un Transform3D, un Model y un MaterialMap para poder funcionar. Para facilitar esta tarea se ha hecho uso de Custom Attributes y de Reflexión:

/// <summary>

/// <see cref="Model"/> to render.

/// </summary>

[RequiredComponent]

public Model Model;

 

/// <summary>

/// Materials used rendering the <see cref="Model"/>.

/// </summary>

[RequiredComponent]

public MaterialsMap MaterialMap;

 

/// <summary>

/// Transform of the <see cref="Model"/>.

/// </summary>

[RequiredComponent]

public Transform3D Transform;

Este código extraído directamente de la clase MeshRenderer es todo lo que se necesita para que este componente pueda empezar a leer propiedades de otros componentes en la entidad en la que se añada.

Además puede añadirse el parámetro opcional de RequiredComponent(false) para especificar si el componente que debe buscar es un Exact Type o un Given Type.

Cabe destacar que también es posible realizar esta conexión manualmente a través de los métodos ResolveDependencies y DeleteDependencies de un custom component. Además estas conexiones pueden actualizarse dinámicamente durante la ejecución del programa, es decir añadir o sustituir componentes de una entidad. Tras hacer las modificaciones precisas es necesario llamar al método RefreshDependencies de la entidad para que todos sus componentes se desconecten de los antiguos y se conecten a los nuevos automáticamente.

Por último hacer hincapié en la clase estática WaveServices, la cual facilita el acceso a todos los servicios del motor sin tener que recordar sus nombres:

– AssetsContainer, GraphicsDevice, InAppPurchases, Input, Layout, Microphone, MusicPlayer, Platform, Random, ScreenLayers, SoundPlayer, Storage, TaskScheduler, TimerFactory, TouchPanel,VideoPlayer

 

Si todo esto te ha resultado interesante te animo a probar la API, en ella podrás encontrar muchas más piezas interesante de software.

 

Te esperamos en la sección de Community en waveengine.net.

Entrevista para la Newsletter de Visual Studio

Javier Cantón lleva vinculado a Microsoft los últimos 10 años. Reconocido MVP experto en tecnologías como DirectX y XNA centradas en el desarrollo de Videojuegos y tratamiento de imágenes, ha centrado parte de su trabajo y carrera profesional en el mundo del desarrollo vinculándose con las últimas tecnologías del entretenimiento. Licenciado en Ingeniería Informática y especializado en el tratamiento y procesamiento de imágenes en tiempo real, ha colaborado en numerosos proyectos de Microsoft y Nvidia. En la actualidad forma parte del equipo de Plain Concepts vinculado al desarrollo de juegos llamado Weekend Game Studio.

Desde el equipo de Visual Studio News nos gustaría preguntarte por tus comienzos y proyectos a lo largo de tu carrera, existe una gran expectación en la actualidad en este tipo de tecnologías dentro del ámbito de la informática y como negocio , el mundo del desarrollo de videojuegos sigue siendo una industria en crecimiento y desarrollo.

¿Cual ha sido tu trayectoria hasta llegar a tu situación actual en el mundo de los videojuegos?

Mi primer ordenador lo tuve bastante tarde, a los 12 años y más tarde a los 14 años empecé a desarrollar juegos con un grupo de amigos que nos hacíamos llamar “Studios Tortuga”. El primer juego fue una aventura gráfica llamada “El secreto de la Esfinge” y lo desarrollamos usando Delphi 2.0. Pronto me empezó a entusiasmar la idea de crear juegos en 3D como aquellos profesionales a los que jugaba, pasé bastante tiempo probando tecnologías Allegro, Div2, DarkBasic pero no fue hasta descubrir Blitz3D (engine que usaba DirectX 7) cuando nos planteamos crear nuestro primer juego en 3D Se llamó “Mantigoras”, aquí podéis ver un vídeo sobre él:

“Mantigoras” el primer proyecto en 3D

Con este proyecto aprendimos muchas cosas, por ejemplo que debes intentar estimar lo mejor posible el alcance de un proyecto en su fase inicial porque sino siempre se complica.

Continuamos aprendiendo tecnologías y lenguajes de programación como c++ y entonces buscamos otra herramienta de desarrollo con la cual pudiéramos llegar más lejos y hacer posible todas las ideas que pasaban por nuestras mentes. La herramienta elegida se llamó Cipher (un engine en c++), al cual tuvimos que añadirle varias librerías de física 3D y red. Con todo esto ya estabamos preparados para afrontar un nuevo proyecto que se llamó “Mexus”, otro vídeo:

Mexus, juego online desarrollado en C++/OpenGL

Tras terminar este proyecto, comencé mis estudios de Ingeniería Informática en la Universidad de Sevilla. Durante los años de universidad formé parte del primer Club .NET de Microsoft en España, lo cual me ayudó a mejorar profesionalmente perdiendo el miedo a hablar en público y dar conferencias. Mi primera charla en el Club .NET consistió en una tarjeta electrónica que había desarrollado y que la usaba para comunicar un PC con cualquier aparato eléctrico (apagar/encender), pero no quedaba ahí el invento ya que el objetivo fue mostrar cómo encender un árbol de navidad desde una PDA la cual se conectaba contra un PC a través de un “web service” y este a su vez encendía las luces del árbol utilizando la mencionada tarjeta electrónica.

También participé 3 veces en el concurso Microsoft Imagine Cup, formando equipo con diferentes personas. El primero proyecto en el que participé se llamó “S.I.M.A.P” que consistía en un sistema inteligente para Parkings, de forma que desde un PC o PDA podías acceder a un servicio que te informaba de cual era el parking más cercano con plazas libres, y así asegurarte que había sitio a tu llegada.

El segundo proyecto fue muy divertido. Se llamó “Step By Step”, para los que lo desarrollamos fue el predecesor de “Kinect”, ya que utilizábamos 2 webcams para capturar los movimientos de una persona en 3D, esta información la utilizábamos posteriormente para evaluar la corrección de ciertos ejercicios de rehabilitación. Este proyecto fue todo un éxito y conseguimos ganar la final de Microsoft Imagine Cup en España e ir a la final internacional en India.

El último proyecto con el que participe en Imagine Cup también se convirtió en mi proyecto fin de carrera. Consistía en un dashboard que se instalaba sobre el sistema operativo el cual estaba centrado en niños que estudiaban a distancia y que necesitaban utilizar el sistema solo con movimientos de cabeza o similares. Aquí os dejo un vídeo de las primeras pruebas que hacia con la WebCam de la pantalla del portátil (todo esto con Kinect hubiese sido mucho más fácil):

Primeras pruebas de gestos

 

Durante los años de universidad aprendí varias tecnologías de Microsoft útiles para el desarrollo de videojuegos como MDX o XNA junto a Visual Studio, lo cual me impulsó a crear una comunidad llamada XNACommunity a la cual se unieron varios desarrolladores de videojuegos. En ella aprendíamos y compartíamos técnicas, artículos y ejemplos.

Durante los últimos años de carrera me interesé mucho por dos productos de Microsoft que me fascinaron, el Kinect y la predecesora de “SUR40 (la mesa multitáctil de Microsoft), y para un trabajo de una asignatura junto a dos compañeros nos propusimos crear nuestra propia mesa multitáctil. El resultado fue el que podéis ver aquí:

Estas investigaciones me llevaron a trabajar durante mis últimos años de carrera en una empresa llamada Syderis, donde desarrollamos una mesa multitáctil (la solución hardware y software). Aquí puede verse un resultado mucho más profesional tras un año de trabajo para crear el software (NUIFramework) que permitía desarrollar aplicaciones 2D de forma simple pero que estuviesen aceleradas por DirectX:

Mesa multitáctil

Este fue el producto estrella pero también realizamos otros proyectos interesantes como un probador de gafas virtuales. La mayoría de los proyectos en los que allí trabajé fueron desarrollados utilizando principalmente XNA y Visual Studio.

Una vez terminé mis estudios de Ingeniería, me hicieron una oferta para venir a Madrid a trabajar en Plainconcepts y siguiendo con el refrán de rodearse de gente muy buena para aprender y mejorar me vine a trabajar a Madrid. El primer proyecto en el que participe fue un juego para Windows Phone 7 llamado “ByeByeBrain”. El juego tuvo una aceptación bastante buena y por ello nos pusimos manos a la obra para crear una segunda versión mejorada y que pudiéramos explotar en varias plataformas. Nació entonces “ByeByeBrain App-ocalypse” y la idea de crear un motor multiplataforma llamado Wave Engine que nos permitiera desarrollar videojuegos para todas las plataformas móviles utilizando C# y Visual Studio.

ByeByeBrain App-ocalypse fue desarrollado en unos 6 meses por un equipo de 3 personas aunque tuvimos varias colaboraciones importantes en diferentes aspectos del proyecto, programación, diseño o testing.

ByeByeBrain App-ocalypse

 

En todas estas aventuras conocí a muchísima gente de la cual he aprendido mucho, “creo que la mejor forma de aprender y superarse es rodeándote de gente muy buena”, también creo que en esto he tenido siempre mucha suerte ya que todos estos proyectos hubiese sido imposible realizarlos por mi solo.

 

¿Como habéis conseguido que ByeByeBrain App-ocalypse se convierta en un proyecto tan conocido?

La versión anterior fue vendida en la tienda a un coste de 2€, pero en la nueva versión intentamos probar con otros modelos de negocio para juegos, en concreto el llamado Freemium. Consiste en que el usuario obtiene el producto totalmente gratis y los beneficios se obtiene a través de publicidad ó In-app purchase (pagos dentro de la aplicación) para conseguir objetos o desbloquear partes del juego más rápidamente. Este modelo ha funcionado mucho mejor, ya que el usuario prefiere probar tu producto y si le gusta entonces pagar. También se consigue una difusión mucho mayor que con el modelo tradicional, esto ayuda a que más usuarios prueben tu producto y ellos mismos hagan publicidad de él ya que es gratis, ¿por qué no probarlo?.

Solo en Windows Phone 7 superamos ya las 500.000 descargas y seguimos creciendo a muy buen ritmo:

image

 

Con este juego también hemos aprendido mucho sobre publicidad y sobre cómo utilizarla en tus juegos. El uso de publicidad es muy llamativo ya que supone coste 0 para el usuario final y sin embargo todo el tiempo que pase jugando se puede convertir en beneficios,. Existen muchísimos proveedores de publicidad y cada uno tiene sus puntos fuertes. Cosas en las que creo que importante fijarse antes de elegir un proveedor de publicidad:

– Cobertura: Cuando lanzamos un juego a alguno de los marketplace actuales, queremos que esté disponible a nivel mundial para aumentar el número potencial de usuarios. Si eliges un proveedor de publicidad que solo tenga anunciantes de un país estarás perdiendo mucho dinero.

– Modelo de ingreso: Existen proveedores que pagan por impresión, es decir cada vez que un anuncio se muestra, otros pagan por click en el banner y otros consisten en redes en las que entras y por cada anuncio que se muestre en tu aplicación, tu aplicación aparecerá en otros juegos de esa misma red.

– FillRate: Este indicador nos informa de cual es la probabilidad de que cuando tu aplicación pida un anuncio el servidor se lo de. Este valor es muy importante ya que si el fillrate es muy bajo por muy alto que sea el coste por impresión o por click los ingresos serán menores. Para mejorar esta cifra algunos proveedores de publicidad empiezan a integrar varios proveedores de publicidad en uno.

Además de todo esto, no existe un proveedor ideal para todas las plataformas y en todos los países. La mejor solución pasa por tener un mapa de proveedores a nivel mundial para cada plataforma, y que la aplicación sea capaz de conectarse con un proveedor u otro dependiendo del país en el que se encuentre. Como podéis imaginar no es nada fácil intentar maximizar los beneficios a nivel mundial, a nosotros nos ha costado mucho tiempo desarrollar nuestro mapa de proveedores.

 

¿En qué proyectos con Kinect has participado recientemente?

Pues recientemente en Plainconcepts desarrollamos un conjunto de demos para el evento de lanzamiento a nivel mundial de Visual Studio 2012. Una de estas aplicaciones consistía en un cliente para Windows 8 que permitía poder seguir la charla en un entorno virtual,. Me explico, durante la charla todos los movimientos del ponente eran capturados con un Kinect que estaba conectado a un servidor, así como las slides y el audio. Este servidor además tenía todos los datos de la conferencia previamente cargados como descripción, foto del ponente o cual era su avatar de Xbox.

Los asistentes al evento podían descargar en sus portátiles y tablets con Windows 8 un cliente que se conectaba al servidor de la sala por red local, el cual encontraban gracias a un servicio de Azure donde previamente se había registrado el servidor indicando cual era su IP local. Una vez establecida la conexión con el servidor de la sala, el cliente nos mostraba toda la información sobre la conferencia, las slides, el audio y el avatar de Xbox moviéndose en tiempo real tal y como el ponente lo hacía por el escenario. Al final del evento podías elegir si lanzar flores o tomates al ponente según si te había o no gustado la conferencia, y el servidor recogía el veredicto del público.

Este proyecto se realizó utilizando una mezcla de tecnologías de Microsoft, C#, XAML, C++ y DirectX. El resultado puede observarse en las siguientes imágenes:

 

W8 AppW8 App

¿Cual es el proyecto con Kinect más espectacular que has visto?

Pues sin duda el proyecto de Microsoft Research llamado Kinect Fusion,. Consiste en utilizar el Kinect a modo de escáner 3d dinámico, puedes escanear objetos en tiempo real o directamente la habitación en la que te encuentras para luego interactuar con ella. Aquí tenéis un vídeo:

La verdad es que las posibilidades de este dispositivo aun no han sido explotadas y en proyectos como este son en los que nos damos cuenta. ¿Quién nos iba a decir hace 10 años que con un dispositivo en un solo punto del espacio íbamos a poder detectar la profundidad/distancia a la que se encuentran los objetos con mayor precisión de lo que lo hacen las personas?. Los campos en los que este dispositivo se está empezando a utilizar no tiene limites. En robótica por ejemplo ha producido una revolución, ahora es mucho más fácil que los robots sean capaces de conocer su situación dentro de una habitación, distancia con los objetos o hacerse un mapa de su entorno.

¿Proyectos futuros?

Actualmente me encuentro dentro de un departamento de Plainconcepts con un grupo de compañeros espectacular de los que aprendo mucho. Estamos centrados en el desarrollo Wave Engine, un motor multiplataforma para dispositivos móviles (smartphone, tablets, consolas portátiles) con el objetivo de hacer la vida más fácil a los desarrolladores de videojuegos en estas plataformas, eliminando las barreras tecnológicas que supone desarrollar un juego para las múltiples plataformas existentes (diferentes lenguajes, librerías, IDEs, SO).

Utilizando Wave Engine el desarrollador puede centrarse en lo que realmente le gusta que es crear su juego. El problema de la plataforma destino es resuelto implícitamente al usar nuestra tecnología. El desarrollador crea una vez su juego utilizando C# y Visual Studio, y luego el proyecto se compila automáticamente para las diferentes plataformas (Windows, Windows 8, Windows Phone, IPad, IPhone, Android, KindleFire, Silverlight). Todo esto no es una idea feliz, llevamos 2 años trabajando en hacer realidad este sueño y por ejemplo ByeByeBrain App-ocalypse ya ha sido desarrollado utilizando esta tecnología, así como múltiples test y demos. Aquí os puedo adelantar algunas imágenes:

imageimage

Ahora mismo nos encontramos en fase Alpha privada (muy pocas personas tienen acceso a esta tecnología), pero para principios de 2013 se va a producir una apuesta muy fuerte por este proyecto, pasaremos a Beta pública y se realizará una ampliación importante del equipo. Esperamos que pronto empecéis a oír más sobre este proyecto y lo probéis para ayudarnos a mejorarlo.

 

En resumen, la oportunidad de desarrollar juegos y aplicaciones para Xbox y Kinect con Visual Studio, podría decir que es casi ilimitada. El potencial de estas tecnologías en muy diversos ámbitos: entretenimiento, salud, medio ambiente, ayuda a las personas discapacitadas, marketing, ventas y un largo etcétera, apenas ha empezado a descubrirse. Puedo decir sin miedo a equivocarme, que las empresas y organizaciones que sepan ver la oportunidad que representa la combinación de estas tecnologías tendrán ante sí un verdadero filón.

Have a good day.

Memoria compartida entre C# struct

 

Problemática:

Imaginemos que estamos desarrollando un graphics ó physics engine en C#, el cual queremos que sea multiplataformas por lo que luego lo usaremos contra diferentes APIs como XNA, OpenTK y SharpDX. Trabajaremos con nuestras propias estructuras para Matrix4x4 ó Vector3 por ejemplo y luego las tendremos que traducir a las structs concretas de cada API.

 

La struct para Vector3 de nuestro motor puede ser tan simple como:

  1. public struct Vector
  2. {
  3.     public float X;
  4.     public float Y;
  5.     public float Z;
  6.  
  7.     public Vector(float x, float y, float z)
  8.     {
  9.         this.X = x;
  10.         this.Y = y;
  11.         this.Z = z;
  12.     }
  13.  
  14.     public override string ToString()
  15.     {
  16.         return string.Format("(X:{0} Y:{1} Z:{2})", X, Y, Z);
  17.     }
  18. }

 

Primera idea:

Por ejemplo para usarlo con XNA, una vez hayamos hecho todos nuestros cálculos para tener la posición de nuestro player necesitaríamos pasar el resultado a un método XNA del estilo:

  1. public void XNAMethod(ref Microsoft.Xna.Framework.Vector3 vector)

 

Como se puede observar el parámetro que se le pasa es un Vector3, y esta estructura a pesar de tener los mismos campos que nuestra estructura no puede “castearse” directamente:

 

  1. Vector myVector = new Vector(1,2,3);
  2. Vector3 xnaVector = (Vector3)myVector;

Error    1    Cannot convert type ‘WindowsGame1.Vector’ to ‘Microsoft.Xna.Framework.Vector3’   

 

(El error nos recuerda que no se puede hacer el casting explícito).

 

Tendremos que crear una struct nueva y copiar todos los valores, por ejemplo:

  1. Vector myVector = new Vector(1,2,3);
  2. Vector3 xnaVector = new Vector3() { X = myVector.X, Y = myVector.Y, Z = myVector.Z };

Pros:

Nos permite intercambiar datos entre las struct de XNA y las de nuestro motor.

 

Contra:

Esta solución no mola mucho ya que hay que hacer un new y duplicar las mismas struct en memoria.

 

 

Segunda idea:

Una segunda idea pasa por intentar algo similar a los Union de C++, lo cual nos permite tener dos tipos distintos de estructuras compartiendo la misma zona de memoria. Esto en C# se hace con los Attributes StructLayout y FieldOffset que encontramos dentro del namespace InteropServices:

  1. using System.Runtime.InteropServices;
  2. [StructLayout(LayoutKind.Explicit)]
  3. public struct UnionVector
  4. {
  5.     [FieldOffset(0)]
  6.     public Microsoft.Xna.Framework.Vector3 XnaVector;
  7.     [FieldOffset(0)]
  8.     public float X;
  9.     [FieldOffset(4)]
  10.     public float Y;
  11.     [FieldOffset(8)]
  12.     public float Z;
  13. }

 

Con esto le estamos indicando cómo hacer el matching a nivel de bytes, ya que especificamos el offset de cada uno de los campos (un float son 4 bytes). Podemos indicar la conversión con varias struct, no solo con una:

  1. [StructLayout(LayoutKind.Explicit)]
  2. public struct UnionVector
  3. {
  4.     [FieldOffset(0)]
  5.     public Microsoft.Xna.Framework.Vector3 XnaVector;
  6.     [FieldOffset(0)]
  7.     public OpenTK.Vector3 OpenTkVector;
  8.     [FieldOffset(0)]
  9.     public float X;
  10.     [FieldOffset(4)]
  11.     public float Y;
  12.     [FieldOffset(8)]
  13.     public float Z;
  14. }

 

Luego lo único que tenemos que hacer es inicializar la estructura y leer los datos con el tiempo concreto que necesitemos en cada caso, es importante destacar que están trabajando sobre la misma zona de memoria:

  1. UnionVector myVector = new UnionVector() { X = 1, Y = 2, Z = 3};
  2. Vector3 xnaVector = myVector.XnaVector;
  3. OpenTK.Vector3 oTKVector3 = myVector.OpenTkVector;

 

Pros:

Trabajamos con la misma zona de memoria, por lo que nos evitamos el duplicado de struct y el coste de los new.

 

Contra:

En la dll en la que definamos nuestra struct Union tenemos que referenciar a las dlls de XNA, OpenTK, por lo que el código de nuestras struct no es independiente, y si en alguna plataforma no podemos enlazar las dlls necesarias no funcionará.

 

 

Tercera idea:

Si la plataforma soporta código unsafe y las struct son idénticas a nivel de bytes podemos intentar referenciar con punteros del tipo necesario a las zonas de memoria donde están nuestras struct:

  1. Vector vector = new Vector(1, 2, 3);
  2. unsafe
  3. {
  4.     // Creamos un puntero de nuestro tipo de struct que apunta a la dirección de memoria de nuestro vector.
  5.     Vector* pVector = &vector;
  6.    
  7.     // Creamos un puntero del tipo de struct de XNA y hacemos un casting entre punteros.
  8.     Vector3* pVector3 = (Vector3 *)pVector;
  9.     // Los métodos de XNA no permiten pasar como parámetro un puntero, por lo que necesitamos la última conversión.
  10.     Vector3 vector3 = *pVector3;
  11.     XNAMethod(vector3);
  12. }

 

Todo esto que hemos hecho por pasos se puede simplicar en una sola linea de la siguiente forma:

  1. Vector vector = new Vector(1, 2, 3);
  2. unsafe
  3. {
  4.     XNAMethod(*(Vector3*)&vector);
  5. }

 

Estas llamadas pasan los parámetros de tipo struct por valor haciendo una copia de la struct en la llamada, muchos de los métodos de XNA tienen las sobrecargas para evitar esto usando ref, out.

La llamada a un método de este tipo quedaría:

  1. Vector vector = new Vector(1, 2, 3);
  2. unsafe
  3. {
  4.     XNAMethod(ref *(Vector3*)&vector);
  5. }

 

Pros:

Evitamos los new para convertir los tipos, evitamos el duplicado en memoria y no es necesario referenciar en la dll donde creamos nuestra struct a las dlls donde se encuentra las otras structs a las que queremos convertir.

 

Contra:

No todas las plataformas soportan código unsafe, por ejemplo en Windows Phone 7 no podríamos hacer esta optimización.

 

 

Espero que todo esto os mole tanto como a mi XD.

 

Saludos

WP7 Problemas de rendimiento con Struct (II / II)

Realicemos un pequeño benchmark para poder evaluar cual es la pérdida de rendimiento obtenido, el cual se producirá por:

CPU: Tiempo empleado en gestionar las llamadas (realizar las copias de las estructuras)

Para el test vamos a crear un proyecto para Windows Phone 7.1 con Visual Studio, y luego lo analizaremos con el profiler de WP7 corriendo directamente en un dispositivo real.

La demo será muy simple, colocaremos un bucle en el método Draw que es donde más operaciones con struct se suelen hacer, y dentro del bucle realizaremos una llamada a un método al cual se le pasarán 50 matrices por parámetro (esto es para probar que realmente el problema viene por las copias de struct).

 

//Iteraciones será igual a 5000 para simular carga de trabajo
for (int i = 0; i < iterations; i++)
{
   //Llamada al método
}

Crearemos dos versiones del método que va dentro del bucle, en uno pasaremos las matrices por valor (lo normal) y en el otro lo haremos por referencia.

 

Por valor:

private void UpdateMatrices(Matrix world0, Matrix view0, Matrix projection0,
                                     Matrix world1, Matrix view1, Matrix projection1,
                                     Matrix world2, Matrix view2, Matrix projection2,
                                     Matrix world3, Matrix view3, Matrix projection3,
                                     Matrix world4, Matrix view4, Matrix projection4,
                                     Matrix world5, Matrix view5, Matrix projection5,
                                     Matrix world6, Matrix view6, Matrix projection6,
                                     Matrix world7, Matrix view7, Matrix projection7,
                                     Matrix world8, Matrix view8, Matrix projection8,
                                     Matrix world9, Matrix view9, Matrix projection9,
                                     Matrix world10, Matrix view10, Matrix projection10,
                                     Matrix world11, Matrix view11, Matrix projection11,
                                     Matrix world12, Matrix view12, Matrix projection12,
                                     Matrix world13, Matrix view13, Matrix projection13,
                                     Matrix world14, Matrix view14, Matrix projection14,
                                     Matrix world15, Matrix view15, Matrix projection15,
                                     Matrix world16, Matrix view16, Matrix projection16,
                                     Matrix world17, Matrix view17, Matrix projection17,
                                     Matrix world18, Matrix view18, Matrix projection18,
                                     Matrix world19, Matrix view19, Matrix projection19,
                                     Matrix world20, Matrix view20, Matrix projection20,
                                     Matrix world21, Matrix view21, Matrix projection21,
                                     Matrix world22, Matrix view22, Matrix projection22,
                                     Matrix world23, Matrix view23, Matrix projection23,
                                     Matrix world24, Matrix view24, Matrix projection24,
                                     Matrix world25, Matrix view25, Matrix projection25,
                                     Matrix world26, Matrix view26, Matrix projection26,
                                     Matrix world27, Matrix view27, Matrix projection27,
                                     Matrix world28, Matrix view28, Matrix projection28,
                                     Matrix world29, Matrix view29, Matrix projection29,
                                     Matrix world30, Matrix view30, Matrix projection30,
                                     Matrix world31, Matrix view31, Matrix projection31,
                                     Matrix world32, Matrix view32, Matrix projection32,
                                     Matrix world33, Matrix view33, Matrix projection33,
                                     Matrix world34, Matrix view34, Matrix projection34,
                                     Matrix world35, Matrix view35, Matrix projection35,
                                     Matrix world36, Matrix view36, Matrix projection36,
                                     Matrix world37, Matrix view37, Matrix projection37,
                                     Matrix world38, Matrix view38, Matrix projection38,
                                     Matrix world39, Matrix view39, Matrix projection39,
                                     Matrix world40, Matrix view40, Matrix projection40,
                                     Matrix world41, Matrix view41, Matrix projection41,
                                     Matrix world42, Matrix view42, Matrix projection42,
                                     Matrix world43, Matrix view43, Matrix projection43,
                                     Matrix world44, Matrix view44, Matrix projection44,
                                     Matrix world45, Matrix view45, Matrix projection45,
                                     Matrix world46, Matrix view46, Matrix projection46,
                                     Matrix world47, Matrix view47, Matrix projection47,
                                     Matrix world48, Matrix view48, Matrix projection48,
                                     Matrix world49, Matrix view49, Matrix projection49)
        {
                   //Dentro no hacemos nada
         }

Por referencia:

private void UpdateMatricesOptimized(ref Matrix world0, ref Matrix view0, ref  Matrix projection0,
                                             ref Matrix world1, ref Matrix view1, ref  Matrix projection1,
                                             ref Matrix world2, ref Matrix view2, ref  Matrix projection2,
                                             ref Matrix world3, ref Matrix view3, ref  Matrix projection3,
                                             ref Matrix world4, ref Matrix view4, ref  Matrix projection4,
                                             ref Matrix world5, ref Matrix view5, ref  Matrix projection5,
                                             ref Matrix world6, ref Matrix view6, ref  Matrix projection6,
                                             ref Matrix world7, ref Matrix view7, ref  Matrix projection7,
                                             ref Matrix world8, ref Matrix view8, ref  Matrix projection8,
                                             ref Matrix world9, ref Matrix view9, ref  Matrix projection9,
                                             ref Matrix world10, ref Matrix view10, ref Matrix projection10,
                                             ref Matrix world11, ref Matrix view11, ref Matrix projection11,
                                             ref Matrix world12, ref Matrix view12, ref Matrix projection12,
                                             ref Matrix world13, ref Matrix view13, ref Matrix projection13,
                                             ref Matrix world14, ref Matrix view14, ref Matrix projection14,
                                             ref Matrix world15, ref Matrix view15, ref Matrix projection15,
                                             ref Matrix world16, ref Matrix view16, ref Matrix projection16,
                                             ref Matrix world17, ref Matrix view17, ref Matrix projection17,
                                             ref Matrix world18, ref Matrix view18, ref Matrix projection18,
                                             ref Matrix world19, ref Matrix view19, ref Matrix projection19,
                                             ref Matrix world20, ref Matrix view20, ref Matrix projection20,
                                             ref Matrix world21, ref Matrix view21, ref Matrix projection21,
                                             ref Matrix world22, ref Matrix view22, ref Matrix projection22,
                                             ref Matrix world23, ref Matrix view23, ref Matrix projection23,
                                             ref Matrix world24, ref Matrix view24, ref Matrix projection24,
                                             ref Matrix world25, ref Matrix view25, ref Matrix projection25,
                                             ref Matrix world26, ref Matrix view26, ref Matrix projection26,
                                             ref Matrix world27, ref Matrix view27, ref Matrix projection27,
                                             ref Matrix world28, ref Matrix view28, ref Matrix projection28,
                                             ref Matrix world29, ref Matrix view29, ref Matrix projection29,
                                             ref Matrix world30, ref Matrix view30, ref Matrix projection30,
                                             ref Matrix world31, ref Matrix view31, ref Matrix projection31,
                                             ref Matrix world32, ref Matrix view32, ref Matrix projection32,
                                             ref Matrix world33, ref Matrix view33, ref Matrix projection33,
                                             ref Matrix world34, ref Matrix view34, ref Matrix projection34,
                                             ref Matrix world35, ref Matrix view35, ref Matrix projection35,
                                             ref Matrix world36, ref Matrix view36, ref Matrix projection36,
                                             ref Matrix world37, ref Matrix view37, ref Matrix projection37,
                                             ref Matrix world38, ref Matrix view38, ref Matrix projection38,
                                             ref Matrix world39, ref Matrix view39, ref Matrix projection39,
                                             ref Matrix world40, ref Matrix view40, ref Matrix projection40,
                                             ref Matrix world41, ref Matrix view41, ref Matrix projection41,
                                             ref Matrix world42, ref Matrix view42, ref Matrix projection42,
                                             ref Matrix world43, ref Matrix view43, ref Matrix projection43,
                                             ref Matrix world44, ref Matrix view44, ref Matrix projection44,
                                             ref Matrix world45, ref Matrix view45, ref Matrix projection45,
                                             ref Matrix world46, ref Matrix view46, ref Matrix projection46,
                                             ref Matrix world47, ref Matrix view47, ref Matrix projection47,
                                             ref Matrix world48, ref Matrix view48, ref Matrix projection48,
                                             ref Matrix world49, ref Matrix view49, ref Matrix projection49)
        {
            //No hacemos nada
        }

Resultados:

Una vez tenemos el benchmark montado, lo ejecutamos sobre un dispositivo y le conectamos el profiler, y este es el resultado:

WP_001061

 

 

WP_001062

 

 

 

benchmark

 

En la primera zona, estaba realizando las 5000 iteraciones llamando al método que hace el paso de parámetros por valor, y en la segunda etapa estaba usando el método que realiza el paso de parámetros por referencia.

 

Conclusiones

Como podemos ver, el paso de parámetros se está “comiendo” literalmente la CPU en la primera zona. Es decir, la CPU está trabajando al 100% solo en realizar las llamadas con las respectivas copias de structs, no hay tiempo de CPU para el código de nuestro juego. Sin embargo, en la segunda zona, realizando la misma operación, tenemos una CPU liberada para ejecutar nuestro código.

 

Este último dato asusta bastante, así que es importante que todo desarrollador de Windows Phone 7 lo conozca y tenga todo esto presente.

 

Proyecto de Visual Studio

WP7 Problemas de rendimiento con Struct (I / II)

Recientemente he pasado bastantes horas optimizando código para Windows Phone 7, y una de las primeras cosas que hay que tener en mente es que el CLR(Common Language Runtime) de WP7 no es el de Windows. Incluso en la última versión (Mango) en la que han hecho grandes mejoras, como por ejemplo que ahora el GC (Garbage Collector) ofrece un rendimiento más decente con 2 generaciones, o como al aprovechamiento de las instrucciones SIMD.

Otro factor a tener en cuenta para todos esos desarrolladores que se están acercando a esta plataforma para desarrollar juegos con XNA, es que a diferencia de la mayoría de APIs que existen en .NET, en XNA se aprovechan mucho las struct para aquellos tipos con tiempo de vida relativamente bajo, algo muy común en el desarrollo de videojuegos. Por ejemplo Vector3, Vector2, Color, Matrix, etc son tipos muy usados y se suelen crear y destruir estructuras de estos tipos muy rápidamente.

Si estos tipos estuviesen definidos como class, se perdería muchísimo tiempo en reservar espacio en el Heap para luego destruirlo, por este motivo se definen como struct que permite una creación y destrucción rápida cuando se almacenan en el Stack.

Podemos llegar a tener grandes problemas de rendimiento si no comprendemos bien cómo se comportan los struct en el .NET Framework. A diferencia de las instancias de class, las instancias de struct se suelen copiar cuando las pasamos como parámetros en métodos, y es importante saber detectar todos los escenarios en los que esto ocurre.

 

Métodos:

Cuando hacemos llamadas como esta:

Matrix a, b, c;
c = Math.Multiply( a, b );

Debemos saber que Matrix está definida como struct y el paso por parámetro de estas en los métodos se hace por valor (por defecto). Dentro del cuerpo del método Multiply no se tiene la referencia a las matrices a y b, si no que dentro se trabaja con una copia de ellas, es decir, se crean dos nuevas matrices y se rellenan con los 16 (4*4) valores de las matrices fuentes. Además, la matriz que devuelve el método también se devuelve como copia, si hacemos un cálculo de cuánta memoria se ha tenido que reservar para hacer esto:

4 bytes / float

16 floats / Matrix

3 copias

3 * 16 * 4 = 192 Bytes

Esto puede llegar a ser un problema, por la basura generada, la cual pueda acelerar una pasada del GC y por el coste de CPU necesario para hacer las copias.

Properties:

Las propiedades no suelen ser muy recomendables para combinarlas con las struct, ya que su set y get tienen un comportamiento parecido al de los métodos y las estructuras se van a pasar por valor (copia).

Por ejemplo:

//Matrices en propiedades
public Matrix World {get; set;}
public Matrix View {get; set;}
public Matrix Projection {get; set;}
 
//Vectores como propiedad
public Vector3 Position {get; set;}

Es común usar estas propiedades como si fuesen campos, sin tener en cuenta que cada vez que se accede a ellas internamente es como si se ejecutara un “método get” que devuelve una copia de la struct:

Vector3: 3 elementos * 4 bytes / float = 12 bytes

Matrix: 16 elementos * 4 bytes / float = 64 bytes

Este comportamiento también es algo que conociéndolo se puede aprovechar. Por ejemplo, Microsoft en su API para Matrix ha colocado un atributo estático llamada identity. De forma que cada vez que queremos inicializar una matriz con la matriz identidad hacemos:

Matrix mat1 = Matrix.Identity;
mat1.M11 = 0;
 
Matrix mat2 = Matrix.Identity;

 

Cada vez que llamamos a Matrix.Identity nos devuelve una copia de la matriz identidad almacenada dentro de la clase Matrix de forma estática. Si no fuese así y lo que nos devolviera fuese una referencia, con la segunda línea estaríamos modificando la matriz identidad y al asignársela a mat2 esta ya no representaría a la matriz identidad.

La API de XNA está llena de esto, ejemplos:

Color.Red, Color.White, Color.Black

Vector3.Up, Vector3.Zero

Operadores:

Los operadores internamente también tienen un comportamiento similar y sufrimos copias para el paso de parámetros cuando usamos struct.

Por ejemplo:

//Operador multiplicación
Matrix worldViewProj = world * view * proj;

Internamente es como si estuviésemos haciendo llamadas a un método multiplicación al cual se le pasan por parámetro las matrices de dos en dos. Esto quiere decir que se ejecutaría una llamada al método pasándole por parámetro (copia) las matrices world y view, después el resultado se devolvería también como copia, y este se volvería a pasar al método multiplicación por valor junto con la matrix proj, y el resultado se devolverá por valor también, en total:

64 bytes / Matrix * 6 copias = 384 bytes

Si añadimos que las matrices fueron definidas como propiedades tenemos que añadir una copia más por cada acceso a matrix:

3 accesos para world, view, proj, y otra copia para realizar el set y worldviewproj es propiedad

4 copias * 64 bytes / Matrix = 256 bytes

256 bytes + 384 bytes = 640 bytes

 

 

¿Qué podemos hacer para evitar todo esto?

Métodos:

Usar los métodos en los que podamos pasar los parámetros de tipo struct como referencias:

Matrix a, b, c;
 
//Paso de matrices por referencia (no se copia la estructura)
Matrix.Multiply(ref a, ref b, out c);

Propiedades:

Usar preferiblemente campos para almacenar las struct, por ejemplo las matrices World, View, Projection de la típica clase cámara, es preferible definirlas como campos públicos ya que serán consumidas desde cada objeto que se vaya a dibujar en pantalla, ahorrándonos las copias de cada acceso.

Operadores:

Sustituir los operadores por las llamadas a métodos que permiten el paso por ref de los parámetros.

//Matrix worldViewProj = world * view * projection;
 
Matrix worldViewProj, temp;
Matrix.Multiply(ref world, ref view, out temp);
Matrix.Multiply(ref temp, ref projection, out worldViewProj);

(Importante: el código aplicando estas optimizaciones suele ser menos elegante y entendible, por lo que es recomendable solo para aquellas partes “críticas” de la aplicación)